浏览代码

Add an 'exclude' option, fixing #31

Raphael Michel 10 年之前
父节点
当前提交
b9ce5b45f6
共有 4 个文件被更改,包括 124 次插入18 次删除
  1. 35 0
      doc/cfgfile.rst
  2. 35 7
      nsist/__init__.py
  3. 1 0
      nsist/configreader.py
  4. 53 11
      nsist/copymodules.py

+ 35 - 0
doc/cfgfile.rst

@@ -157,6 +157,38 @@ the line with the key:
        [Include]
        files=mydata.dat > $COMMONFILES
 
+.. describe:: exclude (optional)
+
+   Files to be excluded from your installer. This can be used to include a
+   Python library or extra directory only partially, for example to include
+   large monolithic python packages without their samples and test suites to
+   achieve a smaller installer file.
+
+   Please note:
+
+   * The parameter is expected to contain a list of files *relative to the
+     build directory*. Therefore, to include files from a package, you have to
+     start your pattern with ``pkgs/<packagename>/``.
+   * You can use `wildcard characters`_ like ``*`` or ``?``, similar to a Unix 
+     shell.
+   * If you want to exclude whole subfolders, do *not* put a path separator 
+     (e.g. ``/``) at their end.
+   * The exclude patterns are only applied to packages and to directories
+     specified using the ``files`` option. If your ``exclude`` option directly 
+     contradicts your ``files`` or ``packages`` option, the files in question
+     will be included (you can not exclude a full package/extra directory
+     or a single file listed in ``files``).
+
+   Example:
+
+   .. code-block:: ini
+
+       [Include]
+       packages=PySide
+       files=data_dir
+       exclude=pkgs/PySide/examples
+         data_dir/ignoredfile
+
 Build section
 -------------
 
@@ -179,3 +211,6 @@ Build section
    of extra files and folders to be installed. See the
    `NSIS Scripting Reference <http://nsis.sourceforge.net/Docs/Chapter4.html>`_
    for details of the format.
+
+
+.. _wildcard characters: https://docs.python.org/3/library/fnmatch.html

+ 35 - 7
nsist/__init__.py

@@ -10,6 +10,7 @@ import re
 import shutil
 from subprocess import call
 import sys
+import fnmatch
 
 PY2 = sys.version_info[0] == 2
 
@@ -78,15 +79,17 @@ class InstallerBuilder(object):
     :param str installer_name: Filename of the installer to produce
     :param str nsi_template: Path to a template NSI file to use
     """
-    def __init__(self, appname, version, shortcuts, icon=DEFAULT_ICON, 
+    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):
+                installer_name=None, nsi_template=DEFAULT_NSI_TEMPLATE,
+                exclude=None):
         self.appname = appname
         self.version = version
         self.shortcuts = shortcuts
         self.icon = icon
         self.packages = packages or []
+        self.exclude = exclude or []
         self.extra_files = extra_files or []
         self.py_version = py_version
         if not self._py_version_pattern.match(py_version):
@@ -242,7 +245,25 @@ if __name__ == '__main__':
             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)
+        copy_modules(self.packages, build_pkg_dir,
+                     py_version=self.py_version, exclude=self.exclude)
+
+    def copytree_ignore_callback(self, directory, files):
+        """This is being called back by our shutil.copytree call to implement the
+        'exclude' feature.
+        """
+        ignored = set()
+
+        # Filter by file names relative to the build directory
+        files = [os.path.join(directory, fname) for fname in files]
+
+        # Execute all patterns
+        for pattern in self.exclude:
+            ignored.update([
+                os.path.basename(fname)
+                for fname in fnmatch.filter(files, pattern)
+            ])
+        return ignored
 
     def copy_extra_files(self):
         """Copy a list of files into the build directory, and add them to
@@ -254,22 +275,28 @@ if __name__ == '__main__':
 
             if not destination:
                 destination = '$INSTDIR'
-    
+
             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)
+                if self.exclude is not None and len(self.exclude) > 0:
+                    shutil.copytree(file, target_name,
+                                    ignore=self.copytree_ignore_callback)
+                else:
+                    # Don't use our exclude callback if we don't need to,
+                    # as it slows things down.
+                    shutil.copytree(file, target_name)
                 self.install_dirs.append((basename, destination))
             else:
                 shutil.copy2(file, self.build_dir)
                 self.install_files.append((basename, destination))
-    
+
     def write_nsi(self):
         """Write the NSI file to define the NSIS installer.
-        
+
         Most of the details of this are in the template and the
         :class:`nsist.nsiswriter.NSISFileWriter` class.
         """
@@ -377,6 +404,7 @@ 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),
+            exclude = cfg.get('Include', 'exclude', fallback='').splitlines(),
         ).run()
     except InputError as e:
         logger.error("Error in config values:")

+ 1 - 0
nsist/configreader.py

@@ -70,6 +70,7 @@ CONFIG_VALIDATORS = {
     'Include': SectionValidator([
         ('packages', False),
         ('files', False),
+        ('exclude', False),
     ]),
     'Python': SectionValidator([
         ('version', True),

+ 53 - 11
nsist/copymodules.py

@@ -3,6 +3,8 @@ import shutil
 import sys
 import tempfile
 import zipfile, zipimport
+import fnmatch
+from functools import partial
 
 pjoin = os.path.join
 
@@ -63,6 +65,26 @@ def copy_zipmodule(loader, modname, target):
 
     shutil.rmtree(tempdir)
 
+def copytree_ignore_callback(excludes, pkgdir, modname, directory, files):
+    """This is being called back by our shutil.copytree call to implement the
+    'exclude' feature.
+    """
+    ignored = set()
+
+    # Filter by file names relative to the build directory
+    reldir = os.path.relpath(directory, pkgdir)
+    target = os.path.join('pkgs/', modname, reldir)
+    files = [os.path.join(target, fname) for fname in files]
+
+    # Execute all patterns
+    for pattern in excludes + ['*.pyc']:
+        ignored.update([
+            os.path.basename(fname)
+            for fname in fnmatch.filter(files, pattern)
+        ])
+
+    return ignored
+
 if not PY2:
     import importlib
     import importlib.abc
@@ -78,7 +100,7 @@ if not PY2:
             self.py_version = py_version
             self.path = path if (path is not None) else ([''] + sys.path)
 
-        def copy(self, modname, target):
+        def copy(self, modname, target, exclude):
             """Copy the importable module 'modname' to the directory 'target'.
 
             modname should be a top-level import, i.e. without any dots.
@@ -104,8 +126,18 @@ if not PY2:
                     assert basename.startswith('__init__')
                     check_package_for_ext_mods(pkgdir, self.py_version)
                     dest = os.path.join(target, modname)
-                    shutil.copytree(pkgdir, dest,
-                                    ignore=shutil.ignore_patterns('*.pyc'))
+                    if exclude is not None and len(exclude) > 0:
+                        shutil.copytree(
+                            pkgdir, dest,
+                            ignore=partial(copytree_ignore_callback, exclude, pkgdir, modname)
+                        )
+                    else:
+                        # Don't use our exclude callback if we don't need to,
+                        # as it slows things down.
+                        shutil.copytree(
+                            pkgdir, dest,
+                            ignore=shutil.ignore_patterns('*.pyc')
+                        )
                 else:
                     shutil.copy2(file, target)
 
@@ -125,7 +157,7 @@ else:
             self.path = path if (path is not None) else ([''] + sys.path)
             self.zip_paths = [p for p in self.path if zipfile.is_zipfile(p)]
 
-        def copy(self, modname, target):
+        def copy(self, modname, target, exclude):
             """Copy the importable module 'modname' to the directory 'target'.
 
             modname should be a top-level import, i.e. without any dots.
@@ -162,25 +194,35 @@ else:
             elif modtype == imp.PKG_DIRECTORY:
                 check_package_for_ext_mods(path, self.py_version)
                 dest = os.path.join(target, modname)
-                shutil.copytree(path, dest,
-                                ignore=shutil.ignore_patterns('*.pyc'))
+                if exclude is not None and len(exclude) > 0:
+                    shutil.copytree(
+                        path, dest,
+                        ignore=partial(copytree_ignore_callback, exclude, path, modname)
+                    )
+                else:
+                    # Don't use our exclude callback if we don't need to,
+                    # as it slows things down.
+                    shutil.copytree(
+                        path, dest,
+                        ignore=shutil.ignore_patterns('*.pyc')
+                    )
 
 
-def copy_modules(modnames, target, py_version, path=None):
+def copy_modules(modnames, target, py_version, path=None, exclude=None):
     """Copy the specified importable modules to the target directory.
-    
+
     By default, it finds modules in :data:`sys.path` - this can be overridden
     by passing the path parameter.
     """
     mc = ModuleCopier(py_version, path)
     files_in_target_noext = [os.path.splitext(f)[0] for f in os.listdir(target)]
-    
+
     for modname in modnames:
         if modname in files_in_target_noext:
             # Already there, no need to copy it.
             continue
-        mc.copy(modname, target)
-    
+        mc.copy(modname, target, exclude)
+
     if not modnames:
         # NSIS abhors an empty folder, so give it a file to find.
         with open(os.path.join(target, 'placeholder'), 'w') as f: