Skip to content

Commit

Permalink
More work on supporting bundled Python
Browse files Browse the repository at this point in the history
  • Loading branch information
takluyver committed Jun 19, 2015
1 parent c358d54 commit 866be95
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 34 deletions.
58 changes: 42 additions & 16 deletions nsist/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,34 +93,52 @@ def __init__(self, appname, version, shortcuts, icon=DEFAULT_ICON,
self.packages = packages or []
self.exclude = [os.path.normpath(p) for p in (exclude or [])]
self.extra_files = extra_files or []

# Python options
self.py_version = py_version
if not self._py_version_pattern.match(py_version):
raise InputError('py_version', py_version, "a full Python version like '3.4.0'")
if not os.environ.get('PYNSIST_PY_PRERELEASE'):
raise InputError('py_version', py_version,
"a full Python version like '3.4.0'")
self.py_bitness = py_bitness
if py_bitness not in {32, 64}:
raise InputError('py_bitness', py_bitness, "32 or 64")
self.py_major_version = self.py_qualifier = '.'.join(self.py_version.split('.')[:2])
if self.py_bitness == 32:
self.py_qualifier += '-32'
self.py_format = py_format
if py_format not in {'installer', 'bundled'}:
raise InputError('py_format', py_format, "installer or bundled")
if self._py_version_tuple >= (3, 5):
if py_format not in {'installer', 'bundled'}:
raise InputError('py_format', py_format, "installer or bundled")
else:
if py_format != 'installer':
raise InputError('py_format', py_format, "installer (for Python < 3.5)")

# Build details
self.build_dir = build_dir
self.installer_name = installer_name or self.make_installer_name()
self.nsi_template = nsi_template
if self.nsi_template is None:
if self.py_version < '3.3':
if self.py_format == 'bundled':
self.nsi_template = 'pyapp.nsi'
elif self._py_version_tuple < (3, 3):
self.nsi_template = 'pyapp_w_pylauncher.nsi'
else:
self.nsi_template = 'pyapp.nsi'
self.nsi_template = 'pyapp_installpy.nsi'

self.nsi_file = pjoin(self.build_dir, 'installer.nsi')
self.py_major_version = self.py_qualifier = '.'.join(self.py_version.split('.')[:2])
if self.py_bitness == 32:
self.py_qualifier += '-32'

# To be filled later
self.install_files = []
self.install_dirs = []

_py_version_pattern = re.compile(r'\d\.\d+\.\d+$')

@property
def _py_version_tuple(self):
parts = self.py_version.split('.')
return int(parts[0]), int(parts[1])

def make_installer_name(self):
"""Generate the filename of the installer exe
Expand All @@ -147,8 +165,8 @@ def fetch_python(self):
def fetch_python_embeddable(self):
arch_tag = 'amd64' if (self.py_bitness==64) else 'win32'
filename = 'python-{}-embed-{}.zip'.format(self.py_version, arch_tag)
url = 'https://www.python.org/ftp/python/{}/{}'.format(self.py_version,
filename)
url = 'https://www.python.org/ftp/python/{}/{}'.format(
re.sub(r'b\d+$', '', self.py_version), filename)
cache_file = get_cache_dir(ensure_existence=True) / filename
if not cache_file.is_file():
logger.info('Downloading embeddable Python build...')
Expand All @@ -162,10 +180,10 @@ def fetch_python_embeddable(self):
if e.errno != errno.ENOENT:
raise

with zipfile.ZipFile(cache_file) as z:
with zipfile.ZipFile(str(cache_file)) as z:
z.extractall(python_dir)

self.install_dirs.append(python_dir)
self.install_dirs.append(('Python', '$INSTDIR'))

def fetch_pylauncher(self):
"""Fetch the MSI for PyLauncher (required for Python2.x).
Expand Down Expand Up @@ -261,7 +279,11 @@ def prepare_shortcuts(self):
else:
shutil.copy2(sc['script'], self.build_dir)

sc['target'] = 'py' if sc['console'] else 'pyw'
if self.py_format == 'bundled':
target = '$INSTDIR\Python\python{}.exe'
else:
target = 'py{}'
sc['target'] = target.format('' if sc['console'] else 'w')
sc['parameters'] = '"%s"' % ntpath.join('$INSTDIR', sc['script'])
files.add(os.path.basename(sc['script']))

Expand Down Expand Up @@ -375,9 +397,13 @@ def run(self, makensis=True):
except OSError as e:
if e.errno != errno.EEXIST:
raise e
self.fetch_python()
if self.py_version < '3.3':
self.fetch_pylauncher()

if self.py_format == 'bundled':
self.fetch_python_embeddable()
else:
self.fetch_python()
if self.py_version < '3.3':
self.fetch_pylauncher()

self.prepare_shortcuts()

Expand Down
5 changes: 5 additions & 0 deletions nsist/nsiswriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ def __init__(self, template_file, installerbuilder, definitions=None):
'single_shortcut': len(installerbuilder.shortcuts) == 1,
}

if installerbuilder.py_format == 'bundled':
self.namespace['python'] = '"$INSTDIR\\Python\\python"'
else:
self.namespace['python'] = 'py -{}'.format(installerbuilder.py_qualifier)

def write(self, target):
"""Fill out the template and write the result to 'target'.
Expand Down
19 changes: 4 additions & 15 deletions nsist/pyapp.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
!define PRODUCT_VERSION "[[ib.version]]"
!define PY_VERSION "[[ib.py_version]]"
!define PY_MAJOR_VERSION "[[ib.py_major_version]]"
!define PY_QUALIFIER "[[ib.py_qualifier]]"
!define BITNESS "[[ib.py_bitness]]"
!define ARCH_TAG "[[arch_tag]]"
!define INSTALLER_NAME "[[ib.installer_name]]"
Expand All @@ -19,11 +18,12 @@ RequestExecutionLevel admin
!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"

; UI pages
[% block ui_pages %]
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
[% endblock ui_pages %]
!insertmacro MUI_LANGUAGE "English"
[% endblock modernui %]

Expand All @@ -32,19 +32,12 @@ OutFile "${INSTALLER_NAME}"
InstallDir "$PROGRAMFILES${BITNESS}\${PRODUCT_NAME}"
ShowInstDetails show

[% block sections %]
Section -SETTINGS
SetOutPath "$INSTDIR"
SetOverwrite ifnewer
SectionEnd

Section "Python ${PY_VERSION}" sec_py
File "python-${PY_VERSION}${ARCH_TAG}.msi"
DetailPrint "Installing Python ${PY_MAJOR_VERSION}, ${BITNESS} bit"
ExecWait 'msiexec /i "$INSTDIR\python-${PY_VERSION}${ARCH_TAG}.msi" \
/qb ALLUSERS=1 TARGETDIR="$COMMONFILES${BITNESS}\Python\${PY_MAJOR_VERSION}"'
Delete $INSTDIR\python-${PY_VERSION}${ARCH_TAG}.msi
SectionEnd
[% block sections %]

Section "!${PRODUCT_NAME}" sec_app
SectionIn RO
Expand Down Expand Up @@ -90,7 +83,7 @@ Section "!${PRODUCT_NAME}" sec_app

; Byte-compile Python files.
DetailPrint "Byte-compiling Python modules..."
nsExec::ExecToLog 'py -${PY_QUALIFIER} -m compileall -q "$INSTDIR\pkgs"'
nsExec::ExecToLog '[[ python ]] -m compileall -q "$INSTDIR\pkgs"'
WriteUninstaller $INSTDIR\uninstall.exe
; Add ourselves to Add/remove programs
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
Expand Down Expand Up @@ -144,10 +137,6 @@ Function .onMouseOverSection
GetDlgItem $R0 $R0 1043 ; description item (must be added to the UI)

[% block mouseover_messages %]
StrCmp $0 ${sec_py} 0 +2
SendMessage $R0 ${WM_SETTEXT} 0 "STR:The Python interpreter. \
This is required for ${PRODUCT_NAME} to run."

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

Expand Down
30 changes: 30 additions & 0 deletions nsist/pyapp_installpy.nsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[% extends "pyapp.nsi" %]

[% block ui_pages %]
[# We only need to add COMPONENTS, but they have to be in order #]
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
[% endblock ui_pages %]

[% block sections %]
Section "Python ${PY_VERSION}" sec_py
File "python-${PY_VERSION}${ARCH_TAG}.msi"
DetailPrint "Installing Python ${PY_MAJOR_VERSION}, ${BITNESS} bit"
ExecWait 'msiexec /i "$INSTDIR\python-${PY_VERSION}${ARCH_TAG}.msi" \
/qb ALLUSERS=1 TARGETDIR="$COMMONFILES${BITNESS}\Python\${PY_MAJOR_VERSION}"'
Delete $INSTDIR\python-${PY_VERSION}${ARCH_TAG}.msi
SectionEnd

[[ super() ]]
[% endblock sections %]

[% block mouseover_messages %]
StrCmp $0 ${sec_py} 0 +2
SendMessage $R0 ${WM_SETTEXT} 0 "STR:The Python interpreter. \
This is required for ${PRODUCT_NAME} to run."

[[ super() ]]
[% endblock mouseover_messages %]
6 changes: 3 additions & 3 deletions nsist/pyapp_w_pylauncher.nsi
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
[% extends "pyapp.nsi" %]
[% extends "pyapp_installpy.nsi" %]
[# For Python 2, add the py/pyw Windows launcher. Python 3 includes it already. #]

[% block sections %]
[[ super() ]]

Section "PyLauncher" sec_pylauncher
; Check for the existence of the pyw command, skip installing if it exists
nsExec::Exec 'where pyw'
Expand All @@ -15,6 +13,8 @@ Section "PyLauncher" sec_pylauncher
Delete "$INSTDIR\launchwin${ARCH_TAG}.msi"
SkipPylauncher:
SectionEnd

[[ super() ]]
[% endblock %]

[% block mouseover_messages %]
Expand Down
2 changes: 2 additions & 0 deletions nsist/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ def get_cache_dir(ensure_existence=False):
# Py2 compatible equivalent of FileExistsError
if e.errno != errno.EEXIST:
raise

return p

0 comments on commit 866be95

Please sign in to comment.