瀏覽代碼

More work on supporting bundled Python

Thomas Kluyver 10 年之前
父節點
當前提交
866be9505f
共有 6 個文件被更改,包括 86 次插入34 次删除
  1. 42 16
      nsist/__init__.py
  2. 5 0
      nsist/nsiswriter.py
  3. 4 15
      nsist/pyapp.nsi
  4. 30 0
      nsist/pyapp_installpy.nsi
  5. 3 3
      nsist/pyapp_w_pylauncher.nsi
  6. 2 0
      nsist/util.py

+ 42 - 16
nsist/__init__.py

@@ -93,27 +93,40 @@ class InstallerBuilder(object):
         self.packages = packages or []
         self.packages = packages or []
         self.exclude = [os.path.normpath(p) for p in (exclude or [])]
         self.exclude = [os.path.normpath(p) for p in (exclude or [])]
         self.extra_files = extra_files or []
         self.extra_files = extra_files or []
+
+        # Python options
         self.py_version = py_version
         self.py_version = py_version
         if not self._py_version_pattern.match(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
         self.py_bitness = py_bitness
         if py_bitness not in {32, 64}:
         if py_bitness not in {32, 64}:
             raise InputError('py_bitness', py_bitness, "32 or 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
         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.build_dir = build_dir
         self.installer_name = installer_name or self.make_installer_name()
         self.installer_name = installer_name or self.make_installer_name()
         self.nsi_template = nsi_template
         self.nsi_template = nsi_template
         if self.nsi_template is None:
         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'
                 self.nsi_template = 'pyapp_w_pylauncher.nsi'
             else:
             else:
-                self.nsi_template = 'pyapp.nsi'
+                self.nsi_template = 'pyapp_installpy.nsi'
+
         self.nsi_file = pjoin(self.build_dir, 'installer.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
         # To be filled later
         self.install_files = []
         self.install_files = []
@@ -121,6 +134,11 @@ class InstallerBuilder(object):
     
     
     _py_version_pattern = re.compile(r'\d\.\d+\.\d+$')
     _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):
     def make_installer_name(self):
         """Generate the filename of the installer exe
         """Generate the filename of the installer exe
         
         
@@ -147,8 +165,8 @@ class InstallerBuilder(object):
     def fetch_python_embeddable(self):
     def fetch_python_embeddable(self):
         arch_tag = 'amd64' if (self.py_bitness==64) else 'win32'
         arch_tag = 'amd64' if (self.py_bitness==64) else 'win32'
         filename = 'python-{}-embed-{}.zip'.format(self.py_version, arch_tag)
         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
         cache_file = get_cache_dir(ensure_existence=True) / filename
         if not cache_file.is_file():
         if not cache_file.is_file():
             logger.info('Downloading embeddable Python build...')
             logger.info('Downloading embeddable Python build...')
@@ -162,10 +180,10 @@ class InstallerBuilder(object):
             if e.errno != errno.ENOENT:
             if e.errno != errno.ENOENT:
                 raise
                 raise
 
 
-        with zipfile.ZipFile(cache_file) as z:
+        with zipfile.ZipFile(str(cache_file)) as z:
             z.extractall(python_dir)
             z.extractall(python_dir)
 
 
-        self.install_dirs.append(python_dir)
+        self.install_dirs.append(('Python', '$INSTDIR'))
 
 
     def fetch_pylauncher(self):
     def fetch_pylauncher(self):
         """Fetch the MSI for PyLauncher (required for Python2.x).
         """Fetch the MSI for PyLauncher (required for Python2.x).
@@ -261,7 +279,11 @@ if __name__ == '__main__':
                 else:
                 else:
                     shutil.copy2(sc['script'], self.build_dir)
                     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'])
                 sc['parameters'] = '"%s"' % ntpath.join('$INSTDIR', sc['script'])
                 files.add(os.path.basename(sc['script']))
                 files.add(os.path.basename(sc['script']))
 
 
@@ -375,9 +397,13 @@ if __name__ == '__main__':
         except OSError as e:
         except OSError as e:
             if e.errno != errno.EEXIST:
             if e.errno != errno.EEXIST:
                 raise e
                 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()
         self.prepare_shortcuts()
         
         

+ 5 - 0
nsist/nsiswriter.py

@@ -56,6 +56,11 @@ class NSISFileWriter(object):
             'single_shortcut': len(installerbuilder.shortcuts) == 1,
             '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):
     def write(self, target):
         """Fill out the template and write the result to 'target'.
         """Fill out the template and write the result to 'target'.
         
         

+ 4 - 15
nsist/pyapp.nsi

@@ -2,7 +2,6 @@
 !define PRODUCT_VERSION "[[ib.version]]"
 !define PRODUCT_VERSION "[[ib.version]]"
 !define PY_VERSION "[[ib.py_version]]"
 !define PY_VERSION "[[ib.py_version]]"
 !define PY_MAJOR_VERSION "[[ib.py_major_version]]"
 !define PY_MAJOR_VERSION "[[ib.py_major_version]]"
-!define PY_QUALIFIER "[[ib.py_qualifier]]"
 !define BITNESS "[[ib.py_bitness]]"
 !define BITNESS "[[ib.py_bitness]]"
 !define ARCH_TAG "[[arch_tag]]"
 !define ARCH_TAG "[[arch_tag]]"
 !define INSTALLER_NAME "[[ib.installer_name]]"
 !define INSTALLER_NAME "[[ib.installer_name]]"
@@ -19,11 +18,12 @@ RequestExecutionLevel admin
 !define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"
 !define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"
 
 
 ; UI pages
 ; UI pages
+[% block ui_pages %]
 !insertmacro MUI_PAGE_WELCOME
 !insertmacro MUI_PAGE_WELCOME
-!insertmacro MUI_PAGE_COMPONENTS
 !insertmacro MUI_PAGE_DIRECTORY
 !insertmacro MUI_PAGE_DIRECTORY
 !insertmacro MUI_PAGE_INSTFILES
 !insertmacro MUI_PAGE_INSTFILES
 !insertmacro MUI_PAGE_FINISH
 !insertmacro MUI_PAGE_FINISH
+[% endblock ui_pages %]
 !insertmacro MUI_LANGUAGE "English"
 !insertmacro MUI_LANGUAGE "English"
 [% endblock modernui %]
 [% endblock modernui %]
 
 
@@ -32,19 +32,12 @@ OutFile "${INSTALLER_NAME}"
 InstallDir "$PROGRAMFILES${BITNESS}\${PRODUCT_NAME}"
 InstallDir "$PROGRAMFILES${BITNESS}\${PRODUCT_NAME}"
 ShowInstDetails show
 ShowInstDetails show
 
 
-[% block sections %]
 Section -SETTINGS
 Section -SETTINGS
   SetOutPath "$INSTDIR"
   SetOutPath "$INSTDIR"
   SetOverwrite ifnewer
   SetOverwrite ifnewer
 SectionEnd
 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
 Section "!${PRODUCT_NAME}" sec_app
   SectionIn RO
   SectionIn RO
@@ -90,7 +83,7 @@ Section "!${PRODUCT_NAME}" sec_app
   
   
   ; Byte-compile Python files.
   ; Byte-compile Python files.
   DetailPrint "Byte-compiling Python modules..."
   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
   WriteUninstaller $INSTDIR\uninstall.exe
   ; Add ourselves to Add/remove programs
   ; Add ourselves to Add/remove programs
   WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
   WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
@@ -144,10 +137,6 @@ Function .onMouseOverSection
     GetDlgItem $R0 $R0 1043 ; description item (must be added to the UI)
     GetDlgItem $R0 $R0 1043 ; description item (must be added to the UI)
 
 
     [% block mouseover_messages %]
     [% 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
     StrCmp $0 ${sec_app} "" +2
       SendMessage $R0 ${WM_SETTEXT} 0 "STR:${PRODUCT_NAME}"
       SendMessage $R0 ${WM_SETTEXT} 0 "STR:${PRODUCT_NAME}"
     
     

+ 30 - 0
nsist/pyapp_installpy.nsi

@@ -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 %]

+ 3 - 3
nsist/pyapp_w_pylauncher.nsi

@@ -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. #]
 [# For Python 2, add the py/pyw Windows launcher. Python 3 includes it already. #]
 
 
 [% block sections %]
 [% block sections %]
-[[ super() ]]
-
 Section "PyLauncher" sec_pylauncher
 Section "PyLauncher" sec_pylauncher
     ; Check for the existence of the pyw command, skip installing if it exists
     ; Check for the existence of the pyw command, skip installing if it exists
     nsExec::Exec 'where pyw'
     nsExec::Exec 'where pyw'
@@ -15,6 +13,8 @@ Section "PyLauncher" sec_pylauncher
     Delete "$INSTDIR\launchwin${ARCH_TAG}.msi"
     Delete "$INSTDIR\launchwin${ARCH_TAG}.msi"
     SkipPylauncher:
     SkipPylauncher:
 SectionEnd
 SectionEnd
+
+[[ super() ]]
 [% endblock %]
 [% endblock %]
 
 
 [% block mouseover_messages %]
 [% block mouseover_messages %]

+ 2 - 0
nsist/util.py

@@ -49,3 +49,5 @@ def get_cache_dir(ensure_existence=False):
             # Py2 compatible equivalent of FileExistsError
             # Py2 compatible equivalent of FileExistsError
             if e.errno != errno.EEXIST:
             if e.errno != errno.EEXIST:
                 raise
                 raise
+
+    return p