utils.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2023, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. import fnmatch
  12. import glob
  13. import hashlib
  14. import io
  15. import marshal
  16. import os
  17. import pathlib
  18. import platform
  19. import shutil
  20. import struct
  21. import subprocess
  22. import sys
  23. import types
  24. import zipfile
  25. from PyInstaller import compat
  26. from PyInstaller import log as logging
  27. from PyInstaller.compat import is_aix, is_darwin, is_win, is_linux, is_termux
  28. from PyInstaller.exceptions import InvalidSrcDestTupleError
  29. from PyInstaller.utils import misc
  30. if is_win:
  31. from PyInstaller.utils.win32 import versioninfo
  32. if is_darwin:
  33. import PyInstaller.utils.osx as osxutils
  34. logger = logging.getLogger(__name__)
  35. # -- Helpers for checking guts.
  36. #
  37. # NOTE: by _GUTS it is meant intermediate files and data structures that PyInstaller creates for bundling files and
  38. # creating final executable.
  39. def _check_guts_eq(attr_name, old_value, new_value, last_build):
  40. """
  41. Rebuild is required if values differ.
  42. """
  43. if old_value != new_value:
  44. logger.info("Building because %s changed", attr_name)
  45. return True
  46. return False
  47. def _check_guts_toc_mtime(attr_name, old_toc, new_toc, last_build):
  48. """
  49. Rebuild is required if mtimes of files listed in old TOC are newer than last_build.
  50. Use this for calculated/analysed values read from cache.
  51. """
  52. for dest_name, src_name, typecode in old_toc:
  53. if misc.mtime(src_name) > last_build:
  54. logger.info("Building because %s changed", src_name)
  55. return True
  56. return False
  57. def _check_guts_toc(attr_name, old_toc, new_toc, last_build):
  58. """
  59. Rebuild is required if either TOC content changed or mtimes of files listed in old TOC are newer than last_build.
  60. Use this for input parameters.
  61. """
  62. return _check_guts_eq(attr_name, old_toc, new_toc, last_build) or \
  63. _check_guts_toc_mtime(attr_name, old_toc, new_toc, last_build)
  64. def destination_name_for_extension(module_name, src_name, typecode):
  65. """
  66. Take a TOC entry (dest_name, src_name, typecode) and determine the full destination name for the extension.
  67. """
  68. assert typecode == 'EXTENSION'
  69. # The `module_name` should be the extension's importable module name, such as `psutil._psutil_linux` or
  70. # `numpy._core._multiarray_umath`. Reconstruct the directory structure from parent package name(s), if any.
  71. dest_elements = module_name.split('.')
  72. # We have the base name of the extension file (the last element in the module name), but we do not know the
  73. # full extension suffix. We can take that from source name; for simplicity, replace the whole base name part.
  74. src_path = pathlib.Path(src_name)
  75. dest_elements[-1] = src_path.name
  76. # Extensions that originate from python's python3.x/lib-dynload directory should be diverted into
  77. # python3.x/lib-dynload destination directory instead of being collected into top-level application directory.
  78. # See #5604 for original motivation (using just lib-dynload), and #9204 for extension (using python3.x/lib-dynload).
  79. if src_path.parent.name == 'lib-dynload':
  80. python_dir = f'python{sys.version_info.major}.{sys.version_info.minor}'
  81. if src_path.parent.parent.name == python_dir:
  82. dest_elements = [python_dir, 'lib-dynload', *dest_elements]
  83. return os.path.join(*dest_elements)
  84. def process_collected_binary(
  85. src_name,
  86. dest_name,
  87. use_strip=False,
  88. use_upx=False,
  89. upx_exclude=None,
  90. target_arch=None,
  91. codesign_identity=None,
  92. entitlements_file=None,
  93. strict_arch_validation=False
  94. ):
  95. """
  96. Process the collected binary using strip or UPX (or both), and apply any platform-specific processing. On macOS,
  97. this rewrites the library paths in the headers, and (re-)signs the binary. On-disk cache is used to avoid processing
  98. the same binary with same options over and over.
  99. In addition to given arguments, this function also uses CONF['cachedir'] and CONF['upx_dir'].
  100. """
  101. from PyInstaller.config import CONF
  102. # We need to use cache in the following scenarios:
  103. # * extra binary processing due to use of `strip` or `upx`
  104. # * building on macOS, where we need to rewrite library paths in binaries' headers and (re-)sign the binaries.
  105. if not use_strip and not use_upx and not is_darwin:
  106. return src_name
  107. # Match against provided UPX exclude patterns.
  108. upx_exclude = upx_exclude or []
  109. if use_upx:
  110. src_path = pathlib.PurePath(src_name)
  111. for upx_exclude_entry in upx_exclude:
  112. # pathlib.PurePath.match() matches from right to left, and supports * wildcard, but does not support the
  113. # "**" syntax for directory recursion. Case sensitivity follows the OS default.
  114. if src_path.match(upx_exclude_entry):
  115. logger.info("Disabling UPX for %s due to match in exclude pattern: %s", src_name, upx_exclude_entry)
  116. use_upx = False
  117. break
  118. # Additional automatic disablement rules for UPX and strip.
  119. # On Windows, avoid using UPX with binaries that have control flow guard (CFG) enabled.
  120. if use_upx and is_win and versioninfo.pefile_check_control_flow_guard(src_name):
  121. logger.info('Disabling UPX for %s due to CFG!', src_name)
  122. use_upx = False
  123. # Avoid using UPX with Qt plugins, as it strips the data required by the Qt plugin loader.
  124. if use_upx and misc.is_file_qt_plugin(src_name):
  125. logger.info('Disabling UPX for %s due to it being a Qt plugin!', src_name)
  126. use_upx = False
  127. # On linux, if a binary has an accompanying HMAC or CHK file, avoid modifying it in any way.
  128. if (use_upx or use_strip) and is_linux:
  129. src_path = pathlib.Path(src_name)
  130. hmac_path = src_path.with_name(f".{src_path.name}.hmac")
  131. chk_path = src_path.with_suffix(".chk")
  132. if hmac_path.is_file():
  133. logger.info('Disabling UPX and/or strip for %s due to accompanying .hmac file!', src_name)
  134. use_upx = use_strip = False
  135. elif chk_path.is_file():
  136. logger.info('Disabling UPX and/or strip for %s due to accompanying .chk file!', src_name)
  137. use_upx = use_strip = False
  138. del src_path, hmac_path, chk_path
  139. # Exit early if no processing is required after above rules are applied.
  140. if not use_strip and not use_upx and not is_darwin:
  141. return src_name
  142. # Prepare cache directory path. Cache is tied to python major/minor version, but also to various processing options.
  143. pyver = f'py{sys.version_info[0]}{sys.version_info[1]}'
  144. arch = platform.architecture()[0]
  145. cache_dir = os.path.join(
  146. CONF['cachedir'],
  147. f'bincache{use_strip:d}{use_upx:d}{pyver}{arch}',
  148. )
  149. if target_arch:
  150. cache_dir = os.path.join(cache_dir, target_arch)
  151. if is_darwin:
  152. # Separate by codesign identity
  153. if codesign_identity:
  154. # Compute hex digest of codesign identity string to prevent issues with invalid characters.
  155. csi_hash = hashlib.sha256(codesign_identity.encode('utf-8'))
  156. cache_dir = os.path.join(cache_dir, csi_hash.hexdigest())
  157. else:
  158. cache_dir = os.path.join(cache_dir, 'adhoc') # ad-hoc signing
  159. # Separate by entitlements
  160. if entitlements_file:
  161. # Compute hex digest of entitlements file contents
  162. with open(entitlements_file, 'rb') as fp:
  163. ef_hash = hashlib.sha256(fp.read())
  164. cache_dir = os.path.join(cache_dir, ef_hash.hexdigest())
  165. else:
  166. cache_dir = os.path.join(cache_dir, 'no-entitlements')
  167. os.makedirs(cache_dir, exist_ok=True)
  168. # Load cache index, if available
  169. cache_index_file = os.path.join(cache_dir, "index.dat")
  170. try:
  171. cache_index = misc.load_py_data_struct(cache_index_file)
  172. except FileNotFoundError:
  173. cache_index = {}
  174. except Exception:
  175. # Tell the user they may want to fix their cache... However, do not delete it for them; if it keeps getting
  176. # corrupted, we will never find out.
  177. logger.warning("PyInstaller bincache may be corrupted; use pyinstaller --clean to fix it.")
  178. raise
  179. # Look up the file in cache; use case-normalized destination name as identifier.
  180. cached_id = os.path.normcase(dest_name)
  181. cached_name = os.path.join(cache_dir, dest_name)
  182. src_digest = _compute_file_digest(src_name)
  183. if cached_id in cache_index:
  184. # If digest matches to the cached digest, return the cached file...
  185. if src_digest == cache_index[cached_id]:
  186. return cached_name
  187. # ... otherwise remove it.
  188. os.remove(cached_name)
  189. # Ensure parent path exists
  190. os.makedirs(os.path.dirname(cached_name), exist_ok=True)
  191. # Use `shutil.copyfile` to copy the file with default permissions bits, then manually set executable
  192. # bits. This way, we avoid copying permission bits and metadata from the original file, which might be too
  193. # restrictive for further processing (read-only permissions, immutable flag on FreeBSD, and so on).
  194. shutil.copyfile(src_name, cached_name)
  195. os.chmod(cached_name, 0o755)
  196. # Apply strip
  197. if use_strip:
  198. strip_options = []
  199. if is_darwin:
  200. # The default strip behavior breaks some shared libraries under macOS.
  201. strip_options = ["-S"] # -S = strip only debug symbols.
  202. elif is_aix:
  203. # Set -X32_64 flag to have strip transparently process both 32-bit and 64-bit binaries, without user having
  204. # to set OBJECT_MODE environment variable prior to the build. Also accommodates potential mixed-case
  205. # scenario, for example a 32-bit utility program being collected into a 64-bit application bundle.
  206. strip_options = ["-X32_64"]
  207. cmd = ["strip", *strip_options, cached_name]
  208. logger.info("Executing: %s", " ".join(cmd))
  209. try:
  210. p = subprocess.run(
  211. cmd,
  212. stdin=subprocess.DEVNULL,
  213. stdout=subprocess.PIPE,
  214. stderr=subprocess.STDOUT,
  215. check=True,
  216. errors='ignore',
  217. encoding='utf-8',
  218. )
  219. logger.debug("Output from strip command:\n%s", p.stdout)
  220. except subprocess.CalledProcessError as e:
  221. show_warning = True
  222. # On AIX, strip utility raises an error when ran against already-stripped binary. Catch the corresponding
  223. # message (`0654-419 The specified archive file was already stripped.`) and suppress the warning.
  224. if is_aix and "0654-419" in e.stdout:
  225. show_warning = False
  226. if show_warning:
  227. logger.warning("Failed to run strip on %r!", cached_name, exc_info=True)
  228. logger.warning("Output from strip command:\n%s", e.stdout)
  229. except Exception:
  230. logger.warning("Failed to run strip on %r!", cached_name, exc_info=True)
  231. # Apply UPX
  232. if use_upx:
  233. upx_exe = 'upx'
  234. upx_dir = CONF['upx_dir']
  235. if upx_dir:
  236. upx_exe = os.path.join(upx_dir, upx_exe)
  237. upx_options = [
  238. # Do not compress icons, so that they can still be accessed externally.
  239. '--compress-icons=0',
  240. # Use LZMA compression.
  241. '--lzma',
  242. # Quiet mode.
  243. '-q',
  244. ]
  245. if is_win:
  246. # Binaries built with Visual Studio 7.1 require --strip-loadconf or they will not compress.
  247. upx_options.append('--strip-loadconf')
  248. cmd = [upx_exe, *upx_options, cached_name]
  249. logger.info("Executing: %s", " ".join(cmd))
  250. try:
  251. p = subprocess.run(
  252. cmd,
  253. stdin=subprocess.DEVNULL,
  254. stdout=subprocess.PIPE,
  255. stderr=subprocess.STDOUT,
  256. check=True,
  257. errors='ignore',
  258. encoding='utf-8',
  259. )
  260. logger.debug("Output from upx command:\n%s", p.stdout)
  261. except subprocess.CalledProcessError as e:
  262. logger.warning("Failed to upx strip on %r!", cached_name, exc_info=True)
  263. logger.warning("Output from upx command:\n%s", e.stdout)
  264. except Exception:
  265. logger.warning("Failed to run upx on %r!", cached_name, exc_info=True)
  266. # On macOS, we need to modify the given binary's paths to the dependent libraries, in order to ensure they are
  267. # relocatable and always refer to location within the frozen application. Specifically, we make all dependent
  268. # library paths relative to @rpath, and set @rpath to point to the top-level application directory, relative to
  269. # the binary's location (i.e., @loader_path).
  270. #
  271. # While modifying the headers invalidates existing signatures, we avoid removing them in order to speed things up
  272. # (and to avoid potential bugs in the codesign utility, like the one reported on macOS 10.13 in #6167).
  273. # The forced re-signing at the end should take care of the invalidated signatures.
  274. if is_darwin:
  275. try:
  276. osxutils.binary_to_target_arch(cached_name, target_arch, display_name=src_name)
  277. #osxutils.remove_signature_from_binary(cached_name) # Disabled as per comment above.
  278. target_rpath = str(
  279. pathlib.PurePath('@loader_path', *['..' for level in pathlib.PurePath(dest_name).parent.parts])
  280. )
  281. osxutils.set_dylib_dependency_paths(cached_name, target_rpath)
  282. osxutils.sign_binary(cached_name, codesign_identity, entitlements_file)
  283. except osxutils.InvalidBinaryError:
  284. # Raised by osxutils.binary_to_target_arch when the given file is not a valid macOS binary (for example,
  285. # a linux .so file; see issue #6327). The error prevents any further processing, so just ignore it.
  286. pass
  287. except osxutils.IncompatibleBinaryArchError:
  288. # Raised by osxutils.binary_to_target_arch when the given file does not contain (all) required arch slices.
  289. # Depending on the strict validation mode, re-raise or swallow the error.
  290. #
  291. # Strict validation should be enabled only for binaries where the architecture *must* match the target one,
  292. # i.e., the extension modules. Everything else is pretty much a gray area, for example:
  293. # * a universal2 extension may have its x86_64 and arm64 slices linked against distinct single-arch/thin
  294. # shared libraries
  295. # * a collected executable that is launched by python code via a subprocess can be x86_64-only, even though
  296. # the actual python code is running on M1 in native arm64 mode.
  297. if strict_arch_validation:
  298. raise
  299. logger.debug("File %s failed optional architecture validation - collecting as-is!", src_name)
  300. except Exception as e:
  301. raise SystemError(f"Failed to process binary {cached_name!r}!") from e
  302. # Update cache index
  303. cache_index[cached_id] = src_digest
  304. misc.save_py_data_struct(cache_index_file, cache_index)
  305. return cached_name
  306. def _compute_file_digest(filename):
  307. hasher = hashlib.sha1()
  308. with open(filename, "rb") as fp:
  309. for chunk in iter(lambda: fp.read(16 * 1024), b""):
  310. hasher.update(chunk)
  311. return bytearray(hasher.digest())
  312. def _check_path_overlap(path):
  313. """
  314. Check that path does not overlap with WORKPATH or SPECPATH (i.e., WORKPATH and SPECPATH may not start with path,
  315. which could be caused by a faulty hand-edited specfile).
  316. Raise SystemExit if there is overlap, return True otherwise
  317. """
  318. from PyInstaller.config import CONF
  319. specerr = 0
  320. if CONF['workpath'].startswith(path):
  321. logger.error('Specfile error: The output path "%s" contains WORKPATH (%s)', path, CONF['workpath'])
  322. specerr += 1
  323. if CONF['specpath'].startswith(path):
  324. logger.error('Specfile error: The output path "%s" contains SPECPATH (%s)', path, CONF['specpath'])
  325. specerr += 1
  326. if specerr:
  327. raise SystemExit(
  328. 'ERROR: Please edit/recreate the specfile (%s) and set a different output name (e.g. "dist").' %
  329. CONF['spec']
  330. )
  331. return True
  332. def _make_clean_directory(path):
  333. """
  334. Create a clean directory from the given directory name.
  335. """
  336. if _check_path_overlap(path):
  337. if os.path.isdir(path) or os.path.isfile(path):
  338. try:
  339. os.remove(path)
  340. except OSError:
  341. _rmtree(path)
  342. os.makedirs(path, exist_ok=True)
  343. def _rmtree(path):
  344. """
  345. Remove directory and all its contents, but only after user confirmation, or if the -y option is set.
  346. """
  347. from PyInstaller.config import CONF
  348. if CONF['noconfirm']:
  349. choice = 'y'
  350. elif sys.stdout.isatty():
  351. choice = input(
  352. 'WARNING: The output directory "%s" and ALL ITS CONTENTS will be REMOVED! Continue? (y/N)' % path
  353. )
  354. else:
  355. raise SystemExit(
  356. 'ERROR: The output directory "%s" is not empty. Please remove all its contents or use the -y option (remove'
  357. ' output directory without confirmation).' % path
  358. )
  359. if choice.strip().lower() == 'y':
  360. if not CONF['noconfirm']:
  361. print("On your own risk, you can use the option `--noconfirm` to get rid of this question.")
  362. logger.info('Removing dir %s', path)
  363. shutil.rmtree(path)
  364. else:
  365. raise SystemExit('User aborted')
  366. # TODO Refactor to prohibit empty target directories. As the docstring below documents, this function currently permits
  367. # the second item of each 2-tuple in "hook.datas" to be the empty string, in which case the target directory defaults to
  368. # the source directory's basename. However, this functionality is very fragile and hence bad. Instead:
  369. #
  370. # * An exception should be raised if such item is empty.
  371. # * All hooks currently passing the empty string for such item (e.g.,
  372. # "hooks/hook-babel.py", "hooks/hook-matplotlib.py") should be refactored
  373. # to instead pass such basename.
  374. def format_binaries_and_datas(binaries_or_datas, workingdir=None):
  375. """
  376. Convert the passed list of hook-style 2-tuples into a returned set of `TOC`-style 2-tuples.
  377. Elements of the passed list are 2-tuples `(source_dir_or_glob, target_dir)`.
  378. Elements of the returned set are 2-tuples `(target_file, source_file)`.
  379. For backwards compatibility, the order of elements in the former tuples are the reverse of the order of elements in
  380. the latter tuples!
  381. Parameters
  382. ----------
  383. binaries_or_datas : list
  384. List of hook-style 2-tuples (e.g., the top-level `binaries` and `datas` attributes defined by hooks) whose:
  385. * The first element is either:
  386. * A glob matching only the absolute or relative paths of source non-Python data files.
  387. * The absolute or relative path of a source directory containing only source non-Python data files.
  388. * The second element is the relative path of the target directory into which these source files will be
  389. recursively copied.
  390. If the optional `workingdir` parameter is passed, source paths may be either absolute or relative; else, source
  391. paths _must_ be absolute.
  392. workingdir : str
  393. Optional absolute path of the directory to which all relative source paths in the `binaries_or_datas`
  394. parameter will be prepended by (and hence converted into absolute paths) _or_ `None` if these paths are to be
  395. preserved as relative. Defaults to `None`.
  396. Returns
  397. ----------
  398. set
  399. Set of `TOC`-style 2-tuples whose:
  400. * First element is the absolute or relative path of a target file.
  401. * Second element is the absolute or relative path of the corresponding source file to be copied to this target
  402. file.
  403. """
  404. toc_datas = set()
  405. for src_root_path_or_glob, trg_root_dir in binaries_or_datas:
  406. # Disallow empty source path. Those are typically result of errors, and result in implicit collection of the
  407. # whole current working directory, which is never a good idea.
  408. if not src_root_path_or_glob:
  409. raise InvalidSrcDestTupleError(
  410. (src_root_path_or_glob, trg_root_dir),
  411. "Empty SRC is not allowed when adding binary and data files, as it would result in collection of the "
  412. "whole current working directory."
  413. )
  414. if not trg_root_dir:
  415. raise InvalidSrcDestTupleError(
  416. (src_root_path_or_glob, trg_root_dir),
  417. "Empty DEST_DIR is not allowed - to collect files into application's top-level directory, use "
  418. f"{os.curdir!r}."
  419. )
  420. # Disallow absolute target paths, as well as target paths that would end up pointing outside of the
  421. # application's top-level directory.
  422. if os.path.isabs(trg_root_dir):
  423. raise InvalidSrcDestTupleError((src_root_path_or_glob, trg_root_dir), "DEST_DIR must be a relative path!")
  424. if os.path.normpath(trg_root_dir).startswith('..'):
  425. raise InvalidSrcDestTupleError(
  426. (src_root_path_or_glob, trg_root_dir),
  427. "DEST_DIR must not point outside of application's top-level directory!",
  428. )
  429. # Convert relative to absolute paths if required.
  430. if workingdir and not os.path.isabs(src_root_path_or_glob):
  431. src_root_path_or_glob = os.path.join(workingdir, src_root_path_or_glob)
  432. # Normalize paths.
  433. src_root_path_or_glob = os.path.normpath(src_root_path_or_glob)
  434. # If given source path is a file or directory path, pass it on.
  435. # If not, treat it as a glob and pass on all matching paths. However, we need to preserve the directories
  436. # captured by the glob - as opposed to collecting their contents into top-level target directory. Therefore,
  437. # we set a flag which is used in subsequent processing to distinguish between original directory paths and
  438. # directory paths that were captured by the glob.
  439. if os.path.isfile(src_root_path_or_glob) or os.path.isdir(src_root_path_or_glob):
  440. src_root_paths = [src_root_path_or_glob]
  441. was_glob = False
  442. else:
  443. src_root_paths = glob.glob(src_root_path_or_glob)
  444. was_glob = True
  445. if not src_root_paths:
  446. raise SystemExit(f'ERROR: Unable to find {src_root_path_or_glob!r} when adding binary and data files.')
  447. for src_root_path in src_root_paths:
  448. if os.path.isfile(src_root_path):
  449. # Normalizing the result to remove redundant relative paths (e.g., removing "./" from "trg/./file").
  450. toc_datas.add((
  451. os.path.normpath(os.path.join(trg_root_dir, os.path.basename(src_root_path))),
  452. os.path.normpath(src_root_path),
  453. ))
  454. elif os.path.isdir(src_root_path):
  455. for src_dir, src_subdir_basenames, src_file_basenames in os.walk(src_root_path):
  456. # Ensure the current source directory is a subdirectory of the passed top-level source directory.
  457. # Since os.walk() does *NOT* follow symlinks by default, this should be the case. (But let's make
  458. # sure.)
  459. assert src_dir.startswith(src_root_path)
  460. # Relative path of the current target directory, obtained by:
  461. #
  462. # * Stripping the top-level source directory from the current source directory (e.g., removing
  463. # "/top" from "/top/dir").
  464. # * Normalizing the result to remove redundant relative paths (e.g., removing "./" from
  465. # "trg/./file").
  466. if was_glob:
  467. # Preserve directories captured by glob.
  468. rel_dir = os.path.relpath(src_dir, os.path.dirname(src_root_path))
  469. else:
  470. rel_dir = os.path.relpath(src_dir, src_root_path)
  471. trg_dir = os.path.normpath(os.path.join(trg_root_dir, rel_dir))
  472. for src_file_basename in src_file_basenames:
  473. src_file = os.path.join(src_dir, src_file_basename)
  474. if os.path.isfile(src_file):
  475. # Normalize the result to remove redundant relative paths (e.g., removing "./" from
  476. # "trg/./file").
  477. toc_datas.add((
  478. os.path.normpath(os.path.join(trg_dir, src_file_basename)), os.path.normpath(src_file)
  479. ))
  480. return toc_datas
  481. def get_code_object(modname, filename, optimize):
  482. """
  483. Get the code-object for a module.
  484. This is a simplifed non-performant version which circumvents __pycache__.
  485. """
  486. # Once upon a time, we compiled dummy code objects for PEP-420 namespace packages. We do not do that anymore.
  487. assert filename not in {'-', None}, "Called with PEP-420 namespace package!"
  488. _, ext = os.path.splitext(filename)
  489. ext = ext.lower()
  490. if ext == '.pyc':
  491. # The module is available in binary-only form. Read the contents of .pyc file using helper function, which
  492. # supports reading from either stand-alone or archive-embedded .pyc files.
  493. logger.debug('Reading code object from .pyc file %s', filename)
  494. pyc_data = _read_pyc_data(filename)
  495. code_object = marshal.loads(pyc_data[16:])
  496. else:
  497. # Assume this is a source .py file, but allow an arbitrary extension (other than .pyc, which is taken in
  498. # the above branch). This allows entry-point scripts to have an arbitrary (or no) extension, as tested by
  499. # the `test_arbitrary_ext` in `test_basic.py`.
  500. logger.debug('Compiling python script/module file %s', filename)
  501. with open(filename, 'rb') as f:
  502. source = f.read()
  503. # If entry-point script has no suffix, append .py when compiling the source. In POSIX builds, the executable
  504. # has no suffix either; this causes issues with `traceback` module, as it tries to read the executable file
  505. # when trying to look up the code for the entry-point script (when current working directory contains the
  506. # executable).
  507. _, ext = os.path.splitext(filename)
  508. if not ext:
  509. logger.debug("Appending .py to compiled entry-point name...")
  510. filename += '.py'
  511. try:
  512. code_object = compile(source, filename, 'exec', optimize=optimize)
  513. except SyntaxError:
  514. logger.warning("Sytnax error while compiling %s", filename)
  515. raise
  516. return code_object
  517. def replace_filename_in_code_object(code_object, filename):
  518. """
  519. Recursively replace the `co_filename` in the given code object and code objects stored in its `co_consts` entries.
  520. Primarily used to anonymize collected code objects, i.e., by removing the build environment's paths from them.
  521. """
  522. consts = tuple(
  523. replace_filename_in_code_object(const_co, filename) if isinstance(const_co, types.CodeType) else const_co
  524. for const_co in code_object.co_consts
  525. )
  526. return code_object.replace(co_consts=consts, co_filename=filename)
  527. def _should_include_system_binary(binary_tuple, exceptions):
  528. """
  529. Return True if the given binary_tuple describes a system binary that should be included.
  530. Exclude all system library binaries other than those with "lib-dynload" in the destination or "python" in the
  531. source, except for those matching the patterns in the exceptions list. Intended to be used from the Analysis
  532. exclude_system_libraries method.
  533. """
  534. dest = binary_tuple[0]
  535. if dest.startswith(f'python{sys.version_info.major}.{sys.version_info.minor}/lib-dynload'):
  536. return True
  537. src = binary_tuple[1]
  538. if fnmatch.fnmatch(src, '*python*'):
  539. return True
  540. if is_termux:
  541. # Termux linux; system libraries are in /system/lib{,64} and /data/data/com.termux/files/usr/lib; in addition
  542. # /usr is symbolic link pointing at /data/data/com.termux/files/usr
  543. SYS_PREFIX = ('/system/lib', '/data/data/com.termux/files/usr/lib', '/usr/lib')
  544. else:
  545. # Standard linux distributions; system libraries are in /lib{,64} and /usr/lib{,64}
  546. SYS_PREFIX = ('/lib', '/usr/lib')
  547. if not src.startswith(SYS_PREFIX):
  548. return True
  549. for exception in exceptions:
  550. if fnmatch.fnmatch(dest, exception):
  551. return True
  552. return False
  553. def compile_pymodule(name, src_path, workpath, optimize, code_cache=None):
  554. """
  555. Given the name and source file for a pure-python module, compile the module in the specified working directory,
  556. and return the name of resulting .pyc file. The paths in the resulting .pyc module are anonymized by having their
  557. absolute prefix removed.
  558. If a .pyc file with matching name already exists in the target working directory, it is re-used (provided it has
  559. compatible bytecode magic in the header, and that its modification time is newer than that of the source file).
  560. If the specified module is available in binary-only form, the input .pyc file is copied to the target working
  561. directory and post-processed. If the specified module is available in source form, it is compiled only if
  562. corresponding code object is not available in the optional code-object cache; otherwise, it is copied from cache
  563. and post-processed. When compiling the module, the specified byte-code optimization level is used.
  564. It is up to caller to ensure that the optional code-object cache contains only code-objects of target optimization
  565. level, and that if the specified working directory already contains .pyc files, that they were created with target
  566. optimization level.
  567. """
  568. # Construct the target .pyc filename in the workpath
  569. split_name = name.split(".")
  570. if "__init__" in src_path:
  571. # __init__ module; use "__init__" as module name, and construct parent path using all components of the
  572. # fully-qualified name
  573. parent_dirs = split_name
  574. mod_basename = "__init__"
  575. else:
  576. # Regular module; use last component of the fully-qualified name as module name, and the rest as the parent
  577. # path.
  578. parent_dirs = split_name[:-1]
  579. mod_basename = split_name[-1]
  580. pyc_path = os.path.join(workpath, *parent_dirs, mod_basename + '.pyc')
  581. # Check if optional cache contains module entry
  582. code_object = code_cache.get(name, None) if code_cache else None
  583. if code_object is None:
  584. _, ext = os.path.splitext(src_path)
  585. ext = ext.lower()
  586. if ext == '.py':
  587. # Source py file; read source and compile it.
  588. with open(src_path, 'rb') as f:
  589. src_data = f.read()
  590. code_object = compile(src_data, src_path, 'exec', optimize=optimize)
  591. elif ext == '.pyc':
  592. # The module is available in binary-only form. Read the contents of .pyc file using helper function, which
  593. # supports reading from either stand-alone or archive-embedded .pyc files.
  594. pyc_data = _read_pyc_data(src_path)
  595. # Unmarshal code object; this is necessary if we want to strip paths from it
  596. code_object = marshal.loads(pyc_data[16:])
  597. else:
  598. raise ValueError(f"Invalid python module file {src_path}; unhandled extension {ext}!")
  599. # Replace co_filename in code object with anonymized filename that does not contain full path. Construct the
  600. # relative filename from module name, similar how we earlier constructed the `pyc_path`.
  601. co_filename = os.path.join(*parent_dirs, mod_basename + '.py')
  602. code_object = replace_filename_in_code_object(code_object, co_filename)
  603. # Write complete .pyc module to in-memory stream. Then, check if .pyc file already exists, compare contents, and
  604. # (re)write it only if different. This avoids unnecessary (re)writing of the file, and in turn also avoids
  605. # unnecessary cache invalidation for targets that make use of the .pyc file (e.g., PKG, COLLECT).
  606. with io.BytesIO() as pyc_stream:
  607. pyc_stream.write(compat.BYTECODE_MAGIC)
  608. pyc_stream.write(struct.pack('<I', 0b01)) # PEP-552: hash-based pyc, check_source=False
  609. pyc_stream.write(b'\00' * 8) # Zero the source hash
  610. marshal.dump(code_object, pyc_stream)
  611. pyc_data = pyc_stream.getvalue()
  612. if os.path.isfile(pyc_path):
  613. with open(pyc_path, 'rb') as fh:
  614. existing_pyc_data = fh.read()
  615. if pyc_data == existing_pyc_data:
  616. return pyc_path # Return path to (existing) file.
  617. # Ensure the existence of parent directories for the target pyc path
  618. os.makedirs(os.path.dirname(pyc_path), exist_ok=True)
  619. # Write
  620. with open(pyc_path, 'wb') as fh:
  621. fh.write(pyc_data)
  622. # Return output path
  623. return pyc_path
  624. def _read_pyc_data(filename):
  625. """
  626. Helper for reading data from .pyc files. Supports both stand-alone and archive-embedded .pyc files. Used by
  627. `compile_pymodule` and `get_code_object` helper functions.
  628. """
  629. src_file = pathlib.Path(filename)
  630. if src_file.is_file():
  631. # Stand-alone .pyc file.
  632. pyc_data = src_file.read_bytes()
  633. else:
  634. # Check if .pyc file is stored in a .zip archive, as is the case for stdlib modules in embeddable
  635. # python on Windows.
  636. parent_zip_file = misc.path_to_parent_archive(src_file)
  637. if parent_zip_file is not None and zipfile.is_zipfile(parent_zip_file):
  638. with zipfile.ZipFile(parent_zip_file, 'r') as zip_archive:
  639. # NOTE: zip entry names must be in POSIX format, even on Windows!
  640. zip_entry_name = str(src_file.relative_to(parent_zip_file).as_posix())
  641. pyc_data = zip_archive.read(zip_entry_name)
  642. else:
  643. raise FileNotFoundError(f"Cannot find .pyc file {filename!r}!")
  644. # Verify the python version
  645. if pyc_data[:4] != compat.BYTECODE_MAGIC:
  646. raise ValueError(f"The .pyc module {filename} was compiled for incompatible version of python!")
  647. return pyc_data
  648. def postprocess_binaries_toc_pywin32(binaries):
  649. """
  650. Process the given `binaries` TOC list to apply work around for `pywin32` package, fixing the target directory
  651. for collected extensions.
  652. """
  653. # Ensure that all files collected from `win32` or `pythonwin` into top-level directory are put back into
  654. # their corresponding directories. They end up in top-level directory because `pywin32.pth` adds both
  655. # directories to the `sys.path`, so they end up visible as top-level directories. But these extensions
  656. # might in fact be linked against each other, so we should preserve the directory layout for consistency
  657. # between modulegraph-discovered extensions and linked binaries discovered by link-time dependency analysis.
  658. # Within the same framework, also consider `pywin32_system32`, just in case.
  659. PYWIN32_SUBDIRS = {'win32', 'pythonwin', 'pywin32_system32'}
  660. processed_binaries = []
  661. for dest_name, src_name, typecode in binaries:
  662. dest_path = pathlib.PurePath(dest_name)
  663. src_path = pathlib.PurePath(src_name)
  664. if dest_path.parent == pathlib.PurePath('.') and src_path.parent.name.lower() in PYWIN32_SUBDIRS:
  665. dest_path = pathlib.PurePath(src_path.parent.name) / dest_path
  666. dest_name = str(dest_path)
  667. processed_binaries.append((dest_name, src_name, typecode))
  668. return processed_binaries
  669. def postprocess_binaries_toc_pywin32_anaconda(binaries):
  670. """
  671. Process the given `binaries` TOC list to apply work around for Anaconda `pywin32` package, fixing the location
  672. of collected `pywintypes3X.dll` and `pythoncom3X.dll`.
  673. """
  674. # The Anaconda-provided `pywin32` package installs three copies of `pywintypes3X.dll` and `pythoncom3X.dll`,
  675. # located in the following directories (relative to the environment):
  676. # - Library/bin
  677. # - Lib/site-packages/pywin32_system32
  678. # - Lib/site-packages/win32
  679. #
  680. # This turns our dependency scanner and directory layout preservation mechanism into a lottery based on what
  681. # `pywin32` modules are imported and in what order. To keep things simple, we deal with this insanity by
  682. # post-processing the `binaries` list, modifying the destination of offending copies, and let the final TOC
  683. # list normalization deal with potential duplicates.
  684. DLL_CANDIDATES = {
  685. f"pywintypes{sys.version_info[0]}{sys.version_info[1]}.dll",
  686. f"pythoncom{sys.version_info[0]}{sys.version_info[1]}.dll",
  687. }
  688. DUPLICATE_DIRS = {
  689. pathlib.PurePath('.'),
  690. pathlib.PurePath('win32'),
  691. }
  692. processed_binaries = []
  693. for dest_name, src_name, typecode in binaries:
  694. # Check if we need to divert - based on the destination base name and destination parent directory.
  695. dest_path = pathlib.PurePath(dest_name)
  696. if dest_path.name.lower() in DLL_CANDIDATES and dest_path.parent in DUPLICATE_DIRS:
  697. dest_path = pathlib.PurePath("pywin32_system32") / dest_path.name
  698. dest_name = str(dest_path)
  699. processed_binaries.append((dest_name, src_name, typecode))
  700. return processed_binaries
  701. def create_base_library_zip(filename, modules_toc, code_cache=None):
  702. """
  703. Create a zip archive with python modules that are needed during python interpreter initialization.
  704. """
  705. with zipfile.ZipFile(filename, 'w') as zf:
  706. for name, src_path, typecode in modules_toc:
  707. # Obtain code object from cache, or compile it.
  708. code = None if code_cache is None else code_cache.get(name, None)
  709. if code is None:
  710. optim_level = {'PYMODULE': 0, 'PYMODULE-1': 1, 'PYMODULE-2': 2}[typecode]
  711. code = get_code_object(name, src_path, optimize=optim_level)
  712. # Determine destination name
  713. dest_name = name.replace('.', os.sep)
  714. # Special case: packages have an implied `__init__` filename that needs to be added.
  715. basename, ext = os.path.splitext(os.path.basename(src_path))
  716. if basename == '__init__':
  717. dest_name += os.sep + '__init__'
  718. dest_name += '.pyc' # Always .pyc, regardless of optimization level.
  719. # Replace full-path co_filename in code object with `dest_name` (and shorten suffix from .pyc to .py).
  720. code = replace_filename_in_code_object(code, dest_name[:-1])
  721. # Write the .pyc module
  722. with io.BytesIO() as fc:
  723. fc.write(compat.BYTECODE_MAGIC)
  724. fc.write(struct.pack('<I', 0b01)) # PEP-552: hash-based pyc, check_source=False
  725. fc.write(b'\00' * 8) # Match behavior of `building.utils.compile_pymodule`
  726. marshal.dump(code, fc)
  727. # Use a ZipInfo to set timestamp for deterministic build.
  728. info = zipfile.ZipInfo(dest_name)
  729. zf.writestr(info, fc.getvalue())