| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781 |
- #-----------------------------------------------------------------------------
- # 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)
- #-----------------------------------------------------------------------------
- """
- PEP-302 and PEP-451 importers for frozen applications.
- """
- # **NOTE** This module is used during bootstrap.
- # Import *ONLY* builtin modules or modules that are collected into the base_library.zip archive.
- # List of built-in modules: sys.builtin_module_names
- # List of modules collected into base_library.zip: PyInstaller.compat.PY3_BASE_MODULES
- import sys
- import os
- import io
- import _frozen_importlib
- import _thread
- import pyimod01_archive
- if sys.flags.verbose and sys.stderr:
- def trace(msg, *a):
- sys.stderr.write(msg % a)
- sys.stderr.write("\n")
- else:
- def trace(msg, *a):
- pass
- def _decode_source(source_bytes):
- """
- Decode bytes representing source code and return the string. Universal newline support is used in the decoding.
- Based on CPython's implementation of the same functionality:
- https://github.com/python/cpython/blob/3.9/Lib/importlib/_bootstrap_external.py#L679-L688
- """
- # Local import to avoid including `tokenize` and its dependencies in `base_library.zip`
- from tokenize import detect_encoding
- source_bytes_readline = io.BytesIO(source_bytes).readline
- encoding = detect_encoding(source_bytes_readline)
- newline_decoder = io.IncrementalNewlineDecoder(decoder=None, translate=True)
- return newline_decoder.decode(source_bytes.decode(encoding[0]))
- # Global instance of PYZ archive reader. Initialized by install().
- pyz_archive = None
- # Some runtime hooks might need to traverse available frozen package/module hierarchy to simulate filesystem.
- # Such traversals can be efficiently implemented using a prefix tree (trie), whose computation we defer until first
- # access.
- _pyz_tree_lock = _thread.RLock()
- _pyz_tree = None
- def get_pyz_toc_tree():
- global _pyz_tree
- with _pyz_tree_lock:
- if _pyz_tree is None:
- _pyz_tree = _build_pyz_prefix_tree(pyz_archive)
- return _pyz_tree
- # Populate list of unresolved (original) and resolved paths to top-level directory, used when trying to determine
- # relative path.
- _TOP_LEVEL_DIRECTORY_PATHS = []
- # Original sys._MEIPASS value; ensure separators are normalized (e.g., when using msys2 python).
- _TOP_LEVEL_DIRECTORY = os.path.normpath(sys._MEIPASS)
- _TOP_LEVEL_DIRECTORY_PATHS.append(_TOP_LEVEL_DIRECTORY)
- # Fully resolve sys._MEIPASS in case its location is symlinked at some level; for example, system temporary directory
- # (used by onefile builds) is usually a symbolic link under macOS.
- _RESOLVED_TOP_LEVEL_DIRECTORY = os.path.realpath(_TOP_LEVEL_DIRECTORY)
- if os.path.normcase(_RESOLVED_TOP_LEVEL_DIRECTORY) != os.path.normcase(_TOP_LEVEL_DIRECTORY):
- _TOP_LEVEL_DIRECTORY_PATHS.append(_RESOLVED_TOP_LEVEL_DIRECTORY)
- # If we are running as macOS .app bundle, compute the alternative top-level directory path as well.
- _is_macos_app_bundle = False
- if sys.platform == 'darwin' and _TOP_LEVEL_DIRECTORY.endswith("Contents/Frameworks"):
- _is_macos_app_bundle = True
- _ALTERNATIVE_TOP_LEVEL_DIRECTORY = os.path.join(
- os.path.dirname(_TOP_LEVEL_DIRECTORY),
- 'Resources',
- )
- _TOP_LEVEL_DIRECTORY_PATHS.append(_ALTERNATIVE_TOP_LEVEL_DIRECTORY)
- _RESOLVED_ALTERNATIVE_TOP_LEVEL_DIRECTORY = os.path.join(
- os.path.dirname(_RESOLVED_TOP_LEVEL_DIRECTORY),
- 'Resources',
- )
- if _RESOLVED_ALTERNATIVE_TOP_LEVEL_DIRECTORY != _ALTERNATIVE_TOP_LEVEL_DIRECTORY:
- _TOP_LEVEL_DIRECTORY_PATHS.append(_RESOLVED_ALTERNATIVE_TOP_LEVEL_DIRECTORY)
- # Helper for computing PYZ prefix tree
- def _build_pyz_prefix_tree(pyz_archive):
- tree = dict()
- for entry_name, entry_data in pyz_archive.toc.items():
- name_components = entry_name.split('.')
- typecode = entry_data[0]
- current = tree
- if typecode in {pyimod01_archive.PYZ_ITEM_PKG, pyimod01_archive.PYZ_ITEM_NSPKG}:
- # Package; create new dictionary node for its modules
- for name_component in name_components:
- current = current.setdefault(name_component, {})
- else:
- # Module; create the leaf node (empty string)
- for name_component in name_components[:-1]:
- current = current.setdefault(name_component, {})
- current[name_components[-1]] = ''
- return tree
- class PyiFrozenFinder:
- """
- PyInstaller's frozen path entry finder for specific search path.
- Per-path instances allow us to properly translate the given module name ("fullname") into full PYZ entry name.
- For example, with search path being `sys._MEIPASS`, the module "mypackage.mod" would translate to "mypackage.mod"
- in the PYZ archive. However, if search path was `sys._MEIPASS/myotherpackage/_vendored` (for example, if
- `myotherpacakge` added this path to `sys.path`), then "mypackage.mod" would need to translate to
- "myotherpackage._vendored.mypackage.mod" in the PYZ archive.
- """
- def __repr__(self):
- return f"{self.__class__.__name__}({self._path})"
- @classmethod
- def path_hook(cls, path):
- trace(f"PyInstaller: running path finder hook for path: {path!r}")
- try:
- finder = cls(path)
- trace("PyInstaller: hook succeeded")
- return finder
- except Exception as e:
- trace(f"PyInstaller: hook failed: {e}")
- raise
- def __init__(self, path):
- self._path = path # Store original path, as given.
- self._pyz_archive = pyz_archive
- # Compute relative path to the top-level application directory. Do not try to resolve the path itself, because
- # it might contain symbolic links in parts other than the prefix that corresponds to the top-level application
- # directory. See #8994 for an example (files symlinked from a common directory outside of the top-level
- # application directory). Instead, try to compute relative path w.r.t. the original and the resolved top-level
- # application directory.
- for top_level_path in _TOP_LEVEL_DIRECTORY_PATHS:
- try:
- relative_path = os.path.relpath(path, top_level_path)
- except ValueError:
- continue # Failed to compute relative path w.r.t. the given top-level directory path.
- if relative_path.startswith('..'):
- continue # Relative path points outside of the given top-level directory.
- break # Successful match; stop here.
- else:
- raise ImportError("Failed to determine relative path w.r.t. top-level application directory.")
- # Ensure that path does not point to a file on filesystem. Strictly speaking, we should be checking that the
- # given path is a valid directory, but that would need to check both PYZ and filesystem. So for now, limit the
- # check to catch paths pointing to file, because that breaks `runpy.run_path()`, as per #8767.
- if os.path.isfile(path):
- raise ImportError("only directories are supported")
- if relative_path == '.':
- self._pyz_entry_prefix = ''
- else:
- self._pyz_entry_prefix = '.'.join(relative_path.split(os.path.sep))
- def _compute_pyz_entry_name(self, fullname):
- """
- Convert module fullname into PYZ entry name, subject to the prefix implied by this finder's search path.
- """
- tail_module = fullname.rpartition('.')[2]
- if self._pyz_entry_prefix:
- return self._pyz_entry_prefix + "." + tail_module
- else:
- return tail_module
- @property
- def fallback_finder(self):
- """
- Opportunistically create a *fallback finder* using `sys.path_hooks` entries that are located *after* our hook.
- The main goal of this exercise is to obtain an instance of python's FileFinder, but in theory any other hook
- that comes after ours is eligible to be a fallback.
- Having this fallback allows our finder to "cooperate" with python's FileFinder, as if the two were a single
- finder, which allows us to work around the python's PathFinder permitting only one finder instance per path
- without subclassing FileFinder.
- """
- if hasattr(self, '_fallback_finder'):
- return self._fallback_finder
- # Try to instantiate fallback finder
- our_hook_found = False
- self._fallback_finder = None
- for idx, hook in enumerate(sys.path_hooks):
- if hook == self.path_hook:
- our_hook_found = True
- continue # Our hook
- if not our_hook_found:
- continue # Skip hooks before our hook
- try:
- self._fallback_finder = hook(self._path)
- break
- except ImportError:
- pass
- return self._fallback_finder
- def _find_fallback_spec(self, fullname, target):
- """
- Attempt to find the spec using fallback finder, which is opportunistically created here. Typically, this would
- be python's FileFinder, which can discover specs for on-filesystem modules, such as extension modules and
- modules that are collected only as source .py files.
- Having this fallback allows our finder to "cooperate" with python's FileFinder, as if the two were a single
- finder, which allows us to work around the python's PathFinder permitting only one finder instance per path
- without subclassing FileFinder.
- """
- if not hasattr(self, '_fallback_finder'):
- self._fallback_finder = self._get_fallback_finder()
- if self._fallback_finder is None:
- return None
- return self._fallback_finder.find_spec(fullname, target)
- #-- Core PEP451 finder functionality, modeled after importlib.abc.PathEntryFinder
- # https://docs.python.org/3/library/importlib.html#importlib.abc.PathEntryFinder
- def invalidate_caches(self):
- """
- A method which, when called, should invalidate any internal cache used by the finder. Used by
- importlib.invalidate_caches() when invalidating the caches of all finders on sys.meta_path.
- https://docs.python.org/3/library/importlib.html#importlib.abc.MetaPathFinder.invalidate_caches
- """
- # We do not use any caches, but if we have created a fallback finder, propagate the function call.
- # NOTE: use getattr() with _fallback_finder attribute, in order to avoid unnecessary creation of the
- # fallback finder in case when it does not exist yet.
- fallback_finder = getattr(self, '_fallback_finder', None)
- if fallback_finder is not None:
- if hasattr(fallback_finder, 'invalidate_caches'):
- fallback_finder.invalidate_caches()
- def find_spec(self, fullname, target=None):
- """
- A method for finding a spec for the specified module. The finder will search for the module only within the
- path entry to which it is assigned. If a spec cannot be found, None is returned. When passed in, target is a
- module object that the finder may use to make a more educated guess about what spec to return.
- https://docs.python.org/3/library/importlib.html#importlib.abc.PathEntryFinder.find_spec
- """
- trace(f"{self}: find_spec: called with fullname={fullname!r}, target={fullname!r}")
- # Convert fullname to PYZ entry name.
- pyz_entry_name = self._compute_pyz_entry_name(fullname)
- # Try looking up the entry in the PYZ archive
- entry_data = self._pyz_archive.toc.get(pyz_entry_name)
- if entry_data is None:
- # Entry not found - try using fallback finder (for example, python's own FileFinder) to resolve on-disk
- # resources, such as extension modules and modules that are collected only as source .py files.
- trace(f"{self}: find_spec: {fullname!r} not found in PYZ...")
- if self.fallback_finder is not None:
- trace(f"{self}: find_spec: attempting resolve using fallback finder {self.fallback_finder!r}.")
- fallback_spec = self.fallback_finder.find_spec(fullname, target)
- trace(f"{self}: find_spec: fallback finder returned spec: {fallback_spec!r}.")
- return fallback_spec
- else:
- trace(f"{self}: find_spec: fallback finder is not available.")
- return None
- # Entry found
- typecode = entry_data[0]
- trace(f"{self}: find_spec: found {fullname!r} in PYZ as {pyz_entry_name!r}, typecode={typecode}")
- if typecode == pyimod01_archive.PYZ_ITEM_NSPKG:
- # PEP420 namespace package
- # We can use regular list for submodule_search_locations; the caller (i.e., python's PathFinder) takes care
- # of constructing _NamespacePath from it.
- spec = _frozen_importlib.ModuleSpec(fullname, None)
- spec.submodule_search_locations = [
- # NOTE: since we are using sys._MEIPASS as prefix, we need to construct path from resolved PYZ entry
- # name (equivalently, we could combine `self._path` and last part of `fullname`).
- os.path.join(sys._MEIPASS, pyz_entry_name.replace('.', os.path.sep)),
- ]
- return spec
- is_package = typecode == pyimod01_archive.PYZ_ITEM_PKG
- # Instantiate frozen loader for the module
- loader = PyiFrozenLoader(
- name=fullname,
- pyz_archive=self._pyz_archive,
- pyz_entry_name=pyz_entry_name,
- is_package=is_package,
- )
- # Resolve full filename, as if the module/package was located on filesystem. This is done by the loader.
- origin = loader.path
- # Construct spec for module, using all collected information.
- spec = _frozen_importlib.ModuleSpec(
- fullname,
- loader,
- is_package=is_package,
- origin=origin,
- )
- # Make the import machinery set __file__.
- # PEP 451 says: "has_location" is true if the module is locatable. In that case the spec's origin is used
- # as the location and __file__ is set to spec.origin. If additional location information is required
- # (e.g., zipimport), that information may be stored in spec.loader_state.
- spec.has_location = True
- # Set submodule_search_locations for packages. Seems to be required for importlib_resources from 3.2.0;
- # see issue #5395.
- if is_package:
- spec.submodule_search_locations = [os.path.dirname(origin)]
- return spec
- # The following methods are part of legacy PEP302 finder interface. They have been deprecated since python 3.4,
- # and removed in python 3.12. Provide compatibility shims to accommodate code that might still be using them.
- if sys.version_info[:2] < (3, 12):
- def find_loader(self, fullname):
- """
- A legacy method for finding a loader for the specified module. Returns a 2-tuple of (loader, portion) where
- portion is a sequence of file system locations contributing to part of a namespace package. The loader may
- be None while specifying portion to signify the contribution of the file system locations to a namespace
- package. An empty list can be used for portion to signify the loader is not part of a namespace package. If
- loader is None and portion is the empty list then no loader or location for a namespace package were found
- (i.e. failure to find anything for the module).
- Deprecated since python 3.4, removed in 3.12.
- """
- # Based on:
- # https://github.com/python/cpython/blob/v3.11.9/Lib/importlib/_bootstrap_external.py#L1587-L1600
- spec = self.find_spec(fullname)
- if spec is None:
- return None, []
- return spec.loader, spec.submodule_search_locations or []
- def find_module(self, fullname):
- """
- A concrete implementation of Finder.find_module() which is equivalent to self.find_loader(fullname)[0].
- Deprecated since python 3.4, removed in 3.12.
- """
- # Based on:
- # https://github.com/python/cpython/blob/v3.11.9/Lib/importlib/_bootstrap_external.py#L1585
- # https://github.com/python/cpython/blob/v3.11.9/Lib/importlib/_bootstrap_external.py#L622-L639
- #
- loader, portions = self.find_loader(fullname)
- return loader
- # Helper for enforcing module name in PyiFrozenLoader methods.
- def _check_name(method):
- def _check_name_wrapper(self, name, *args, **kwargs):
- if self.name != name:
- raise ImportError(f'loader for {self.name} cannot handle {name}', name=name)
- return method(self, name, *args, **kwargs)
- return _check_name_wrapper
- class PyiFrozenLoader:
- """
- PyInstaller's frozen loader for modules in the PYZ archive, which are discovered by PyiFrozenFinder.
- Since this loader is instantiated only from PyiFrozenFinder and since each loader instance is tied to a specific
- module, the fact that the loader was instantiated serves as the proof that the module exists in the PYZ archive.
- Hence, we can avoid any additional validation in the implementation of the loader's methods.
- """
- def __init__(self, name, pyz_archive, pyz_entry_name, is_package):
- # Store the reference to PYZ archive (for code object retrieval), as well as full PYZ entry name
- # and typecode, all of which are passed from the PyiFrozenFinder.
- self._pyz_archive = pyz_archive
- self._pyz_entry_name = pyz_entry_name
- self._is_package = is_package
- # Compute the module file path, as if module was located on filesystem.
- #
- # Rather than returning path to the .pyc file, return the path to .py file - which might actually exist, if it
- # was explicitly collected into the frozen application). This improves compliance with
- # https://docs.python.org/3/library/importlib.html#importlib.abc.ExecutionLoader.get_filename
- # as well as general compatibility with 3rd party code that blindly assumes that module's file path points to
- # the source .py file.
- #
- # NOTE: since we are using sys._MEIPASS as prefix, we need to construct path from full PYZ entry name
- # (so that a module with `name`=`jaraco.text` and `pyz_entry_name`=`setuptools._vendor.jaraco.text`
- # ends up with path set to `sys._MEIPASS/setuptools/_vendor/jaraco/text/__init__.pyc` instead of
- # `sys._MEIPASS/jaraco/text/__init__.pyc`).
- if is_package:
- module_file = os.path.join(sys._MEIPASS, pyz_entry_name.replace('.', os.path.sep), '__init__.py')
- else:
- module_file = os.path.join(sys._MEIPASS, pyz_entry_name.replace('.', os.path.sep) + '.py')
- # These properties are defined as part of importlib.abc.FileLoader. They are used by our implementation
- # (e.g., module name validation, get_filename(), get_source(), get_resource_reader()), and might also be used
- # by 3rd party code that naively expects to be dealing with a FileLoader instance.
- self.name = name # The name of the module the loader can handle.
- self.path = module_file # Path to the file of the module
- #-- Core PEP451 loader functionality as defined by importlib.abc.Loader
- # https://docs.python.org/3/library/importlib.html#importlib.abc.Loader
- def create_module(self, spec):
- """
- A method that returns the module object to use when importing a module. This method may return None, indicating
- that default module creation semantics should take place.
- https://docs.python.org/3/library/importlib.html#importlib.abc.Loader.create_module
- """
- return None
- def exec_module(self, module):
- """
- A method that executes the module in its own namespace when a module is imported or reloaded. The module
- should already be initialized when exec_module() is called. When this method exists, create_module()
- must be defined.
- https://docs.python.org/3/library/importlib.html#importlib.abc.Loader.exec_module
- """
- spec = module.__spec__
- bytecode = self.get_code(spec.name) # NOTE: get_code verifies that `spec.name` matches `self.name`!
- if bytecode is None:
- raise RuntimeError(f"Failed to retrieve bytecode for {spec.name!r}!")
- # Set by the import machinery
- assert hasattr(module, '__file__')
- # If `submodule_search_locations` is not None, this is a package; set __path__.
- if spec.submodule_search_locations is not None:
- module.__path__ = spec.submodule_search_locations
- exec(bytecode, module.__dict__)
- # The following method is part of legacy PEP302 loader interface. It has been deprecated since python 3.4, and
- # slated for removal in python 3.12, although that has not happened yet. Provide compatibility shim to accommodate
- # code that might still be using it.
- if True:
- @_check_name
- def load_module(self, fullname):
- """
- A legacy method for loading a module. If the module cannot be loaded, ImportError is raised, otherwise the
- loaded module is returned.
- Deprecated since python 3.4, slated for removal in 3.12 (but still present in python's own FileLoader in
- both v3.12.4 and v3.13.0rc1).
- """
- # Based on:
- # https://github.com/python/cpython/blob/v3.11.9/Lib/importlib/_bootstrap_external.py#L942-L945
- import importlib._bootstrap as _bootstrap
- return _bootstrap._load_module_shim(self, fullname)
- #-- PEP302 protocol extensions as defined by importlib.abc.ExecutionLoader
- # https://docs.python.org/3/library/importlib.html#importlib.abc.ExecutionLoader
- @_check_name
- def get_filename(self, fullname):
- """
- A method that is to return the value of __file__ for the specified module. If no path is available, ImportError
- is raised.
- If source code is available, then the method should return the path to the source file, regardless of whether a
- bytecode was used to load the module.
- https://docs.python.org/3/library/importlib.html#importlib.abc.ExecutionLoader.get_filename
- """
- return self.path
- #-- PEP302 protocol extensions as defined by importlib.abc.InspectLoader
- # https://docs.python.org/3/library/importlib.html#importlib.abc.InspectLoader
- @_check_name
- def get_code(self, fullname):
- """
- Return the code object for a module, or None if the module does not have a code object (as would be the case,
- for example, for a built-in module). Raise an ImportError if loader cannot find the requested module.
- https://docs.python.org/3/library/importlib.html#importlib.abc.InspectLoader.get_code
- """
- return self._pyz_archive.extract(self._pyz_entry_name)
- @_check_name
- def get_source(self, fullname):
- """
- A method to return the source of a module. It is returned as a text string using universal newlines, translating
- all recognized line separators into '\n' characters. Returns None if no source is available (e.g. a built-in
- module). Raises ImportError if the loader cannot find the module specified.
- https://docs.python.org/3/library/importlib.html#importlib.abc.InspectLoader.get_source
- """
- # The `path` attribute (which is also returned from `get_filename()`) already points to where the source .py
- # file should exist, if it is available.
- filename = self.path
- try:
- # Read in binary mode, then decode
- with open(filename, 'rb') as fp:
- source_bytes = fp.read()
- return _decode_source(source_bytes)
- except FileNotFoundError:
- pass
- # Source code is unavailable.
- return None
- @_check_name
- def is_package(self, fullname):
- """
- A method to return a true value if the module is a package, a false value otherwise. ImportError is raised if
- the loader cannot find the module.
- https://docs.python.org/3/library/importlib.html#importlib.abc.InspectLoader.is_package
- """
- return self._is_package
- #-- PEP302 protocol extensions as dfined by importlib.abc.ResourceLoader
- # https://docs.python.org/3/library/importlib.html#importlib.abc.ResourceLoader
- def get_data(self, path):
- """
- A method to return the bytes for the data located at path. Loaders that have a file-like storage back-end that
- allows storing arbitrary data can implement this abstract method to give direct access to the data stored.
- OSError is to be raised if the path cannot be found. The path is expected to be constructed using a module’s
- __file__ attribute or an item from a package’s __path__.
- https://docs.python.org/3/library/importlib.html#importlib.abc.ResourceLoader.get_data
- """
- # Try to fetch the data from the filesystem. Since __file__ attribute works properly, just try to open the file
- # and read it.
- with open(path, 'rb') as fp:
- return fp.read()
- #-- Support for `importlib.resources`.
- @_check_name
- def get_resource_reader(self, fullname):
- """
- Return resource reader compatible with `importlib.resources`.
- """
- return PyiFrozenResourceReader(self)
- class PyiFrozenResourceReader:
- """
- Resource reader for importlib.resources / importlib_resources support.
- Supports only on-disk resources, which should cover the typical use cases, i.e., the access to data files;
- PyInstaller collects data files onto filesystem, and as of v6.0.0, the embedded PYZ archive is guaranteed
- to contain only .pyc modules.
- When listing resources, source .py files will not be listed as they are not collected by default. Similarly,
- sub-directories that contained only .py files are not reconstructed on filesystem, so they will not be listed,
- either. If access to .py files is required for whatever reason, they need to be explicitly collected as data files
- anyway, which will place them on filesystem and make them appear as resources.
- For on-disk resources, we *must* return path compatible with pathlib.Path() in order to avoid copy to a temporary
- file, which might break under some circumstances, e.g., metpy with importlib_resources back-port, due to:
- https://github.com/Unidata/MetPy/blob/a3424de66a44bf3a92b0dcacf4dff82ad7b86712/src/metpy/plots/wx_symbols.py#L24-L25
- (importlib_resources tries to use 'fonts/wx_symbols.ttf' as a temporary filename suffix, which fails as it contains
- a separator).
- Furthermore, some packages expect files() to return either pathlib.Path or zipfile.Path, e.g.,
- https://github.com/tensorflow/datasets/blob/master/tensorflow_datasets/core/utils/resource_utils.py#L81-L97
- This makes implementation of mixed support for on-disk and embedded resources using importlib.abc.Traversable
- protocol rather difficult.
- So in order to maximize compatibility with unfrozen behavior, the below implementation is basically equivalent of
- importlib.readers.FileReader from python 3.10:
- https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/readers.py#L11
- and its underlying classes, importlib.abc.TraversableResources and importlib.abc.ResourceReader:
- https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/abc.py#L422
- https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/abc.py#L312
- """
- def __init__(self, loader):
- # Local import to avoid including `pathlib` and its dependencies in `base_library.zip`
- import pathlib
- # This covers both modules and (regular) packages. Note that PEP-420 namespace packages are not handled by this
- # resource reader (since they are not handled by PyiFrozenLoader, which uses this reader).
- self.path = pathlib.Path(loader.path).parent
- def open_resource(self, resource):
- return self.files().joinpath(resource).open('rb')
- def resource_path(self, resource):
- return str(self.path.joinpath(resource))
- def is_resource(self, path):
- return self.files().joinpath(path).is_file()
- def contents(self):
- return (item.name for item in self.files().iterdir())
- def files(self):
- return self.path
- class PyiFrozenEntryPointLoader:
- """
- A special loader that enables retrieval of the code-object for the __main__ module.
- """
- def __repr__(self):
- return self.__class__.__name__
- def get_code(self, fullname):
- if fullname == '__main__':
- # Special handling for __main__ module; the bootloader should store code object to _pyi_main_co
- # attribute of the module.
- return sys.modules['__main__']._pyi_main_co
- raise ImportError(f'{self} cannot handle module {fullname!r}')
- def install():
- """
- Install PyInstaller's frozen finders/loaders/importers into python's import machinery.
- """
- # Setup PYZ archive reader.
- #
- # The bootloader should store the path to PYZ archive (the path to the PKG archive and the offset within it; for
- # executable-embedded archive, this is for example /path/executable_name?117568) into _pyinstaller_pyz
- # attribute of the sys module.
- global pyz_archive
- if not hasattr(sys, '_pyinstaller_pyz'):
- raise RuntimeError("Bootloader did not set sys._pyinstaller_pyz!")
- try:
- pyz_archive = pyimod01_archive.ZlibArchiveReader(sys._pyinstaller_pyz, check_pymagic=True)
- except Exception as e:
- raise RuntimeError("Failed to setup PYZ archive reader!") from e
- delattr(sys, '_pyinstaller_pyz')
- # On Windows, there is finder called `_frozen_importlib.WindowsRegistryFinder`, which looks for Python module
- # locations in Windows registry. The frozen application should not look for those, so remove this finder
- # from `sys.meta_path`.
- for entry in sys.meta_path:
- if getattr(entry, '__name__', None) == 'WindowsRegistryFinder':
- sys.meta_path.remove(entry)
- break
- # Insert our hook for `PyiFrozenFinder` into `sys.path_hooks`. Place it after `zipimporter`, if available.
- for idx, entry in enumerate(sys.path_hooks):
- if getattr(entry, '__name__', None) == 'zipimporter':
- trace(f"PyInstaller: inserting our finder hook at index {idx + 1} in sys.path_hooks.")
- sys.path_hooks.insert(idx + 1, PyiFrozenFinder.path_hook)
- break
- else:
- trace("PyInstaller: zipimporter hook not found in sys.path_hooks! Prepending our finder hook to the list.")
- sys.path_hooks.insert(0, PyiFrozenFinder.path_hook)
- # Monkey-patch `zipimporter.get_source` to allow loading out-of-zip source .py files for modules that are
- # in `base_library.zip`.
- _patch_zipimporter_get_source()
- # Python might have already created a `FileFinder` for `sys._MEIPASS`. Remove the entry from path importer cache,
- # so that next loading attempt creates `PyiFrozenFinder` instead. This could probably be avoided altogether if
- # we refrained from adding `sys._MEIPASS` to `sys.path` until our importer hooks is in place.
- sys.path_importer_cache.pop(sys._MEIPASS, None)
- # Set the PyiFrozenEntryPointLoader as loader for __main__, in order for python to treat __main__ as a module
- # instead of a built-in, and to allow its code object to be retrieved.
- try:
- sys.modules['__main__'].__loader__ = PyiFrozenEntryPointLoader()
- except Exception:
- pass
- # Apply hack for python >= 3.11 and its frozen stdlib modules.
- if sys.version_info >= (3, 11):
- _fixup_frozen_stdlib()
- # A hack for python >= 3.11 and its frozen stdlib modules. Unless `sys._stdlib_dir` is set, these modules end up
- # missing __file__ attribute, which causes problems with 3rd party code. At the time of writing, python interpreter
- # configuration API does not allow us to influence `sys._stdlib_dir` - it always resets it to `None`. Therefore,
- # we manually set the path, and fix __file__ attribute on modules.
- def _fixup_frozen_stdlib():
- import _imp # built-in
- # If sys._stdlib_dir is None or empty, override it with sys._MEIPASS
- if not sys._stdlib_dir:
- try:
- sys._stdlib_dir = sys._MEIPASS
- except AttributeError:
- pass
- # The sys._stdlib_dir set above should affect newly-imported python-frozen modules. However, most of them have
- # been already imported during python initialization and our bootstrap, so we need to retroactively fix their
- # __file__ attribute.
- for module_name, module in sys.modules.items():
- if not _imp.is_frozen(module_name):
- continue
- is_pkg = _imp.is_frozen_package(module_name)
- # Determine "real" name from __spec__.loader_state.
- loader_state = module.__spec__.loader_state
- orig_name = loader_state.origname
- if is_pkg:
- orig_name += '.__init__'
- # We set suffix to .pyc to be consistent with our PyiFrozenLoader.
- filename = os.path.join(sys._MEIPASS, *orig_name.split('.')) + '.pyc'
- # Fixup the __file__ attribute
- if not hasattr(module, '__file__'):
- try:
- module.__file__ = filename
- except AttributeError:
- pass
- # Fixup the loader_state.filename
- # Except for _frozen_importlib (importlib._bootstrap), whose loader_state.filename appears to be left at
- # None in python.
- if loader_state.filename is None and orig_name != 'importlib._bootstrap':
- loader_state.filename = filename
- # Monkey-patch the `get_source` implementation of python's `zipimport.zipimporter` with our custom implementation that
- # looks up for source files in top-level application directory instead of within the zip file. This allows us to collect
- # source .py files for modules that are collected in the `base_library.zip` in the same way as for modules in the PYZ
- # archive.
- def _patch_zipimporter_get_source():
- import zipimport
- _orig_get_source = zipimport.zipimporter.get_source
- def _get_source(self, fullname):
- # Call original implementation first, in case we are dealing with a zip file other than `base_library.zip` (or
- # if the source .py file is actually in there, for whatever reason). This also implicitly validates the module
- # name, as it raises exception if module does not exist and returns None if module exists but the source code
- # is not present in the archive.
- source = _orig_get_source(self, fullname)
- if source is not None:
- return source
- # Our override should apply only to `base_library.zip`.
- if os.path.basename(self.archive) != 'base_library.zip':
- return None
- # Translate module/package name into .py filename in the top-level application directory.
- if self.is_package(fullname):
- filename = os.path.join(*fullname.split('.'), '__init__.py')
- else:
- filename = os.path.join(*fullname.split('.')) + '.py'
- filename = os.path.join(_RESOLVED_TOP_LEVEL_DIRECTORY, filename)
- try:
- # Read in binary mode, then decode
- with open(filename, 'rb') as fp:
- source_bytes = fp.read()
- return _decode_source(source_bytes)
- except FileNotFoundError:
- pass
- # Source code is unavailable.
- return None
- zipimport.zipimporter.get_source = _get_source
|