소스 검색

Refactor main functionality into a class

Thomas Kluyver 11 년 전
부모
커밋
984e31bbaf
2개의 변경된 파일202개의 추가작업 그리고 196개의 파일을 삭제
  1. 189 183
      nsist/__init__.py
  2. 13 13
      nsist/nsiswriter.py

+ 189 - 183
nsist/__init__.py

@@ -39,204 +39,210 @@ if os.name == 'nt' and sys.maxsize == (2**63)-1:
 else:
     DEFAULT_BITNESS = 32
 
-def fetch_python(version=DEFAULT_PY_VERSION, bitness=DEFAULT_BITNESS,
-                 destination=DEFAULT_BUILD_DIR):
-    """Fetch the MSI for the specified version of Python.
+class InstallerBuilder(object):
+    def __init__(self, appname, version, shortcuts, icon=DEFAULT_ICON, 
+                packages=None, extra_files=None, py_version=DEFAULT_PY_VERSION,
+                py_bitness=DEFAULT_BITNESS, build_dir=DEFAULT_BUILD_DIR,
+                installer_name=None, nsi_template=DEFAULT_NSI_TEMPLATE):
+        self.appname = appname
+        self.version = version
+        self.shortcuts = shortcuts
+        self.icon = icon
+        self.packages = packages or []
+        self.extra_files = extra_files or []
+        self.py_version = py_version
+        self.py_bitness = py_bitness
+        self.build_dir = build_dir
+        self.installer_name = installer_name or self.make_installer_name()
+        self.nsi_template = nsi_template
+        self.nsi_file = pjoin(self.build_dir, 'installer.nsi')
+        
+        # To be filled later
+        self.install_files = []
+        self.install_dirs = []
     
-    It will be placed in the destination directory, and validated using GPG
-    if possible.
-    """
-    arch_tag = '.amd64' if (bitness==64) else ''
-    url = 'http://python.org/ftp/python/{0}/python-{0}{1}.msi'.format(version, arch_tag)
-    target = pjoin(destination, 'python-{0}{1}.msi'.format(version, arch_tag))
-    if os.path.isfile(target):
-        logger.info('Python MSI already in build directory.')
-        return
-    logger.info('Downloading Python MSI...')
-    urlretrieve(url, target)
-
-    urlretrieve(url+'.asc', target+'.asc')
-    try:
-        keys_file = os.path.join(_PKGDIR, 'python-pubkeys.txt')
-        check_output(['gpg', '--import', keys_file])
-        check_output(['gpg', '--verify', target+'.asc'])
-    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).
+    def make_installer_name(self):
+        """Generate the filename of the installer exe
+        
+        e.g. My_App_1.0.exe
+        """
+        s = self.appname + '_' + self.version + '.exe'
+        return s.replace(' ', '_')
 
-    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, os
-scriptdir, script = os.path.split(__file__)
-pkgdir = os.path.join(scriptdir, 'pkgs')
-sys.path.insert(0, pkgdir)
-os.environ['PYTHONPATH'] = pkgdir + os.pathsep + os.environ.get('PYTHONPATH', '')
-
-def excepthook(etype, value, tb):
-    "Write unhandled exceptions to a file rather than exiting silently."
-    import traceback
-    with open(os.path.join(scriptdir, script+'.log'), 'w') as f:
-        traceback.print_exception(etype, value, tb, file=f)
-sys.excepthook = excepthook
-
-from {module} import {func}
-{func}()
-"""
-
-def write_script(entrypt, python_version, bitness, target):
-    """Write a launcher script from a 'module:function' entry point
+    def fetch_python(self):
+        """Fetch the MSI for the specified version of Python.
+        
+        It will be placed in the destination directory, and validated using GPG
+        if possible.
+        """
+        version = self.py_version
+        arch_tag = '.amd64' if (self.py_bitness==64) else ''
+        url = 'http://python.org/ftp/python/{0}/python-{0}{1}.msi'.format(version, arch_tag)
+        target = pjoin(self.build_dir, 'python-{0}{1}.msi'.format(version, arch_tag))
+        if os.path.isfile(target):
+            logger.info('Python MSI already in build directory.')
+            return
+        logger.info('Downloading Python MSI...')
+        urlretrieve(url, target)
     
-    python_version and bitness are used to write an appropriate shebang line
-    for the PEP 397 Windows launcher.
-    """
-    qualifier = '.'.join(python_version.split('.')[:2])
-    if bitness == 32:
-        qualifier += '-32'
-    module, func = entrypt.split(":")
-    with open(target, 'w') as f:
-        f.write(SCRIPT_TEMPLATE.format(qualifier=qualifier, module=module, func=func))
+        urlretrieve(url+'.asc', target+'.asc')
+        try:
+            keys_file = os.path.join(_PKGDIR, 'python-pubkeys.txt')
+            check_output(['gpg', '--import', keys_file])
+            check_output(['gpg', '--verify', target+'.asc'])
+        except OSError:
+            logger.warn("GPG not available - could not check signature of {0}".format(target))
 
-def prepare_shortcuts(shortcuts, py_version, py_bitness, build_dir):
-    files = set()
-    for scname, sc in shortcuts.items():
-        if sc.get('entry_point'):
-            sc['script'] = script = scname.replace(' ', '_') + '.launch.py'
-            write_script(sc['entry_point'], py_version, py_bitness,
-                            pjoin(build_dir, script))
-        else:
-            shutil.copy2(sc['script'], build_dir)
-    
-        shutil.copy2(sc['icon'], build_dir)
-        sc['icon'] = os.path.basename(sc['icon'])
-        sc['script'] = os.path.basename(sc['script'])
-        files.add(sc['script'])
-        files.add(sc['icon'])
+    def fetch_pylauncher(self):
+        """Fetch the MSI for PyLauncher (required for Python2.x).
     
-    return files
+        It will be placed in the destination directory.
+        """
+        arch_tag = '.amd64' if (self.py_bitness == 64) else ''
+        url = ("https://bitbucket.org/vinay.sajip/pylauncher/downloads/"
+               "launchwin{0}.msi".format(arch_tag))
+        target = pjoin(self.build_dir, '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)
 
-def copy_extra_files(filelist, build_dir):
-    """Copy a list of files into the build directory.
+    SCRIPT_TEMPLATE = """#!python{qualifier}
+    import sys, os
+    scriptdir, script = os.path.split(__file__)
+    pkgdir = os.path.join(scriptdir, 'pkgs')
+    sys.path.insert(0, pkgdir)
+    os.environ['PYTHONPATH'] = pkgdir + os.pathsep + os.environ.get('PYTHONPATH', '')
     
-    Returns two lists, files and directories, with only the base filenames
-    (i.e. no leading path components)
-    """
-    files, directories = [], []
-    for file in filelist:
-        file = file.rstrip('/\\')
-        basename = os.path.basename(file)
-
-        if os.path.isdir(file):
-            target_name = pjoin(build_dir, basename)
-            if os.path.isdir(target_name):
-                shutil.rmtree(target_name)
-            elif os.path.exists(target_name):
-                os.unlink(target_name)
-            shutil.copytree(file, target_name)
-            directories.append(basename)
-        else:
-            shutil.copy2(file, build_dir)
-            files.append(basename)
-
-    return files, directories
-
-def make_installer_name(appname, version):
-    """Generate the filename of the installer exe
+    def excepthook(etype, value, tb):
+        "Write unhandled exceptions to a file rather than exiting silently."
+        import traceback
+        with open(os.path.join(scriptdir, script+'.log'), 'w') as f:
+            traceback.print_exception(etype, value, tb, file=f)
+    sys.excepthook = excepthook
     
-    e.g. My_App_1.0.exe
+    from {module} import {func}
+    {func}()
     """
-    s = appname + '_' + version + '.exe'
-    return s.replace(' ', '_')
-
-def run_nsis(nsi_file):
-    """Runs makensis using the specified .nsi file
     
-    Returns the exit code.
-    """
-    try:
-        if os.name == 'nt':
-            makensis = pjoin(winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\NSIS'),
-                                     'makensis.exe')
-        else:
-            makensis = 'makensis'
-        return call([makensis, nsi_file])
-    except OSError as e:
-        # This should catch either the registry key or makensis being absent
-        if e.errno == errno.ENOENT:
-            print("makensis was not found. Install NSIS and try again.")
-            print("http://nsis.sourceforge.net/Download")
-            return 1
+    def write_script(self, entrypt, target):
+        """Write a launcher script from a 'module:function' entry point
+        
+        python_version and bitness are used to write an appropriate shebang line
+        for the PEP 397 Windows launcher.
+        """
+        qualifier = '.'.join(self.py_version.split('.')[:2])
+        if self.py_bitness == 32:
+            qualifier += '-32'
+        module, func = entrypt.split(":")
+        with open(target, 'w') as f:
+            f.write(self.SCRIPT_TEMPLATE.format(qualifier=qualifier, module=module, func=func))
 
-def all_steps(appname, version, shortcuts, icon=DEFAULT_ICON, 
-                packages=None, extra_files=None, py_version=DEFAULT_PY_VERSION,
-                py_bitness=DEFAULT_BITNESS, build_dir=DEFAULT_BUILD_DIR,
-                installer_name=None, nsi_template=DEFAULT_NSI_TEMPLATE):
-    """Run all the steps to build an installer.
+    def prepare_shortcuts(self):
+        files = set()
+        for scname, sc in self.shortcuts.items():
+            if sc.get('entry_point'):
+                sc['script'] = script = scname.replace(' ', '_') + '.launch.py'
+                self.write_script(sc['entry_point'], pjoin(self.build_dir, script))
+            else:
+                shutil.copy2(sc['script'], self.build_dir)
+        
+            shutil.copy2(sc['icon'], self.build_dir)
+            sc['icon'] = os.path.basename(sc['icon'])
+            sc['script'] = os.path.basename(sc['script'])
+            files.add(sc['script'])
+            files.add(sc['icon'])
     
-    For details of the parameters, see the documentation for the config file
-    options.
-    """
-    installer_name = installer_name or make_installer_name(appname, version)
+        self.install_files.extend(files)
+    
+    def prepare_packages(self):
+        logger.info("Copying packages into build directory...")
+        build_pkg_dir = pjoin(self.build_dir, 'pkgs')
+        if os.path.isdir(build_pkg_dir):
+            shutil.rmtree(build_pkg_dir)
+        if os.path.isdir('pynsist_pkgs'):
+            shutil.copytree('pynsist_pkgs', build_pkg_dir)
+        else:
+            os.mkdir(build_pkg_dir)
+        copy_modules(self.packages, build_pkg_dir, py_version=self.py_version)
 
-    try:
-        os.makedirs(build_dir)
-    except OSError as e:
-        if e.errno != errno.EEXIST:
-            raise e
-    fetch_python(version=py_version, bitness=py_bitness, destination=build_dir)
-    if PY2:
-        fetch_pylauncher(bitness=py_bitness, destination=build_dir)
+    def copy_extra_files(self):
+        """Copy a list of files into the build directory, and add them to
+        install_files or install_dirs as appropriate.
+        """
+        for file in self.extra_files:
+            file = file.rstrip('/\\')
+            basename = os.path.basename(file)
     
-    shortcuts_files = prepare_shortcuts(shortcuts, py_version, py_bitness, build_dir)
+            if os.path.isdir(file):
+                target_name = pjoin(self.build_dir, basename)
+                if os.path.isdir(target_name):
+                    shutil.rmtree(target_name)
+                elif os.path.exists(target_name):
+                    os.unlink(target_name)
+                shutil.copytree(file, target_name)
+                self.install_dirs.append(basename)
+            else:
+                shutil.copy2(file, self.build_dir)
+                self.install_files.append(basename)
     
-    # Packages
-    logger.info("Copying packages into build directory...")
-    build_pkg_dir = pjoin(build_dir, 'pkgs')
-    if os.path.isdir(build_pkg_dir):
-        shutil.rmtree(build_pkg_dir)
-    if os.path.isdir('pynsist_pkgs'):
-        shutil.copytree('pynsist_pkgs', build_pkg_dir)
-    else:
-        os.mkdir(build_pkg_dir)
-    copy_modules(packages or [], build_pkg_dir, py_version=py_version)
+    def write_nsi(self):
+        nsis_writer = NSISFileWriter(self.nsi_template, installerbuilder=self,
+            definitions = {'PRODUCT_NAME': self.appname,
+                           'PRODUCT_VERSION': self.version,
+                           'PY_VERSION': self.py_version,
+                           'PRODUCT_ICON': os.path.basename(self.icon),
+                           'INSTALLER_NAME': self.installer_name,
+                           'ARCH_TAG': '.amd64' if (self.py_bitness==64) else '',
+                          },
+            )
+        
+        nsis_writer.write(self.nsi_file)    
 
-    nsis_writer = NSISFileWriter(nsi_template,
-        definitions = {'PRODUCT_NAME': appname,
-                       'PRODUCT_VERSION': version,
-                       'PY_VERSION': py_version,
-                       'PRODUCT_ICON': os.path.basename(icon),
-                       'INSTALLER_NAME': installer_name,
-                       'ARCH_TAG': '.amd64' if (py_bitness==64) else '',
-                      }
-        )
-    # Extra files
-    nsis_writer.files, nsis_writer.directories = \
-                            copy_extra_files(extra_files or [], build_dir)
-
-    nsis_writer.files.extend(shortcuts_files)
-    nsis_writer.shortcuts = shortcuts
-    
-    nsi_file = pjoin(build_dir, 'installer.nsi')
-    nsis_writer.write(nsi_file)
+    def run_nsis(self):
+        """Runs makensis using the specified .nsi file
+        
+        Returns the exit code.
+        """
+        try:
+            if os.name == 'nt':
+                makensis = pjoin(winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\NSIS'),
+                                         'makensis.exe')
+            else:
+                makensis = 'makensis'
+            return call([makensis, self.nsi_file])
+        except OSError as e:
+            # This should catch either the registry key or makensis being absent
+            if e.errno == errno.ENOENT:
+                print("makensis was not found. Install NSIS and try again.")
+                print("http://nsis.sourceforge.net/Download")
+                return 1
 
-    exitcode = run_nsis(nsi_file)
+    def run(self):
+        """Run all the steps to build an installer.
+        """
+        try:
+            os.makedirs(self.build_dir)
+        except OSError as e:
+            if e.errno != errno.EEXIST:
+                raise e
+        self.fetch_python()
+        if PY2:
+            self.fetch_pylauncher()
+        
+        self.prepare_shortcuts()
+        
+        # Packages
+        
+        
+        # Extra files
+        self.copy_extra_files()
     
-    if not exitcode:
-        logger.info('Installer written to %s', pjoin(build_dir, installer_name))
+        exitcode = self.run_nsis()
+        
+        if not exitcode:
+            logger.info('Installer written to %s', pjoin(self.build_dir, self.installer_name))
 
 def read_shortcuts_config(cfg):
     
@@ -271,7 +277,7 @@ def main(argv=None):
     :func:`all_steps` with the extracted information.
     """
     logger.setLevel(logging.INFO)
-    logger.addHandler(logging.StreamHandler())
+    logger.handlers = [logging.StreamHandler()]
     
     import argparse
     argp = argparse.ArgumentParser(prog='pynsist')
@@ -290,7 +296,7 @@ def main(argv=None):
         logger.error(str(e))
         sys.exit(1)
     appcfg = cfg['Application']
-    all_steps(
+    InstallerBuilder(
         appname = appcfg['name'],
         version = appcfg['version'],
         icon = appcfg.get('icon', DEFAULT_ICON),
@@ -302,4 +308,4 @@ def main(argv=None):
         build_dir = cfg.get('Build', 'directory', fallback=DEFAULT_BUILD_DIR),
         installer_name = cfg.get('Build', 'installer_name', fallback=None),
         nsi_template = cfg.get('Build', 'nsi_template', fallback=DEFAULT_NSI_TEMPLATE),
-    )
+    ).run()

+ 13 - 13
nsist/nsiswriter.py

@@ -7,17 +7,15 @@ PY2 = sys.version_info[0] == 2
 class NSISFileWriter(object):
     """Write an .nsi script file by filling in a template.
     """
-    def __init__(self, template_file, definitions=None):
+    def __init__(self, template_file, installerbuilder, definitions=None):
         """Instantiate an NSISFileWriter
         
         :param str template_file: Path to the .nsi template
         :param dict definitions: Mapping of name to value (values will be quoted)
         """
         self.template_file = template_file
+        self.installerbuilder = installerbuilder
         self.definitions = definitions or {}
-        self.files = []
-        self.directories = []
-        self.shortcuts = {}
         self.template_fields = {
                 ';INSTALL_FILES': self.files_install,
                 ';INSTALL_DIRECTORIES': self.dirs_install,
@@ -62,18 +60,19 @@ class NSISFileWriter(object):
     # These return an iterable of lines to fill after a given template field
 
     def files_install(self):
-        for file in self.files:
+        for file in self.installerbuilder.install_files:
             yield 'File "{}"'.format(file)
 
     def dirs_install(self):
-        for dir in self.directories:
+        for dir in self.installerbuilder.install_dirs:
             yield 'SetOutPath "$INSTDIR\{}"'.format(dir)
             yield 'File /r "{}\*.*"'.format(dir)
         yield 'SetOutPath "$INSTDIR"'
     
     def shortcuts_install(self):
-        if len(self.shortcuts) == 1:
-            scname, sc = next(iter(self.shortcuts.items()))
+        shortcuts = self.installerbuilder.shortcuts
+        if len(shortcuts) == 1:
+            scname, sc = next(iter(shortcuts.items()))
             yield 'CreateShortCut "$SMPROGRAMS\{}.lnk" "{}" \'"$INSTDIR\{}"\' \\'.format(\
                     scname, ('py' if sc['console'] else 'pyw'), sc['script'])
             yield '    "$INSTDIR\{}"'.format(sc['icon'])
@@ -81,22 +80,23 @@ class NSISFileWriter(object):
         
         # Multiple shortcuts - make a folder
         yield 'CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"'
-        for scname, sc in self.shortcuts.items():
+        for scname, sc in shortcuts.items():
             yield 'CreateShortCut "$SMPROGRAMS\${{PRODUCT_NAME}}\{}.lnk" "{}" \\'.format(\
                     scname, ('py' if sc['console'] else 'pyw'))
             yield '    \'"$INSTDIR\{}"\' "$INSTDIR\{}"'.format(sc['script'], sc['icon'])
 
     def files_uninstall(self):
-        for file in self.files:
+        for file in self.installerbuilder.install_files:
             yield 'Delete "$INSTDIR\{}"'.format(file)
 
     def dirs_uninstall(self):
-        for dir in self.directories:
+        for dir in self.installerbuilder.install_dirs:
             yield 'RMDir /r "$INSTDIR\{}"'.format(dir)
     
     def shortcuts_uninstall(self):
-        if len(self.shortcuts) == 1:
-            scname = next(iter(self.shortcuts))
+        shortcuts = self.installerbuilder.shortcuts
+        if len(shortcuts) == 1:
+            scname = next(iter(shortcuts))
             yield 'Delete "$SMPROGRAMS\{}.lnk"'.format(scname)
         else:
             yield 'RMDir /r "$SMPROGRAMS\${PRODUCT_NAME}"'