Skip to content

Commit

Permalink
Add script to build Windows MSI installers
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaiter committed Apr 10, 2014
1 parent e3f84ef commit 3105ea8
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 0 deletions.
163 changes: 163 additions & 0 deletions buildmsi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#!/usr/bin/env python2.7
""" Build Windows MSI distributions.
Requirements:
- A 'windeps' folder with all of the *exe installers listed in
BINARY_PACKAGES, available at http://www.lfd.uci.edu/~gohlke/pythonlibs/
in both 32 and 64 bit.
- 'pywin32' installers for 32 and 64 bit in 'windeps' folder, , available
at http://sourceforge.net/projects/pywin32/files/pywin32/
- spreads and all of its dependencies installed in the present Python
environment (*not* with pip's '-e' flag!)
- 'pynsist' package must be installed in the present Python environment,
currently (2014/04/11) from GitHub master, not from PyPi.
- 'nsis' executable must be on $PATH ('apt-get install nsis' on Debian
systems)
Run:
$ python buildmsi.py
When complete, MSIs can be found under 'build/msi{32,64}/spreads_{version}.exe'
"""

import os
import shutil
import sys
import tempfile
import zipfile
from collections import namedtuple

import nsist
import pkg_resources
from spreads.vendor.pathlib import Path

import spreads

BINARY_PACKAGES = {
"cffi": "cffi-0.8.2.{arch}-py2.7.exe",
"MarkupSafe": "MarkupSafe-0.19.{arch}-py2.7.exe",
"PIL": "Pillow-2.4.0.{arch}-py2.7.exe",
"psutil": "psutil-2.1.0.{arch}-py2.7.exe",
"pyexiv2": "pyexiv2-0.3.2.{arch}-py2.7.exe",
"PySide": "PySide-1.2.1.{arch}-py2.7.exe",
"PyYAML": "PyYAML-3.11.{arch}-py2.7.exe",
"tornado": "tornado-3.2.{arch}-py2.7.exe",
"setuptools": "setuptools-3.4.1.{arch}-py2.7.exe"
}

SourceDep = namedtuple("SourceDep", ("project_name", "module_name"))
SOURCE_PACKAGES = [
# project module
SourceDep(*("spreads",)*2),
SourceDep(*(None, "spreadsplug")),
SourceDep(*("Flask", "flask")),
SourceDep(*("Jinja2", "jinja2")),
SourceDep(*("Werkzeug", "werkzeug")),
SourceDep(*("backports.ssl-match-hostname", "backports")),
SourceDep(*("blinker",)*2),
SourceDep(*("colorama",)*2),
SourceDep(*("futures", "concurrent")),
SourceDep(*("itsdangerous",)*2),
SourceDep(*("pyusb", "usb")),
SourceDep(*("requests",)*2),
SourceDep(*("waitress",)*2),
SourceDep(*("zipstream",)*2),
]


def extract_native_pkg(fname, pkg_dir):
zf = zipfile.ZipFile(unicode(Path('win_deps')/fname))
tmpdir = Path(tempfile.mkdtemp())
zf.extractall(unicode(tmpdir))
fpaths = []
if (tmpdir/'PLATLIB').exists():
fpaths += [p for p in (tmpdir/'PLATLIB').iterdir()]
if (tmpdir/'PURELIB').exists():
fpaths += [p for p in (tmpdir/'PURELIB').iterdir()]
for path in fpaths:
if path.is_dir():
shutil.copytree(unicode(path), unicode(pkg_dir/path.name))
else:
shutil.copy2(unicode(path), unicode(pkg_dir/path.name))
shutil.rmtree(unicode(tmpdir))


def copy_info(pkg, pkg_dir):
try:
dist = pkg_resources.get_distribution(pkg)
except pkg_resources.DistributionNotFound:
raise IOError("No distribution could be found for {0}!".format(pkg))
if dist.location == os.getcwd():
egg_name = dist.project_name
else:
egg_name = dist.egg_name()

egg_path = Path(dist.location)/(egg_name + ".egg-info")
dist_path = Path(dist.location)/(dist.project_name + "-" + dist.version
+ ".dist-info")
if egg_path.exists():
src_path = egg_path
elif dist_path.exists():
src_path = dist_path
else:
raise IOError("No egg-info or dist-info could be found for {0}!"
.format(pkg))
if src_path.is_dir():
shutil.copytree(unicode(src_path), unicode(pkg_dir/src_path.name))
else:
shutil.copy2(unicode(src_path), unicode(pkg_dir/src_path.name))


def build_msi(bitness=32):
build_path = Path('build')
if not build_path.exists():
build_path.mkdir()
pkg_dir = build_path/'pynsist_pkgs'
if pkg_dir.exists():
shutil.rmtree(unicode(pkg_dir))
pkg_dir.mkdir()
for pkg in BINARY_PACKAGES.itervalues():
arch = 'win32' if bitness == 32 else 'win-amd64'
extract_native_pkg(pkg.format(arch=arch), pkg_dir)

for pkg in (x.project_name for x in SOURCE_PACKAGES
if x.project_name is not None):
copy_info(pkg, pkg_dir)

icon = os.path.abspath("spreads.ico")
extra_files = [
os.path.join(os.path.abspath('win_deps'),
'pywin32-2.7.6{0}.exe'
.format('.amd64' if bitness == 64 else '')
)]
nsi_template = os.path.abspath("template.nsi")

# NOTE: We need to remove the working directory from sys.path to force
# pynsist to copy all of our modules, including 'spreads' and 'spreadsplug'
# from the site-packages. Additionally, we need to change into the
# build directory.
if os.getcwd() in sys.path:
sys.path.remove(os.getcwd())
os.chdir(unicode(build_path))
nsist.all_steps(
appname="spreads",
version=spreads.__version__,
script=None,
entry_point="spreads.main:main",
icon=icon,
console=False,
packages=[x.module_name for x in SOURCE_PACKAGES],
extra_files=extra_files,
py_version="2.7.6",
py_bitness=bitness,
build_dir='msi{0}'.format(bitness),
installer_name=None,
nsi_template=nsi_template
)
os.chdir('..')

if __name__ == '__main__':
if os.path.exists('spreads.egg-info'):
shutil.rmtree('spreads.egg-info')
for bitness in (32, 64):
build_msi(bitness)
Binary file added spreads.ico
Binary file not shown.
104 changes: 104 additions & 0 deletions template.nsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@

; Definitions will be added above

SetCompressor lzma

; Modern UI installer stuff
!include "MUI2.nsh"
!define MUI_ABORTWARNING
!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"

; UI pages
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_LANGUAGE "English"
; MUI end ------

Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
OutFile "${INSTALLER_NAME}"
InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
ShowInstDetails show

Section -SETTINGS
SetOutPath "$INSTDIR"
SetOverwrite ifnewer
SectionEnd

Section "Python ${PY_VERSION}" sec_py
File "python-${PY_VERSION}${ARCH_TAG}.msi"
ExecWait 'msiexec /i "$INSTDIR\python-${PY_VERSION}${ARCH_TAG}.msi" /qb ALLUSERS=1'
Delete $INSTDIR\python-${PY_VERSION}.msi
SectionEnd

;PYLAUNCHER_INSTALL
;------------------

Section "pywin32" sec_pywin32
File "pywin32-${PY_VERSION}${ARCH_TAG}.exe"
ExecWait "$INSTDIR\pywin32-${PY_VERSION}${ARCH_TAG}.exe"
Delete $INSTDIR\pywin32-${PY_VERSION}${ARCH_TAG}.exe
SectionEnd

Section "!${PRODUCT_NAME}" sec_app
SectionIn RO
File ${SCRIPT}
File ${PRODUCT_ICON}
SetOutPath "$INSTDIR\pkgs"
File /r "pkgs\*.*"
SetOutPath "$INSTDIR"
;EXTRA_FILES_INSTALL
;-------------------
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "${PY_EXE}" '"$INSTDIR\${SCRIPT}"' \
"$INSTDIR\${PRODUCT_ICON}"
WriteUninstaller $INSTDIR\uninstall.exe
; Add ourselves to Add/remove programs
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
"DisplayName" "${PRODUCT_NAME}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
"UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
"InstallLocation" "$INSTDIR"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
"DisplayIcon" "$INSTDIR\${PRODUCT_ICON}"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
"NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
"NoRepair" 1
SectionEnd

Section "Uninstall"
Delete $INSTDIR\uninstall.exe
Delete "$INSTDIR\${SCRIPT}"
Delete "$INSTDIR\${PRODUCT_ICON}"
RMDir /r "$INSTDIR\pkgs"
;EXTRA_FILES_UNINSTALL
;---------------------
Delete "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
RMDir $INSTDIR
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
SectionEnd

; Functions

Function .onMouseOverSection
; Find which section the mouse is over, and set the corresponding description.
FindWindow $R0 "#32770" "" $HWNDPARENT
GetDlgItem $R0 $R0 1043 ; description item (must be added to the UI)

StrCmp $0 ${sec_py} 0 +2
SendMessage $R0 ${WM_SETTEXT} 0 "STR:The Python interpreter. \
This is required for ${PRODUCT_NAME} to run."
;
;PYLAUNCHER_HELP
;------------------

StrCmp $0 ${sec_pywin32} 0 +2
SendMessage $R0 ${WM_SETTEXT} 0 "STR:The pywin32 library. \
This is required for ${PRODUCT_NAME} to run."

StrCmp $0 ${sec_app} "" +2
SendMessage $R0 ${WM_SETTEXT} 0 "STR:${PRODUCT_NAME}"
FunctionEnd

0 comments on commit 3105ea8

Please sign in to comment.