فهرست منبع

Implement Python 2.7 compatibility

- Add 'optionparser' backport as a dependency when installing with Python 2
- Use Python 2.7.6 as default version when building with Python 2
- Include pylauncher in MSI when building with Python 2
Johannes Baiter 11 سال پیش
والد
کامیت
e674bed2f9
5فایلهای تغییر یافته به همراه180 افزوده شده و 58 حذف شده
  1. 42 8
      nsist/__init__.py
  2. 97 47
      nsist/copymodules.py
  3. 24 2
      nsist/nsiswriter.py
  4. 6 0
      nsist/template.nsi
  5. 11 1
      setup.py

+ 42 - 8
nsist/__init__.py

@@ -1,12 +1,21 @@
 """Build NSIS installers for Python applications.
 """
+import errno
 import logging
 import os
 import shutil
 from subprocess import check_output, call
 import sys
-from urllib.request import urlretrieve
-if os.name == 'nt':
+
+PY2 = sys.version_info[0] == 2
+
+if PY2:
+    from urllib import urlretrieve
+else:
+    from urllib.request import urlretrieve
+if os.name == 'nt' and PY2:
+    import _winreg as winreg
+elif os.name == 'nt':
     import winreg
 else:
     winreg = None
@@ -18,7 +27,7 @@ pjoin = os.path.join
 logger = logging.getLogger(__name__)
 
 _PKGDIR = os.path.abspath(os.path.dirname(__file__))
-DEFAULT_PY_VERSION = '3.3.2'
+DEFAULT_PY_VERSION = '2.7.6' if PY2 else '3.4.0'
 DEFAULT_BUILD_DIR = pjoin('build', 'nsis')
 DEFAULT_NSI_TEMPLATE = pjoin(_PKGDIR, 'template.nsi')
 DEFAULT_ICON = pjoin(_PKGDIR, 'glossyorb.ico')
@@ -47,9 +56,25 @@ def fetch_python(version=DEFAULT_PY_VERSION, bitness=DEFAULT_BITNESS,
         keys_file = os.path.join(_PKGDIR, 'python-pubkeys.txt')
         check_output(['gpg', '--import', keys_file])
         check_output(['gpg', '--verify', target+'.asc'])
-    except FileNotFoundError:
+    except OSError:
         logger.warn("GPG not available - could not check signature of {0}".format(target))
 
+
+def fetch_pylauncher(bitness=DEFAULT_BITNESS, destination=DEFAULT_BUILD_DIR):
+    """Fetch the MSI for PyLauncher (required for Python2.x).
+
+    It will be placed in the destination directory.
+    """
+    arch_tag = '.amd64' if (bitness == 64) else ''
+    url = ("https://bitbucket.org/vinay.sajip/pylauncher/downloads/"
+           "launchwin{0}.msi".format(arch_tag))
+    target = pjoin(destination, 'launchwin{0}.msi'.format(arch_tag))
+    if os.path.isfile(target):
+        logger.info('PyLauncher MSI already in build directory.')
+        return
+    logger.info('Downloading PyLauncher MSI...')
+    urlretrieve(url, target)
+
 SCRIPT_TEMPLATE = """#!python{qualifier}
 import sys
 sys.path.insert(0, 'pkgs')
@@ -113,9 +138,9 @@ def run_nsis(nsi_file):
         else:
             makensis = 'makensis'
         return call([makensis, nsi_file])
-    except FileNotFoundError:
-        # FileNotFoundError catches both the registry lookup failing and call()
-        # not finding makensis
+    except OSError:
+        # OSError catches both the registry lookup failing and call() not
+        # finding makensis
         print("makensis was not found. Install NSIS and try again.")
         print("http://nsis.sourceforge.net/Download")
         return 1
@@ -131,8 +156,17 @@ def all_steps(appname, version, script=None, entry_point=None, icon=DEFAULT_ICON
     """
     installer_name = installer_name or make_installer_name(appname, version)
 
-    os.makedirs(build_dir, exist_ok=True)
+    try:
+        os.makedirs(build_dir)
+    except OSError as e:
+        if e.errno == errno.EEXIST:
+            # It's okay if the build directory already exists
+            pass
+        else:
+            raise e
     fetch_python(version=py_version, bitness=py_bitness, destination=build_dir)
+    if PY2:
+        fetch_pylauncher(bitness=py_bitness, destination=build_dir)
 
     if entry_point is not None:
         if script is not None:

+ 97 - 47
nsist/copymodules.py

@@ -1,57 +1,107 @@
-import importlib, importlib.abc, importlib.machinery
 import os
 import shutil
 import sys
 import zipfile, zipimport
 
-class ModuleCopier:
-    """Finds and copies importable Python modules and packages.
-    
-    This uses importlib to locate modules.
-    """
-    def __init__(self, path=None):
-        self.path = path if (path is not None) else ([''] + sys.path)
-    
-    def copy(self, modname, target):
-        """Copy the importable module 'modname' to the directory 'target'.
-        
-        modname should be a top-level import, i.e. without any dots. Packages
-        are always copied whole.
-        
-        This can currently copy regular filesystem files and directories, and
-        extract modules and packages from appropriately structured zip files.
+PY2 = sys.version_info[0] == 2
+
+
+def copy_zipmodule(loader, modname, target):
+    file = loader.get_filename(modname)
+    prefix = loader.archive + '/' + loader.prefix
+    assert file.startswith(prefix)
+    path_in_zip = file[len(prefix):]
+    zf = zipfile.ZipFile(loader.archive)
+    if loader.is_package(modname):
+        pkgdir, basename = path_in_zip.rsplit('/', 1)
+        assert basename.startswith('__init__')
+        pkgfiles = [f for f in zf.namelist() if f.startswith(pkgdir)]
+        zf.extractall(target, pkgfiles)
+    else:
+        zf.extract(path_in_zip, target)
+
+if not PY2:
+    import importlib
+    import importlib.abc
+    import importlib.machinery
+
+    class ModuleCopier:
+        """Finds and copies importable Python modules and packages.
+
+        This uses importlib to locate modules.
+        """
+        def __init__(self, path=None):
+            self.path = path if (path is not None) else ([''] + sys.path)
+
+        def copy(self, modname, target):
+            """Copy the importable module 'modname' to the directory 'target'.
+
+            modname should be a top-level import, i.e. without any dots.
+            Packages are always copied whole.
+
+            This can currently copy regular filesystem files and directories,
+            and extract modules and packages from appropriately structured zip
+            files.
+            """
+            loader = importlib.find_loader(modname, self.path)
+            if loader is None:
+                raise ImportError('Could not find %s' % modname)
+            pkg = loader.is_package(modname)
+
+            if isinstance(loader, importlib.machinery.ExtensionFileLoader):
+                shutil.copy2(loader.path, target)
+
+            elif isinstance(loader, importlib.abc.FileLoader):
+                file = loader.get_filename(modname)
+                if pkg:
+                    pkgdir, basename = os.path.split(file)
+                    assert basename.startswith('__init__')
+                    dest = os.path.join(target, modname)
+                    shutil.copytree(pkgdir, dest,
+                                    ignore=shutil.ignore_patterns('*.pyc'))
+                else:
+                    shutil.copy2(file, target)
+
+            elif isinstance(loader, zipimport.zipimporter):
+                copy_zipmodule(loader, modname, target)
+else:
+    import imp
+
+    class ModuleCopier:
+        """Finds and copies importable Python modules and packages.
+
+        This uses importlib to locate modules.
         """
-        loader = importlib.find_loader(modname, self.path)
-        if loader is None:
-            raise ImportError('Could not find %s' % modname)
-        pkg = loader.is_package(modname)
-
-        if isinstance(loader, importlib.machinery.ExtensionFileLoader):
-            shutil.copy2(loader.path, target)
-
-        elif isinstance(loader, importlib.abc.FileLoader):
-            file = loader.get_filename(modname)
-            if pkg:
-                pkgdir, basename = os.path.split(file)
-                assert basename.startswith('__init__')
+        def __init__(self, path=None):
+            self.path = path if (path is not None) else ([''] + sys.path)
+
+        def copy(self, modname, target):
+            """Copy the importable module 'modname' to the directory 'target'.
+
+            modname should be a top-level import, i.e. without any dots.
+            Packages are always copied whole.
+
+            This can currently copy regular filesystem files and directories,
+            and extract modules and packages from appropriately structured zip
+            files.
+            """
+            modinfo = imp.find_module(modname, self.path)
+
+            if modinfo[2][2] in (imp.PY_SOURCE, imp.C_EXTENSION):
+                shutil.copy2(modinfo[1], target)
+
+            elif modinfo[2][2] == imp.PKG_DIRECTORY:
                 dest = os.path.join(target, modname)
-                shutil.copytree(pkgdir, dest, ignore=shutil.ignore_patterns('*.pyc'))
-            else:                
-                shutil.copy2(file, target)
-        
-        elif isinstance(loader, zipimport.zipimporter):
-            file = loader.get_filename(modname)
-            prefix = loader.archive + '/' + loader.prefix
-            assert file.startswith(prefix)
-            path_in_zip = file[len(prefix):]
-            zf = zipfile.ZipFile(loader.archive)
-            if pkg:
-                pkgdir, basename = path_in_zip.rsplit('/', 1)
-                assert basename.startswith('__init__')
-                pkgfiles = [f for f in zf.namelist() if f.startswith(pkgdir)]
-                zf.extractall(target, pkgfiles)
-            else:
-                zf.extract(path_in_zip, target)
+                shutil.copytree(modinfo[1], dest,
+                                ignore=shutil.ignore_patterns('*.pyc'))
+
+            elif any(p.endswith('.zip') for p in self.path):
+                for fpath in (p for p in self.path if p.endswith('.zip')):
+                    loader = zipimport.zipimporter(fpath)
+                    if loader.find_module(modname) is None:
+                        continue
+                    copy_zipmodule(loader, modname, target)
+
 
 def copy_modules(modnames, target, path=None):
     """Copy the specified importable modules to the target directory.

+ 24 - 2
nsist/nsiswriter.py

@@ -1,4 +1,8 @@
 import re
+import sys
+
+PY2 = sys.version_info[0] == 2
+
 
 class NSISFileWriter(object):
     """Write an .nsi script file by filling in a template.
@@ -16,7 +20,11 @@ class NSISFileWriter(object):
                 ';EXTRA_FILES_INSTALL': self.write_extra_files_install,
                 ';EXTRA_FILES_UNINSTALL': self.write_extra_files_uninstall,
         }
-    
+        if PY2:
+            self.write_after_line.update({
+                ';PYLAUNCHER_INSTALL': self.write_pylauncher_install,
+                ';PYLAUNCHER_HELP': self.write_pylauncher_help})
+
     def write_definitions(self, f):
         """Write definition lines at the start of the file.
         
@@ -50,7 +58,21 @@ class NSISFileWriter(object):
                 f.write(indent+'RMDir /r "$INSTDIR\{}"\n'.format(file))
             else:
                 f.write(indent+'Delete "$INSTDIR\{}"\n'.format(file))
-    
+
+    def write_pylauncher_install(self, f, indent):
+        f.write(indent+"Section \"PyLauncher\" sec_pylauncher\n")
+        f.write(indent+"File \"launchwin${ARCH_TAG}.msi\"\n")
+        f.write(indent+"ExecWait 'msiexec /i "
+                "\"$INSTDIR\launchwin${ARCH_TAG}.msi\" /qb ALLUSERS=1'\n")
+        f.write(indent+"Delete $INSTDIR\launchwin${ARCH_TAG}.msi\n")
+        f.write(indent+"SectionEnd\n")
+
+    def write_pylauncher_help(self, f, indent):
+        f.write(indent+"StrCmp $0 ${sec_pylauncher} 0 +2\n")
+        f.write(indent+"SendMessage $R0 ${WM_SETTEXT} 0 "
+                "\"STR:The Python launcher. \\\n")
+        f.write(indent+"This is required for ${PRODUCT_NAME} to run.\"")
+
     def write(self, target):
         """Fill out the template and write the result to 'target'.
         

+ 6 - 0
nsist/template.nsi

@@ -33,6 +33,9 @@ Section "Python ${PY_VERSION}" sec_py
   Delete $INSTDIR\python-${PY_VERSION}.msi
 SectionEnd
 
+;PYLAUNCHER_INSTALL
+;------------------
+
 Section "!${PRODUCT_NAME}" sec_app
   SectionIn RO
   File ${SCRIPT}
@@ -82,6 +85,9 @@ Function .onMouseOverSection
     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_app} "" +2
       SendMessage $R0 ${WM_SETTEXT} 0 "STR:${PRODUCT_NAME}"

+ 11 - 1
setup.py

@@ -1,5 +1,14 @@
+import sys
 from distutils.core import setup
 
+PY2 = sys.version_info[0] == 2
+if PY2:
+    requirements = [
+        'configparser == 3.3.0r2'
+    ]
+else:
+    requirements = []
+
 with open('README.rst', 'r') as f:
     readme=f.read()
 
@@ -25,5 +34,6 @@ setup(name='pynsist',
           'Topic :: Software Development',
           'Topic :: System :: Installation/Setup',
           'Topic :: System :: Software Distribution',
-      ]
+      ],
+      install_requires=requirements
 )