1#----------------------------------------------------------------------------- 2# Copyright (c) 2005-2019, PyInstaller Development Team. 3# 4# Distributed under the terms of the GNU General Public License with exception 5# for distributing bootloader. 6# 7# The full license is in the file COPYING.txt, distributed with this software. 8#----------------------------------------------------------------------------- 9 10 11 12#--- functions for checking guts --- 13# NOTE: By GUTS it is meant intermediate files and data structures that 14# PyInstaller creates for bundling files and creating final executable. 15import glob 16import hashlib 17import os 18import os.path 19import pkgutil 20import platform 21import shutil 22import sys 23 24import struct 25 26from PyInstaller.config import CONF 27from .. import compat 28from ..compat import is_darwin, is_win, EXTENSION_SUFFIXES, \ 29 open_file, is_py3, is_py37, is_cygwin 30from ..depend import dylib 31from ..depend.bindepend import match_binding_redirect 32from ..utils import misc 33from ..utils.misc import load_py_data_struct, save_py_data_struct 34from .. import log as logging 35 36if is_win: 37 from ..utils.win32 import winmanifest, winresource 38 39logger = logging.getLogger(__name__) 40 41 42#-- Helpers for checking guts. 43# 44# NOTE: By _GUTS it is meant intermediate files and data structures that 45# PyInstaller creates for bundling files and creating final executable. 46 47def _check_guts_eq(attr, old, new, last_build): 48 """ 49 rebuild is required if values differ 50 """ 51 if old != new: 52 logger.info("Building because %s changed", attr) 53 return True 54 return False 55 56 57def _check_guts_toc_mtime(attr, old, toc, last_build, pyc=0): 58 """ 59 rebuild is required if mtimes of files listed in old toc are newer 60 than last_build 61 62 if pyc=1, check for .py files, too 63 64 Use this for calculated/analysed values read from cache. 65 """ 66 for (nm, fnm, typ) in old: 67 if misc.mtime(fnm) > last_build: 68 logger.info("Building because %s changed", fnm) 69 return True 70 elif pyc and misc.mtime(fnm[:-1]) > last_build: 71 logger.info("Building because %s changed", fnm[:-1]) 72 return True 73 return False 74 75 76def _check_guts_toc(attr, old, toc, last_build, pyc=0): 77 """ 78 rebuild is required if either toc content changed or mtimes of 79 files listed in old toc are newer than last_build 80 81 if pyc=1, check for .py files, too 82 83 Use this for input parameters. 84 """ 85 return (_check_guts_eq(attr, old, toc, last_build) 86 or _check_guts_toc_mtime(attr, old, toc, last_build, pyc=pyc)) 87 88 89#--- 90 91def add_suffix_to_extensions(toc): 92 """ 93 Returns a new TOC with proper library suffix for EXTENSION items. 94 """ 95 # TODO: Fix this recursive import 96 from .datastruct import TOC 97 new_toc = TOC() 98 for inm, fnm, typ in toc: 99 if typ == 'EXTENSION': 100 if is_py3: 101 # Change the dotted name into a relative path. This places C 102 # extensions in the Python-standard location. This only works 103 # in Python 3; see comments above 104 # ``sys.meta_path.append(CExtensionImporter())`` in 105 # ``pyimod03_importers``. 106 inm = inm.replace('.', os.sep) 107 # In some rare cases extension might already contain a suffix. 108 # Skip it in this case. 109 if os.path.splitext(inm)[1] not in EXTENSION_SUFFIXES: 110 # Determine the base name of the file. 111 if is_py3: 112 base_name = os.path.basename(inm) 113 else: 114 base_name = inm.rsplit('.')[-1] 115 assert '.' not in base_name 116 # Use this file's existing extension. For extensions such as 117 # ``libzmq.cp36-win_amd64.pyd``, we can't use 118 # ``os.path.splitext``, which would give only the ```.pyd`` part 119 # of the extension. 120 inm = inm + os.path.basename(fnm)[len(base_name):] 121 122 elif typ == 'DEPENDENCY': 123 # Use the suffix from the filename. 124 # TODO Verify what extensions are by DEPENDENCIES. 125 binext = os.path.splitext(fnm)[1] 126 if not os.path.splitext(inm)[1] == binext: 127 inm = inm + binext 128 new_toc.append((inm, fnm, typ)) 129 return new_toc 130 131def applyRedirects(manifest, redirects): 132 """ 133 Apply the binding redirects specified by 'redirects' to the dependent assemblies 134 of 'manifest'. 135 136 :param manifest: 137 :type manifest: 138 :param redirects: 139 :type redirects: 140 :return: 141 :rtype: 142 """ 143 redirecting = False 144 for binding in redirects: 145 for dep in manifest.dependentAssemblies: 146 if match_binding_redirect(dep, binding): 147 logger.info("Redirecting %s version %s -> %s", 148 binding.name, dep.version, binding.newVersion) 149 dep.version = binding.newVersion 150 redirecting = True 151 return redirecting 152 153 154def checkCache(fnm, strip=False, upx=False, upx_exclude=None, dist_nm=None): 155 """ 156 Cache prevents preprocessing binary files again and again. 157 158 'dist_nm' Filename relative to dist directory. We need it on Mac 159 to determine level of paths for @loader_path like 160 '@loader_path/../../' for qt4 plugins. 161 """ 162 from ..config import CONF 163 # On darwin a cache is required anyway to keep the libaries 164 # with relative install names. Caching on darwin does not work 165 # since we need to modify binary headers to use relative paths 166 # to dll depencies and starting with '@loader_path'. 167 if not strip and not upx and not is_darwin and not is_win: 168 return fnm 169 170 if dist_nm is not None and ":" in dist_nm: 171 # A file embedded in another pyinstaller build via multipackage 172 # No actual file exists to process 173 return fnm 174 175 if strip: 176 strip = True 177 else: 178 strip = False 179 upx_exclude = upx_exclude or [] 180 upx = (upx and (is_win or is_cygwin) and 181 os.path.normcase(os.path.basename(fnm)) not in upx_exclude) 182 183 # Load cache index 184 # Make cachedir per Python major/minor version. 185 # This allows parallel building of executables with different 186 # Python versions as one user. 187 pyver = ('py%d%s') % (sys.version_info[0], sys.version_info[1]) 188 arch = platform.architecture()[0] 189 cachedir = os.path.join(CONF['cachedir'], 'bincache%d%d_%s_%s' % (strip, upx, pyver, arch)) 190 if not os.path.exists(cachedir): 191 os.makedirs(cachedir) 192 cacheindexfn = os.path.join(cachedir, "index.dat") 193 if os.path.exists(cacheindexfn): 194 try: 195 cache_index = load_py_data_struct(cacheindexfn) 196 except Exception as e: 197 # tell the user they may want to fix their cache 198 # .. however, don't delete it for them; if it keeps getting 199 # corrupted, we'll never find out 200 logger.warn("pyinstaller bincache may be corrupted; " 201 "use pyinstaller --clean to fix") 202 raise 203 else: 204 cache_index = {} 205 206 # Verify if the file we're looking for is present in the cache. 207 # Use the dist_mn if given to avoid different extension modules 208 # sharing the same basename get corrupted. 209 if dist_nm: 210 basenm = os.path.normcase(dist_nm) 211 else: 212 basenm = os.path.normcase(os.path.basename(fnm)) 213 214 # Binding redirects should be taken into account to see if the file 215 # needs to be reprocessed. The redirects may change if the versions of dependent 216 # manifests change due to system updates. 217 redirects = CONF.get('binding_redirects', []) 218 digest = cacheDigest(fnm, redirects) 219 cachedfile = os.path.join(cachedir, basenm) 220 cmd = None 221 if basenm in cache_index: 222 if digest != cache_index[basenm]: 223 os.remove(cachedfile) 224 else: 225 # On Mac OS X we need relative paths to dll dependencies 226 # starting with @executable_path 227 if is_darwin: 228 dylib.mac_set_relative_dylib_deps(cachedfile, dist_nm) 229 return cachedfile 230 231 232 # Optionally change manifest and its deps to private assemblies 233 if fnm.lower().endswith(".manifest"): 234 manifest = winmanifest.Manifest() 235 manifest.filename = fnm 236 with open(fnm, "rb") as f: 237 manifest.parse_string(f.read()) 238 if CONF.get('win_private_assemblies', False): 239 if manifest.publicKeyToken: 240 logger.info("Changing %s into private assembly", os.path.basename(fnm)) 241 manifest.publicKeyToken = None 242 for dep in manifest.dependentAssemblies: 243 # Exclude common-controls which is not bundled 244 if dep.name != "Microsoft.Windows.Common-Controls": 245 dep.publicKeyToken = None 246 247 applyRedirects(manifest, redirects) 248 249 manifest.writeprettyxml(cachedfile) 250 return cachedfile 251 252 if upx: 253 if strip: 254 fnm = checkCache(fnm, strip=True, upx=False) 255 bestopt = "--best" 256 # FIXME: Linux builds of UPX do not seem to contain LZMA (they assert out) 257 # A better configure-time check is due. 258 if CONF["hasUPX"] >= (3,) and os.name == "nt": 259 bestopt = "--lzma" 260 261 upx_executable = "upx" 262 if CONF.get('upx_dir'): 263 upx_executable = os.path.join(CONF['upx_dir'], upx_executable) 264 cmd = [upx_executable, bestopt, "-q", cachedfile] 265 else: 266 if strip: 267 strip_options = [] 268 if is_darwin: 269 # The default strip behaviour breaks some shared libraries 270 # under Mac OSX. 271 # -S = strip only debug symbols. 272 strip_options = ["-S"] 273 cmd = ["strip"] + strip_options + [cachedfile] 274 275 if not os.path.exists(os.path.dirname(cachedfile)): 276 os.makedirs(os.path.dirname(cachedfile)) 277 # There are known some issues with 'shutil.copy2' on Mac OS X 10.11 278 # with copying st_flags. Issue #1650. 279 # 'shutil.copy' copies also permission bits and it should be sufficient for 280 # PyInstalle purposes. 281 shutil.copy(fnm, cachedfile) 282 # TODO find out if this is still necessary when no longer using shutil.copy2() 283 if hasattr(os, 'chflags'): 284 # Some libraries on FreeBSD have immunable flag (libthr.so.3, for example) 285 # If flags still remains, os.chmod will failed with: 286 # OSError: [Errno 1] Operation not permitted. 287 try: 288 os.chflags(cachedfile, 0) 289 except OSError: 290 pass 291 os.chmod(cachedfile, 0o755) 292 293 if os.path.splitext(fnm.lower())[1] in (".pyd", ".dll"): 294 # When shared assemblies are bundled into the app, they may optionally be 295 # changed into private assemblies. 296 try: 297 res = winmanifest.GetManifestResources(os.path.abspath(cachedfile)) 298 except winresource.pywintypes.error as e: 299 if e.args[0] == winresource.ERROR_BAD_EXE_FORMAT: 300 # Not a win32 PE file 301 pass 302 else: 303 logger.error(os.path.abspath(cachedfile)) 304 raise 305 else: 306 if winmanifest.RT_MANIFEST in res and len(res[winmanifest.RT_MANIFEST]): 307 for name in res[winmanifest.RT_MANIFEST]: 308 for language in res[winmanifest.RT_MANIFEST][name]: 309 try: 310 manifest = winmanifest.Manifest() 311 manifest.filename = ":".join([cachedfile, 312 str(winmanifest.RT_MANIFEST), 313 str(name), 314 str(language)]) 315 manifest.parse_string(res[winmanifest.RT_MANIFEST][name][language], 316 False) 317 except Exception as exc: 318 logger.error("Cannot parse manifest resource %s, " 319 "%s", name, language) 320 logger.error("From file %s", cachedfile, exc_info=1) 321 else: 322 # optionally change manifest to private assembly 323 private = CONF.get('win_private_assemblies', False) 324 if private: 325 if manifest.publicKeyToken: 326 logger.info("Changing %s into a private assembly", 327 os.path.basename(fnm)) 328 manifest.publicKeyToken = None 329 330 # Change dep to private assembly 331 for dep in manifest.dependentAssemblies: 332 # Exclude common-controls which is not bundled 333 if dep.name != "Microsoft.Windows.Common-Controls": 334 dep.publicKeyToken = None 335 redirecting = applyRedirects(manifest, redirects) 336 if redirecting or private: 337 try: 338 manifest.update_resources(os.path.abspath(cachedfile), 339 [name], 340 [language]) 341 except Exception as e: 342 logger.error(os.path.abspath(cachedfile)) 343 raise 344 345 if cmd: 346 logger.info("Executing - " + ' '.join(cmd)) 347 # terminates if execution fails 348 compat.exec_command(*cmd) 349 350 # update cache index 351 cache_index[basenm] = digest 352 save_py_data_struct(cacheindexfn, cache_index) 353 354 # On Mac OS X we need relative paths to dll dependencies 355 # starting with @executable_path 356 if is_darwin: 357 dylib.mac_set_relative_dylib_deps(cachedfile, dist_nm) 358 return cachedfile 359 360 361def cacheDigest(fnm, redirects): 362 hasher = hashlib.md5() 363 with open(fnm, "rb") as f: 364 for chunk in iter(lambda: f.read(16 * 1024), b""): 365 hasher.update(chunk) 366 if redirects: 367 redirects = str(redirects) 368 if is_py3: 369 redirects = redirects.encode('utf-8') 370 hasher.update(redirects) 371 digest = bytearray(hasher.digest()) 372 return digest 373 374 375def _check_path_overlap(path): 376 """ 377 Check that path does not overlap with WORKPATH or SPECPATH (i.e. 378 WORKPATH and SPECPATH may not start with path, which could be 379 caused by a faulty hand-edited specfile) 380 381 Raise SystemExit if there is overlap, return True otherwise 382 """ 383 from ..config import CONF 384 specerr = 0 385 if CONF['workpath'].startswith(path): 386 logger.error('Specfile error: The output path "%s" contains ' 387 'WORKPATH (%s)', path, CONF['workpath']) 388 specerr += 1 389 if CONF['specpath'].startswith(path): 390 logger.error('Specfile error: The output path "%s" contains ' 391 'SPECPATH (%s)', path, CONF['specpath']) 392 specerr += 1 393 if specerr: 394 raise SystemExit('Error: Please edit/recreate the specfile (%s) ' 395 'and set a different output name (e.g. "dist").' 396 % CONF['spec']) 397 return True 398 399 400def _make_clean_directory(path): 401 """ 402 Create a clean directory from the given directory name 403 """ 404 if _check_path_overlap(path): 405 if os.path.isdir(path) or os.path.isfile(path): 406 try: 407 os.remove(path) 408 except OSError: 409 _rmtree(path) 410 411 os.makedirs(path) 412 413 414def _rmtree(path): 415 """ 416 Remove directory and all its contents, but only after user confirmation, 417 or if the -y option is set 418 """ 419 from ..config import CONF 420 if CONF['noconfirm']: 421 choice = 'y' 422 elif sys.stdout.isatty(): 423 choice = compat.stdin_input('WARNING: The output directory "%s" and ALL ITS ' 424 'CONTENTS will be REMOVED! Continue? (y/N)' % path) 425 else: 426 raise SystemExit('Error: The output directory "%s" is not empty. ' 427 'Please remove all its contents or use the ' 428 '-y option (remove output directory without ' 429 'confirmation).' % path) 430 if choice.strip().lower() == 'y': 431 logger.info('Removing dir %s', path) 432 shutil.rmtree(path) 433 else: 434 raise SystemExit('User aborted') 435 436 437# TODO Refactor to prohibit empty target directories. As the docstring 438#below documents, this function currently permits the second item of each 439#2-tuple in "hook.datas" to be the empty string, in which case the target 440#directory defaults to the source directory's basename. However, this 441#functionality is very fragile and hence bad. Instead: 442# 443#* An exception should be raised if such item is empty. 444#* All hooks currently passing the empty string for such item (e.g., 445# "hooks/hook-babel.py", "hooks/hook-matplotlib.py") should be refactored 446# to instead pass such basename. 447def format_binaries_and_datas(binaries_or_datas, workingdir=None): 448 """ 449 Convert the passed list of hook-style 2-tuples into a returned set of 450 `TOC`-style 2-tuples. 451 452 Elements of the passed list are 2-tuples `(source_dir_or_glob, target_dir)`. 453 Elements of the returned set are 2-tuples `(target_file, source_file)`. 454 For backwards compatibility, the order of elements in the former tuples are 455 the reverse of the order of elements in the latter tuples! 456 457 Parameters 458 ---------- 459 binaries_or_datas : list 460 List of hook-style 2-tuples (e.g., the top-level `binaries` and `datas` 461 attributes defined by hooks) whose: 462 * The first element is either: 463 * A glob matching only the absolute or relative paths of source 464 non-Python data files. 465 * The absolute or relative path of a source directory containing only 466 source non-Python data files. 467 * The second element ist he relative path of the target directory 468 into which these source files will be recursively copied. 469 470 If the optional `workingdir` parameter is passed, source paths may be 471 either absolute or relative; else, source paths _must_ be absolute. 472 workingdir : str 473 Optional absolute path of the directory to which all relative source 474 paths in the `binaries_or_datas` parameter will be prepended by (and 475 hence converted into absolute paths) _or_ `None` if these paths are to 476 be preserved as relative. Defaults to `None`. 477 478 Returns 479 ---------- 480 set 481 Set of `TOC`-style 2-tuples whose: 482 * First element is the absolute or relative path of a target file. 483 * Second element is the absolute or relative path of the corresponding 484 source file to be copied to this target file. 485 """ 486 toc_datas = set() 487 488 for src_root_path_or_glob, trg_root_dir in binaries_or_datas: 489 if not trg_root_dir: 490 raise SystemExit("Empty DEST not allowed when adding binary " 491 "and data files. " 492 "Maybe you want to used %r.\nCaused by %r." % 493 (os.curdir, src_root_path_or_glob)) 494 # Convert relative to absolute paths if required. 495 if workingdir and not os.path.isabs(src_root_path_or_glob): 496 src_root_path_or_glob = os.path.join( 497 workingdir, src_root_path_or_glob) 498 499 # Normalize paths. 500 src_root_path_or_glob = os.path.normpath(src_root_path_or_glob) 501 if os.path.isfile(src_root_path_or_glob): 502 src_root_paths = [src_root_path_or_glob] 503 else: 504 # List of the absolute paths of all source paths matching the 505 # current glob. 506 src_root_paths = glob.glob(src_root_path_or_glob) 507 508 if not src_root_paths: 509 msg = 'Unable to find "%s" when adding binary and data files.' % ( 510 src_root_path_or_glob) 511 # on Debian/Ubuntu, missing pyconfig.h files can be fixed with 512 # installing python-dev 513 if src_root_path_or_glob.endswith("pyconfig.h"): 514 msg += """This would mean your Python installation doesn't 515come with proper library files. This usually happens by missing development 516package, or unsuitable build parameters of Python installation. 517* On Debian/Ubuntu, you would need to install Python development packages 518 * apt-get install python3-dev 519 * apt-get install python-dev 520* If you're building Python by yourself, please rebuild your Python with 521`--enable-shared` (or, `--enable-framework` on Darwin) 522""" 523 raise SystemExit(msg) 524 525 for src_root_path in src_root_paths: 526 if os.path.isfile(src_root_path): 527 # Normalizing the result to remove redundant relative 528 # paths (e.g., removing "./" from "trg/./file"). 529 toc_datas.add(( 530 os.path.normpath(os.path.join( 531 trg_root_dir, os.path.basename(src_root_path))), 532 os.path.normpath(src_root_path))) 533 elif os.path.isdir(src_root_path): 534 for src_dir, src_subdir_basenames, src_file_basenames in \ 535 os.walk(src_root_path): 536 # Ensure the current source directory is a subdirectory 537 # of the passed top-level source directory. Since 538 # os.walk() does *NOT* follow symlinks by default, this 539 # should be the case. (But let's make sure.) 540 assert src_dir.startswith(src_root_path) 541 542 # Relative path of the current target directory, 543 # obtained by: 544 # 545 # * Stripping the top-level source directory from the 546 # current source directory (e.g., removing "/top" from 547 # "/top/dir"). 548 # * Normalizing the result to remove redundant relative 549 # paths (e.g., removing "./" from "trg/./file"). 550 trg_dir = os.path.normpath(os.path.join( 551 trg_root_dir, 552 os.path.relpath(src_dir, src_root_path))) 553 554 for src_file_basename in src_file_basenames: 555 src_file = os.path.join(src_dir, src_file_basename) 556 if os.path.isfile(src_file): 557 # Normalize the result to remove redundant relative 558 # paths (e.g., removing "./" from "trg/./file"). 559 toc_datas.add(( 560 os.path.normpath( 561 os.path.join(trg_dir, src_file_basename)), 562 os.path.normpath(src_file))) 563 564 return toc_datas 565 566 567def _load_code(modname, filename): 568 path_item = os.path.dirname(filename) 569 if os.path.basename(filename).startswith('__init__.py'): 570 # this is a package 571 path_item = os.path.dirname(path_item) 572 if os.path.basename(path_item) == '__pycache__': 573 path_item = os.path.dirname(path_item) 574 importer = pkgutil.get_importer(path_item) 575 package, _, modname = modname.rpartition('.') 576 577 if sys.version_info >= (3, 3) and hasattr(importer, 'find_loader'): 578 loader, portions = importer.find_loader(modname) 579 else: 580 loader = importer.find_module(modname) 581 582 logger.debug('Compiling %s', filename) 583 if loader and hasattr(loader, 'get_code'): 584 return loader.get_code(modname) 585 else: 586 # Just as ``python foo.bar`` will read and execute statements in 587 # ``foo.bar``, even though it lacks the ``.py`` extension, so 588 # ``pyinstaller foo.bar`` should also work. However, Python's import 589 # machinery doesn't load files without a ``.py`` extension. So, use 590 # ``compile`` instead. 591 # 592 # On a side note, neither the Python 2 nor Python 3 calls to 593 # ``pkgutil`` and ``find_module`` above handle modules ending in 594 # ``.pyw``, even though ``imp.find_module`` and ``import <name>`` both 595 # work. This code supports ``.pyw`` files. 596 597 # Open the source file in binary mode and allow the `compile()` call to 598 # detect the source encoding. 599 with open_file(filename, 'rb') as f: 600 source = f.read() 601 return compile(source, filename, 'exec') 602 603def get_code_object(modname, filename): 604 """ 605 Get the code-object for a module. 606 607 This is a extra-simple version for compiling a module. It's 608 not worth spending more effort here, as it is only used in the 609 rare case if outXX-Analysis.toc exists, but outXX-PYZ.toc does 610 not. 611 """ 612 613 try: 614 if filename in ('-', None): 615 # This is a NamespacePackage, modulegraph marks them 616 # by using the filename '-'. (But wants to use None, 617 # so check for None, too, to be forward-compatible.) 618 logger.debug('Compiling namespace package %s', modname) 619 txt = '#\n' 620 return compile(txt, filename, 'exec') 621 else: 622 logger.debug('Compiling %s', filename) 623 co = _load_code(modname, filename) 624 if not co: 625 raise ValueError("Module file %s is missing" % filename) 626 return co 627 except SyntaxError as e: 628 print("Syntax error in ", filename) 629 print(e.args) 630 raise 631 632 633def strip_paths_in_code(co, new_filename=None): 634 635 # Paths to remove from filenames embedded in code objects 636 replace_paths = sys.path + CONF['pathex'] 637 # Make sure paths end with os.sep 638 replace_paths = [os.path.join(f, '') for f in replace_paths] 639 640 if new_filename is None: 641 original_filename = os.path.normpath(co.co_filename) 642 for f in replace_paths: 643 if original_filename.startswith(f): 644 new_filename = original_filename[len(f):] 645 break 646 647 else: 648 return co 649 650 code_func = type(co) 651 652 consts = tuple( 653 strip_paths_in_code(const_co, new_filename) 654 if isinstance(const_co, code_func) else const_co 655 for const_co in co.co_consts 656 ) 657 658 # co_kwonlyargcount added in some version of Python 3 659 if hasattr(co, 'co_kwonlyargcount'): 660 return code_func(co.co_argcount, co.co_kwonlyargcount, co.co_nlocals, co.co_stacksize, 661 co.co_flags, co.co_code, consts, co.co_names, 662 co.co_varnames, new_filename, co.co_name, 663 co.co_firstlineno, co.co_lnotab, 664 co.co_freevars, co.co_cellvars) 665 else: 666 return code_func(co.co_argcount, co.co_nlocals, co.co_stacksize, 667 co.co_flags, co.co_code, consts, co.co_names, 668 co.co_varnames, new_filename, co.co_name, 669 co.co_firstlineno, co.co_lnotab, 670 co.co_freevars, co.co_cellvars) 671 672 673def fake_pyc_timestamp(buf): 674 """ 675 Reset the timestamp from a .pyc-file header to a fixed value. 676 677 This enables deterministic builds without having to set pyinstaller 678 source metadata (mtime) since that changes the pyc-file contents. 679 680 _buf_ must at least contain the full pyc-file header. 681 """ 682 assert buf[:4] == compat.BYTECODE_MAGIC, \ 683 "Expected pyc magic {}, got {}".format(compat.BYTECODE_MAGIC, buf[:4]) 684 start, end = 4, 8 685 if is_py37: 686 # see https://www.python.org/dev/peps/pep-0552/ 687 (flags,) = struct.unpack_from(">I", buf, 4) 688 if flags & 1: 689 # We are in the future and hash-based pyc-files are used, so 690 # clear "check_source" flag, since there is no source 691 buf[4:8] = struct.pack(">I", flags ^ 2) 692 return buf 693 else: 694 # no hash-based pyc-file, timestamp is the next field 695 start, end = 8, 12 696 697 ts = b'pyi0' # So people know where this comes from 698 return buf[:start] + ts + buf[end:] 699