copymodules.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import os
  2. import shutil
  3. import sys
  4. import tempfile
  5. import zipfile, zipimport
  6. import fnmatch
  7. from functools import partial
  8. pjoin = os.path.join
  9. PY2 = sys.version_info[0] == 2
  10. running_python = '.'.join(str(x) for x in sys.version_info[:2])
  11. class ExtensionModuleMismatch(ImportError):
  12. pass
  13. extensionmod_errmsg = """Found an extension module that will not be usable on %s:
  14. %s
  15. Put Windows packages in pynsist_pkgs/ to avoid this."""
  16. def check_ext_mod(path, target_python):
  17. """If path is an extension module, check that it matches target platform.
  18. It should be for Windows and we should be running on the same version
  19. of Python that we're targeting. Raises ExtensionModuleMismatch if not.
  20. Does nothing if path is not an extension module.
  21. """
  22. if path.endswith('.so'):
  23. raise ExtensionModuleMismatch(extensionmod_errmsg % ('Windows', path))
  24. elif path.endswith('.pyd') and not target_python.startswith(running_python):
  25. # TODO: From Python 3.2, extension modules can restrict themselves
  26. # to a stable ABI. Can we detect this?
  27. raise ExtensionModuleMismatch(extensionmod_errmsg % ('Python '+target_python, path))
  28. def check_package_for_ext_mods(path, target_python):
  29. """Walk the directory path, calling :func:`check_ext_mod` on each file.
  30. """
  31. for dirpath, dirnames, filenames in os.walk(path):
  32. for filename in filenames:
  33. check_ext_mod(os.path.join(path, dirpath, filename), target_python)
  34. def copy_zipmodule(loader, modname, target):
  35. """Copy a module or package out of a zip file to the target directory."""
  36. file = loader.get_filename(modname)
  37. assert file.startswith(loader.archive)
  38. path_in_zip = file[len(loader.archive+'/'):]
  39. zf = zipfile.ZipFile(loader.archive)
  40. # If the packages are in a subdirectory, extracting them recreates the
  41. # directory structure from the zip file. So extract to a temp dir first,
  42. # and then copy the modules to target.
  43. tempdir = tempfile.mkdtemp()
  44. if loader.is_package(modname):
  45. # Extract everything in a folder
  46. pkgdir, basename = os.path.split(path_in_zip)
  47. assert basename.startswith('__init__')
  48. pkgfiles = [f for f in zf.namelist() if f.startswith(pkgdir)]
  49. zf.extractall(tempdir, pkgfiles)
  50. shutil.copytree(pjoin(tempdir, pkgdir), pjoin(target, modname))
  51. else:
  52. # Extract a single file
  53. zf.extract(path_in_zip, tempdir)
  54. shutil.copy2(pjoin(tempdir, path_in_zip), target)
  55. shutil.rmtree(tempdir)
  56. def copytree_ignore_callback(excludes, pkgdir, modname, directory, files):
  57. """This is being called back by our shutil.copytree call to implement the
  58. 'exclude' feature.
  59. """
  60. ignored = set()
  61. # Filter by file names relative to the build directory
  62. reldir = os.path.relpath(directory, pkgdir)
  63. target = os.path.join('pkgs/', modname, reldir)
  64. files = [os.path.join(target, fname) for fname in files]
  65. # Execute all patterns
  66. for pattern in excludes + ['*.pyc']:
  67. ignored.update([
  68. os.path.basename(fname)
  69. for fname in fnmatch.filter(files, pattern)
  70. ])
  71. return ignored
  72. if not PY2:
  73. import importlib
  74. import importlib.abc
  75. import importlib.machinery
  76. class ModuleCopier:
  77. """Finds and copies importable Python modules and packages.
  78. This is the Python >3.3 version and uses the `importlib` package to
  79. locate modules.
  80. """
  81. def __init__(self, py_version, path=None):
  82. self.py_version = py_version
  83. self.path = path if (path is not None) else ([''] + sys.path)
  84. def copy(self, modname, target, exclude):
  85. """Copy the importable module 'modname' to the directory 'target'.
  86. modname should be a top-level import, i.e. without any dots.
  87. Packages are always copied whole.
  88. This can currently copy regular filesystem files and directories,
  89. and extract modules and packages from appropriately structured zip
  90. files.
  91. """
  92. loader = importlib.find_loader(modname, self.path)
  93. if loader is None:
  94. raise ImportError('Could not find %s' % modname)
  95. pkg = loader.is_package(modname)
  96. if isinstance(loader, importlib.machinery.ExtensionFileLoader):
  97. check_ext_mod(loader.path, self.py_version)
  98. shutil.copy2(loader.path, target)
  99. elif isinstance(loader, importlib.abc.FileLoader):
  100. file = loader.get_filename(modname)
  101. if pkg:
  102. pkgdir, basename = os.path.split(file)
  103. assert basename.startswith('__init__')
  104. check_package_for_ext_mods(pkgdir, self.py_version)
  105. dest = os.path.join(target, modname)
  106. if exclude is not None and len(exclude) > 0:
  107. shutil.copytree(
  108. pkgdir, dest,
  109. ignore=partial(copytree_ignore_callback, exclude, pkgdir, modname)
  110. )
  111. else:
  112. # Don't use our exclude callback if we don't need to,
  113. # as it slows things down.
  114. shutil.copytree(
  115. pkgdir, dest,
  116. ignore=shutil.ignore_patterns('*.pyc')
  117. )
  118. else:
  119. shutil.copy2(file, target)
  120. elif isinstance(loader, zipimport.zipimporter):
  121. copy_zipmodule(loader, modname, target)
  122. else:
  123. import imp
  124. class ModuleCopier:
  125. """Finds and copies importable Python modules and packages.
  126. This is the Python 2.7 version and uses the `imp` package to locate
  127. modules.
  128. """
  129. def __init__(self, py_version, path=None):
  130. self.py_version = py_version
  131. self.path = path if (path is not None) else ([''] + sys.path)
  132. self.zip_paths = [p for p in self.path if zipfile.is_zipfile(p)]
  133. def copy(self, modname, target, exclude):
  134. """Copy the importable module 'modname' to the directory 'target'.
  135. modname should be a top-level import, i.e. without any dots.
  136. Packages are always copied whole.
  137. This can currently copy regular filesystem files and directories,
  138. and extract modules and packages from appropriately structured zip
  139. files.
  140. """
  141. try:
  142. info = imp.find_module(modname, self.path)
  143. except ImportError:
  144. # Search all ZIP files in self.path for the module name
  145. # NOTE: `imp.find_module(...)` will *not* find modules in ZIP
  146. # files, so we have to check each file for ourselves
  147. for zpath in self.zip_paths:
  148. loader = zipimport.zipimporter(zpath)
  149. if loader.find_module(modname) is None:
  150. continue
  151. copy_zipmodule(loader, modname, target)
  152. return
  153. # Not found in zip files either - re-raise exception
  154. raise
  155. path = info[1]
  156. modtype = info[2][2]
  157. if modtype == imp.C_EXTENSION:
  158. check_ext_mod(path, self.py_version)
  159. if modtype in (imp.PY_SOURCE, imp.C_EXTENSION):
  160. shutil.copy2(path, target)
  161. elif modtype == imp.PKG_DIRECTORY:
  162. check_package_for_ext_mods(path, self.py_version)
  163. dest = os.path.join(target, modname)
  164. if exclude is not None and len(exclude) > 0:
  165. shutil.copytree(
  166. path, dest,
  167. ignore=partial(copytree_ignore_callback, exclude, path, modname)
  168. )
  169. else:
  170. # Don't use our exclude callback if we don't need to,
  171. # as it slows things down.
  172. shutil.copytree(
  173. path, dest,
  174. ignore=shutil.ignore_patterns('*.pyc')
  175. )
  176. def copy_modules(modnames, target, py_version, path=None, exclude=None):
  177. """Copy the specified importable modules to the target directory.
  178. By default, it finds modules in :data:`sys.path` - this can be overridden
  179. by passing the path parameter.
  180. """
  181. mc = ModuleCopier(py_version, path)
  182. files_in_target_noext = [os.path.splitext(f)[0] for f in os.listdir(target)]
  183. for modname in modnames:
  184. if modname in files_in_target_noext:
  185. # Already there, no need to copy it.
  186. continue
  187. mc.copy(modname, target, exclude)
  188. if not modnames:
  189. # NSIS abhors an empty folder, so give it a file to find.
  190. with open(os.path.join(target, 'placeholder'), 'w') as f:
  191. f.write('This file only exists so NSIS finds something in this directory.')