#----------------------------------------------------------------------------- # Copyright (c) 2005-2023, PyInstaller Development Team. # # Distributed under the terms of the GNU General Public License (version 2 # or later) with exception for distributing the bootloader. # # The full license is in the file COPYING.txt, distributed with this software. # # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception) #----------------------------------------------------------------------------- # --- functions for checking guts --- # NOTE: by GUTS it is meant intermediate files and data structures that PyInstaller creates for bundling files and # creating final executable. import fnmatch import glob import hashlib import marshal import os import pathlib import platform import py_compile import shutil import struct import subprocess import sys from PyInstaller import compat from PyInstaller import log as logging from PyInstaller.compat import (EXTENSION_SUFFIXES, is_cygwin, is_darwin, is_win) from PyInstaller.config import CONF from PyInstaller.depend import dylib from PyInstaller.depend.bindepend import match_binding_redirect from PyInstaller.utils import misc if is_win: from PyInstaller.utils.win32 import versioninfo, winmanifest, winresource if is_darwin: import PyInstaller.utils.osx as osxutils logger = logging.getLogger(__name__) # -- Helpers for checking guts. # # NOTE: by _GUTS it is meant intermediate files and data structures that PyInstaller creates for bundling files and # creating final executable. def _check_guts_eq(attr_name, old_value, new_value, last_build): """ Rebuild is required if values differ. """ if old_value != new_value: logger.info("Building because %s changed", attr_name) return True return False def _check_guts_toc_mtime(attr_name, old_toc, new_toc, last_build): """ Rebuild is required if mtimes of files listed in old TOC are newer than last_build. Use this for calculated/analysed values read from cache. """ for dest_name, src_name, typecode in old_toc: if misc.mtime(src_name) > last_build: logger.info("Building because %s changed", src_name) return True return False def _check_guts_toc(attr_name, old_toc, new_toc, last_build): """ Rebuild is required if either TOC content changed or mtimes of files listed in old TOC are newer than last_build. Use this for input parameters. """ return _check_guts_eq(attr_name, old_toc, new_toc, last_build) or \ _check_guts_toc_mtime(attr_name, old_toc, new_toc, last_build) def add_suffix_to_extension(dest_name, src_name, typecode): """ Take a TOC entry (dest_name, src_name, typecode) and adjust the dest_name for EXTENSION to include the full library suffix. """ # No-op for non-extension if typecode != 'EXTENSION': return dest_name, src_name, typecode # If dest_name completely fits into end of the src_name, it has already been processed. if src_name.endswith(dest_name): return dest_name, src_name, typecode # Change the dotted name into a relative path. This places C extensions in the Python-standard location. dest_name = dest_name.replace('.', os.sep) # In some rare cases extension might already contain a suffix. Skip it in this case. if os.path.splitext(dest_name)[1] not in EXTENSION_SUFFIXES: # Determine the base name of the file. base_name = os.path.basename(dest_name) assert '.' not in base_name # Use this file's existing extension. For extensions such as ``libzmq.cp36-win_amd64.pyd``, we cannot use # ``os.path.splitext``, which would give only the ```.pyd`` part of the extension. dest_name = dest_name + os.path.basename(src_name)[len(base_name):] return dest_name, src_name, typecode def applyRedirects(manifest, redirects): """ Apply the binding redirects specified by 'redirects' to the dependent assemblies of 'manifest'. :param manifest: :type manifest: :param redirects: :type redirects: :return: :rtype: """ redirecting = False for binding in redirects: for dep in manifest.dependentAssemblies: if match_binding_redirect(dep, binding): logger.info("Redirecting %s version %s -> %s", binding.name, dep.version, binding.newVersion) dep.version = binding.newVersion redirecting = True return redirecting def checkCache( fnm, strip=False, upx=False, upx_exclude=None, dist_nm=None, target_arch=None, codesign_identity=None, entitlements_file=None, strict_arch_validation=False ): """ Cache prevents preprocessing binary files again and again. 'dist_nm' Filename relative to dist directory. We need it on Mac to determine level of paths for @loader_path like '@loader_path/../../' for qt4 plugins. """ from PyInstaller.config import CONF # Binding redirects should be taken into account to see if the file needs to be reprocessed. The redirects may # change if the versions of dependent manifests change due to system updates. redirects = CONF.get('binding_redirects', []) # optionally change manifest to private assembly win_private_assemblies = CONF.get('win_private_assemblies', False) # Disable UPX on non-Windows. Using UPX (3.96) on modern Linux shared libraries (for example, the python3.x.so # shared library) seems to result in segmentation fault when they are dlopen'd. This happens in recent versions # of Fedora and Ubuntu linux, as well as in Alpine containers. On Mac OS, UPX (3.96) fails with # UnknownExecutableFormatException on most .dylibs (and interferes with code signature on other occasions). And # even when it would succeed, compressed libraries cannot be (re)signed due to failed strict validation. upx = upx and (is_win or is_cygwin) # On Mac OS, a cache is required anyway to keep the libraries with relative install names. # Caching on Mac OS does not work since we need to modify binary headers to use relative paths to dll dependencies # and starting with '@loader_path'. if not strip and not upx and not is_darwin and not (is_win and (redirects or win_private_assemblies)): return fnm # Match against provided UPX exclude patterns. upx_exclude = upx_exclude or [] if upx: fnm_path = pathlib.PurePath(fnm) for upx_exclude_entry in upx_exclude: # pathlib.PurePath.match() matches from right to left, and supports * wildcard, but does not support the # "**" syntax for directory recursion. Case sensitivity follows the OS default. if fnm_path.match(upx_exclude_entry): logger.info("Disabling UPX for %s due to match in exclude pattern: %s", fnm, upx_exclude_entry) upx = False break # Load cache index. # Make cachedir per Python major/minor version. # This allows parallel building of executables with different Python versions as one user. pyver = 'py%d%s' % (sys.version_info[0], sys.version_info[1]) arch = platform.architecture()[0] cachedir = os.path.join(CONF['cachedir'], 'bincache%d%d_%s_%s' % (strip, upx, pyver, arch)) if target_arch: cachedir = os.path.join(cachedir, target_arch) if is_darwin: # Separate by codesign identity if codesign_identity: # Compute hex digest of codesign identity string to prevent issues with invalid characters. csi_hash = hashlib.sha256(codesign_identity.encode('utf-8')) cachedir = os.path.join(cachedir, csi_hash.hexdigest()) else: cachedir = os.path.join(cachedir, 'adhoc') # ad-hoc signing # Separate by entitlements if entitlements_file: # Compute hex digest of entitlements file contents with open(entitlements_file, 'rb') as fp: ef_hash = hashlib.sha256(fp.read()) cachedir = os.path.join(cachedir, ef_hash.hexdigest()) else: cachedir = os.path.join(cachedir, 'no-entitlements') if not os.path.exists(cachedir): os.makedirs(cachedir) cacheindexfn = os.path.join(cachedir, "index.dat") if os.path.exists(cacheindexfn): try: cache_index = misc.load_py_data_struct(cacheindexfn) except Exception: # Tell the user they may want to fix their cache... However, do not delete it for them; if it keeps getting # corrupted, we will never find out. logger.warning("PyInstaller bincache may be corrupted; use pyinstaller --clean to fix it.") raise else: cache_index = {} # Verify that the file we are looking for is present in the cache. Use the dist_mn if given to avoid different # extension modules sharing the same basename get corrupted. if dist_nm: basenm = os.path.normcase(dist_nm) else: basenm = os.path.normcase(os.path.basename(fnm)) digest = cacheDigest(fnm, redirects) cachedfile = os.path.join(cachedir, basenm) cmd = None if basenm in cache_index: if digest != cache_index[basenm]: os.remove(cachedfile) else: return cachedfile # Optionally change manifest and its dependencies to private assemblies. if fnm.lower().endswith(".manifest") and is_win: manifest = winmanifest.Manifest() manifest.filename = fnm with open(fnm, "rb") as f: manifest.parse_string(f.read()) if CONF.get('win_private_assemblies', False): if manifest.publicKeyToken: logger.info("Changing %s into private assembly", os.path.basename(fnm)) manifest.publicKeyToken = None for dep in manifest.dependentAssemblies: # Exclude common-controls which is not bundled if dep.name != "Microsoft.Windows.Common-Controls": dep.publicKeyToken = None applyRedirects(manifest, redirects) manifest.writeprettyxml(cachedfile) return cachedfile if upx: if strip: fnm = checkCache( fnm, strip=True, upx=False, dist_nm=dist_nm, target_arch=target_arch, codesign_identity=codesign_identity, entitlements_file=entitlements_file, strict_arch_validation=strict_arch_validation, ) # We need to avoid using UPX with Windows DLLs that have Control Flow Guard enabled, as it breaks them. if is_win and versioninfo.pefile_check_control_flow_guard(fnm): logger.info('Disabling UPX for %s due to CFG!', fnm) elif misc.is_file_qt_plugin(fnm): logger.info('Disabling UPX for %s due to it being a Qt plugin!', fnm) else: bestopt = "--best" # FIXME: Linux builds of UPX do not seem to contain LZMA (they assert out). # A better configure-time check is due. if CONF["hasUPX"] >= (3,) and os.name == "nt": bestopt = "--lzma" upx_executable = "upx" if CONF.get('upx_dir'): upx_executable = os.path.join(CONF['upx_dir'], upx_executable) cmd = [upx_executable, bestopt, "-q", cachedfile] else: if strip: strip_options = [] if is_darwin: # The default strip behavior breaks some shared libraries under Mac OS. strip_options = ["-S"] # -S = strip only debug symbols. cmd = ["strip"] + strip_options + [cachedfile] if not os.path.exists(os.path.dirname(cachedfile)): os.makedirs(os.path.dirname(cachedfile)) # There are known some issues with 'shutil.copy2' on Mac OS 10.11 with copying st_flags. Issue #1650. # 'shutil.copy' copies also permission bits and it should be sufficient for PyInstaller's purposes. shutil.copy(fnm, cachedfile) # TODO: find out if this is still necessary when no longer using shutil.copy2() if hasattr(os, 'chflags'): # Some libraries on FreeBSD have immunable flag (libthr.so.3, for example). If this flag is preserved, # os.chmod() fails with: OSError: [Errno 1] Operation not permitted. try: os.chflags(cachedfile, 0) except OSError: pass os.chmod(cachedfile, 0o755) if os.path.splitext(fnm.lower())[1] in (".pyd", ".dll") and is_win: # When shared assemblies are bundled into the app, they may optionally be changed into private assemblies. try: res = winmanifest.GetManifestResources(os.path.abspath(cachedfile)) except winresource.pywintypes.error as e: if e.args[0] == winresource.ERROR_BAD_EXE_FORMAT: # Not a win32 PE file pass else: logger.error(os.path.abspath(cachedfile)) raise else: if winmanifest.RT_MANIFEST in res and len(res[winmanifest.RT_MANIFEST]): for name in res[winmanifest.RT_MANIFEST]: for language in res[winmanifest.RT_MANIFEST][name]: try: manifest = winmanifest.Manifest() manifest.filename = ":".join([ cachedfile, str(winmanifest.RT_MANIFEST), str(name), str(language) ]) manifest.parse_string(res[winmanifest.RT_MANIFEST][name][language], False) except Exception: logger.error("Cannot parse manifest resource %s, =%s", name, language) logger.error("From file %s", cachedfile, exc_info=1) else: if win_private_assemblies: if manifest.publicKeyToken: logger.info("Changing %s into a private assembly", os.path.basename(fnm)) manifest.publicKeyToken = None # Change dep to private assembly for dep in manifest.dependentAssemblies: # Exclude common-controls which is not bundled if dep.name != "Microsoft.Windows.Common-Controls": dep.publicKeyToken = None redirecting = applyRedirects(manifest, redirects) if redirecting or win_private_assemblies: try: manifest.update_resources(os.path.abspath(cachedfile), [name], [language]) except Exception: logger.error(os.path.abspath(cachedfile)) raise if cmd: logger.info("Executing - " + "".join(cmd)) subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # update cache index cache_index[basenm] = digest misc.save_py_data_struct(cacheindexfn, cache_index) # On Mac OS we need relative paths to dll dependencies starting with @executable_path. While modifying # the headers invalidates existing signatures, we avoid removing them in order to speed things up (and # to avoid potential bugs in the codesign utility, like the one reported on Mac OS 10.13 in #6167). # The forced re-signing at the end should take care of the invalidated signatures. if is_darwin: try: osxutils.binary_to_target_arch(cachedfile, target_arch, display_name=fnm) #osxutils.remove_signature_from_binary(cachedfile) # Disabled as per comment above. dylib.mac_set_relative_dylib_deps(cachedfile, dist_nm) osxutils.sign_binary(cachedfile, codesign_identity, entitlements_file) except osxutils.InvalidBinaryError: # Raised by osxutils.binary_to_target_arch when the given file is not a valid macOS binary (for example, # a linux .so file; see issue #6327). The error prevents any further processing, so just ignore it. pass except osxutils.IncompatibleBinaryArchError: # Raised by osxutils.binary_to_target_arch when the given file does not contain (all) required arch slices. # Depending on the strict validation mode, re-raise or swallow the error. # # Strict validation should be enabled only for binaries where the architecture *must* match the target one, # i.e., the extension modules. Everything else is pretty much a gray area, for example: # * a universal2 extension may have its x86_64 and arm64 slices linked against distinct single-arch/thin # shared libraries # * a collected executable that is launched by python code via a subprocess can be x86_64-only, even though # the actual python code is running on M1 in native arm64 mode. if strict_arch_validation: raise logger.debug("File %s failed optional architecture validation - collecting as-is!", fnm) return cachedfile def cacheDigest(fnm, redirects): hasher = hashlib.md5() with open(fnm, "rb") as f: for chunk in iter(lambda: f.read(16 * 1024), b""): hasher.update(chunk) if redirects: redirects = str(redirects).encode('utf-8') hasher.update(redirects) digest = bytearray(hasher.digest()) return digest def _check_path_overlap(path): """ Check that path does not overlap with WORKPATH or SPECPATH (i.e., WORKPATH and SPECPATH may not start with path, which could be caused by a faulty hand-edited specfile). Raise SystemExit if there is overlap, return True otherwise """ from PyInstaller.config import CONF specerr = 0 if CONF['workpath'].startswith(path): logger.error('Specfile error: The output path "%s" contains WORKPATH (%s)', path, CONF['workpath']) specerr += 1 if CONF['specpath'].startswith(path): logger.error('Specfile error: The output path "%s" contains SPECPATH (%s)', path, CONF['specpath']) specerr += 1 if specerr: raise SystemExit( 'Error: Please edit/recreate the specfile (%s) and set a different output name (e.g. "dist").' % CONF['spec'] ) return True def _make_clean_directory(path): """ Create a clean directory from the given directory name. """ if _check_path_overlap(path): if os.path.isdir(path) or os.path.isfile(path): try: os.remove(path) except OSError: _rmtree(path) os.makedirs(path, exist_ok=True) def _rmtree(path): """ Remove directory and all its contents, but only after user confirmation, or if the -y option is set. """ from PyInstaller.config import CONF if CONF['noconfirm']: choice = 'y' elif sys.stdout.isatty(): choice = input( 'WARNING: The output directory "%s" and ALL ITS CONTENTS will be REMOVED! Continue? (y/N)' % path ) else: raise SystemExit( 'Error: The output directory "%s" is not empty. Please remove all its contents or use the -y option (remove' ' output directory without confirmation).' % path ) if choice.strip().lower() == 'y': if not CONF['noconfirm']: print("On your own risk, you can use the option `--noconfirm` to get rid of this question.") logger.info('Removing dir %s', path) shutil.rmtree(path) else: raise SystemExit('User aborted') # TODO Refactor to prohibit empty target directories. As the docstring below documents, this function currently permits # the second item of each 2-tuple in "hook.datas" to be the empty string, in which case the target directory defaults to # the source directory's basename. However, this functionality is very fragile and hence bad. Instead: # # * An exception should be raised if such item is empty. # * All hooks currently passing the empty string for such item (e.g., # "hooks/hook-babel.py", "hooks/hook-matplotlib.py") should be refactored # to instead pass such basename. def format_binaries_and_datas(binaries_or_datas, workingdir=None): """ Convert the passed list of hook-style 2-tuples into a returned set of `TOC`-style 2-tuples. Elements of the passed list are 2-tuples `(source_dir_or_glob, target_dir)`. Elements of the returned set are 2-tuples `(target_file, source_file)`. For backwards compatibility, the order of elements in the former tuples are the reverse of the order of elements in the latter tuples! Parameters ---------- binaries_or_datas : list List of hook-style 2-tuples (e.g., the top-level `binaries` and `datas` attributes defined by hooks) whose: * The first element is either: * A glob matching only the absolute or relative paths of source non-Python data files. * The absolute or relative path of a source directory containing only source non-Python data files. * The second element is the relative path of the target directory into which these source files will be recursively copied. If the optional `workingdir` parameter is passed, source paths may be either absolute or relative; else, source paths _must_ be absolute. workingdir : str Optional absolute path of the directory to which all relative source paths in the `binaries_or_datas` parameter will be prepended by (and hence converted into absolute paths) _or_ `None` if these paths are to be preserved as relative. Defaults to `None`. Returns ---------- set Set of `TOC`-style 2-tuples whose: * First element is the absolute or relative path of a target file. * Second element is the absolute or relative path of the corresponding source file to be copied to this target file. """ toc_datas = set() for src_root_path_or_glob, trg_root_dir in binaries_or_datas: # Disallow empty source path. Those are typically result of errors, and result in implicit collection of the # whole current working directory, which is never a good idea. if not src_root_path_or_glob: raise SystemExit( "Empty SRC is not allowed when adding binary and data files, as it would result in collection of the " "whole current working directory." ) if not trg_root_dir: raise SystemExit( "Empty DEST not allowed when adding binary and data files. Maybe you want to used %r.\nCaused by %r." % (os.curdir, src_root_path_or_glob) ) # Convert relative to absolute paths if required. if workingdir and not os.path.isabs(src_root_path_or_glob): src_root_path_or_glob = os.path.join(workingdir, src_root_path_or_glob) # Normalize paths. src_root_path_or_glob = os.path.normpath(src_root_path_or_glob) if os.path.isfile(src_root_path_or_glob): src_root_paths = [src_root_path_or_glob] else: # List of the absolute paths of all source paths matching the current glob. src_root_paths = glob.glob(src_root_path_or_glob) if not src_root_paths: msg = 'Unable to find "%s" when adding binary and data files.' % src_root_path_or_glob # on Debian/Ubuntu, missing pyconfig.h files can be fixed with installing python-dev if src_root_path_or_glob.endswith("pyconfig.h"): msg += """This means your Python installation does not come with proper shared library files. This usually happens due to missing development package, or unsuitable build parameters of the Python installation. * On Debian/Ubuntu, you need to install Python development packages: * apt-get install python3-dev * apt-get install python-dev * If you are building Python by yourself, rebuild with `--enable-shared` (or, `--enable-framework` on macOS). """ raise SystemExit(msg) for src_root_path in src_root_paths: if os.path.isfile(src_root_path): # Normalizing the result to remove redundant relative paths (e.g., removing "./" from "trg/./file"). toc_datas.add(( os.path.normpath(os.path.join(trg_root_dir, os.path.basename(src_root_path))), os.path.normpath(src_root_path), )) elif os.path.isdir(src_root_path): for src_dir, src_subdir_basenames, src_file_basenames in os.walk(src_root_path): # Ensure the current source directory is a subdirectory of the passed top-level source directory. # Since os.walk() does *NOT* follow symlinks by default, this should be the case. (But let's make # sure.) assert src_dir.startswith(src_root_path) # Relative path of the current target directory, obtained by: # # * Stripping the top-level source directory from the current source directory (e.g., removing # "/top" from "/top/dir"). # * Normalizing the result to remove redundant relative paths (e.g., removing "./" from # "trg/./file"). trg_dir = os.path.normpath(os.path.join(trg_root_dir, os.path.relpath(src_dir, src_root_path))) for src_file_basename in src_file_basenames: src_file = os.path.join(src_dir, src_file_basename) if os.path.isfile(src_file): # Normalize the result to remove redundant relative paths (e.g., removing "./" from # "trg/./file"). toc_datas.add(( os.path.normpath(os.path.join(trg_dir, src_file_basename)), os.path.normpath(src_file) )) return toc_datas def get_code_object(modname, filename): """ Get the code-object for a module. This is a simplifed non-performant version which circumvents __pycache__. """ try: if filename in ('-', None): # This is a NamespacePackage, modulegraph marks them by using the filename '-'. (But wants to use None, so # check for None, too, to be forward-compatible.) logger.debug('Compiling namespace package %s', modname) txt = '#\n' return compile(txt, filename, 'exec') else: logger.debug('Compiling %s', filename) with open(filename, 'rb') as f: source = f.read() return compile(source, filename, 'exec') except SyntaxError as e: print("Syntax error in ", filename) print(e.args) raise def strip_paths_in_code(co, new_filename=None): # Paths to remove from filenames embedded in code objects replace_paths = sys.path + CONF['pathex'] # Make sure paths end with os.sep and the longest paths are first replace_paths = sorted((os.path.join(f, '') for f in replace_paths), key=len, reverse=True) if new_filename is None: original_filename = os.path.normpath(co.co_filename) for f in replace_paths: if original_filename.startswith(f): new_filename = original_filename[len(f):] break else: return co code_func = type(co) consts = tuple( strip_paths_in_code(const_co, new_filename) if isinstance(const_co, code_func) else const_co for const_co in co.co_consts ) if hasattr(co, 'replace'): # is_py38 return co.replace(co_consts=consts, co_filename=new_filename) elif hasattr(co, 'co_kwonlyargcount'): # co_kwonlyargcount was added in some version of Python 3 return code_func( co.co_argcount, co.co_kwonlyargcount, co.co_nlocals, co.co_stacksize, co.co_flags, co.co_code, consts, co.co_names, co.co_varnames, new_filename, co.co_name, co.co_firstlineno, co.co_lnotab, co.co_freevars, co.co_cellvars ) else: return code_func( co.co_argcount, co.co_nlocals, co.co_stacksize, co.co_flags, co.co_code, consts, co.co_names, co.co_varnames, new_filename, co.co_name, co.co_firstlineno, co.co_lnotab, co.co_freevars, co.co_cellvars ) def _should_include_system_binary(binary_tuple, exceptions): """ Return True if the given binary_tuple describes a system binary that should be included. Exclude all system library binaries other than those with "lib-dynload" in the destination or "python" in the source, except for those matching the patterns in the exceptions list. Intended to be used from the Analysis exclude_system_libraries method. """ dest = binary_tuple[0] if dest.startswith('lib-dynload'): return True src = binary_tuple[1] if fnmatch.fnmatch(src, '*python*'): return True if not src.startswith('/lib') and not src.startswith('/usr/lib'): return True for exception in exceptions: if fnmatch.fnmatch(dest, exception): return True return False def compile_pymodule(name, src_path, workpath, code_cache=None): """ Given the TOC entry (name, path, typecode) for a pure-python module, compile the module in the specified working directory, and return the TOC entry for collecting the byte-compiled module. No-op for typecodes other than PYMODULE. """ # Construct the target .pyc filename in the workpath split_name = name.split(".") if "__init__" in src_path: # __init__ module; use "__init__" as module name, and construct parent path using all components of the # fully-qualified name parent_dirs = split_name mod_basename = "__init__" else: # Regular module; use last component of the fully-qualified name as module name, and the rest as the parent # path. parent_dirs = split_name[:-1] mod_basename = split_name[-1] pyc_path = os.path.join(workpath, *parent_dirs, mod_basename + '.pyc') # If .pyc file already exists in our workpath, check if we can re-use it. For that: # - its modification timestamp must be newer than that of the source file # - it must be compiled for compatible python version if os.path.exists(pyc_path): can_reuse = False if misc.mtime(pyc_path) > misc.mtime(src_path): with open(pyc_path, 'rb') as fh: can_reuse = fh.read(4) == compat.BYTECODE_MAGIC if can_reuse: return pyc_path # Ensure the existence of parent directories for the target pyc path os.makedirs(os.path.dirname(pyc_path), exist_ok=True) # Check if optional cache contains module entry code_object = code_cache.get(name, None) if code_cache else None if code_object is None: _, ext = os.path.splitext(src_path) ext = ext.lower() if ext == '.py': # Source py file; compile... py_compile.compile(src_path, pyc_path) # ... and read the contents with open(pyc_path, 'rb') as fp: pyc_data = fp.read() elif ext == '.pyc': # The module is available in binary-only form. Read it... with open(src_path, 'rb') as fp: pyc_data = fp.read() # ... verify the python version... if pyc_data[:4] != compat.BYTECODE_MAGIC: raise ValueError(f"The .pyc module {src_path} was compiled for incompatible version of python!") else: raise ValueError(f"Invalid python module file {src_path}; unhandled extension {ext}!") # Unmarshal code object; this is necessary if we want to strip paths from it code_object = marshal.loads(pyc_data[16:]) # Strip code paths from the code object code_object = strip_paths_in_code(code_object) # Write module file with open(pyc_path, 'wb') as fh: fh.write(compat.BYTECODE_MAGIC) fh.write(struct.pack('