#-----------------------------------------------------------------------------
|
# Copyright (c) 2021-2023, PyInstaller Development Team.
|
#
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
# you may not use this file except in compliance with the License.
|
#
|
# The full license is in the file COPYING.txt, distributed with this software.
|
#
|
# SPDX-License-Identifier: Apache-2.0
|
#-----------------------------------------------------------------------------
|
#
|
# This rthook overrides pkgutil.iter_modules with custom implementation that uses PyInstaller's FrozenImporter to list
|
# sub-modules embedded in the PYZ archive. The non-embedded modules (binary extensions, or .pyc modules in noarchive
|
# build) are handled by original pkgutil iter_modules implementation (and consequently, python's FileFinder).
|
#
|
# The preferred way of adding support for iter_modules would be adding non-standard iter_modules() method to
|
# FrozenImporter itself. However, that seems to work only for path entry finders (for use with sys.path_hooks), while
|
# PyInstaller's FrozenImporter is registered as meta path finders (for use with sys.meta_path). Turning FrozenImporter
|
# into path entry finder, would seemingly require the latter to support on-filesystem resources (e.g., extension
|
# modules) in addition to PYZ-embedded ones.
|
#
|
# Therefore, we instead opt for overriding pkgutil.iter_modules with custom implementation that augments the output of
|
# original implementation with contents of PYZ archive from FrozenImporter's TOC.
|
|
import os
|
import pkgutil
|
import sys
|
|
from pyimod02_importers import FrozenImporter
|
|
_orig_pkgutil_iter_modules = pkgutil.iter_modules
|
|
|
def _pyi_pkgutil_iter_modules(path=None, prefix=''):
|
# Use original implementation to discover on-filesystem modules (binary extensions in regular builds, or both binary
|
# extensions and compiled pyc modules in noarchive debug builds).
|
yield from _orig_pkgutil_iter_modules(path, prefix)
|
|
# Find the instance of PyInstaller's FrozenImporter.
|
for importer in pkgutil.iter_importers():
|
if isinstance(importer, FrozenImporter):
|
break
|
else:
|
return
|
|
if path is None:
|
# Search for all top-level packages/modules. These will have no dots in their entry names.
|
for entry in importer.toc:
|
if entry.count('.') != 0:
|
continue
|
is_pkg = importer.is_package(entry)
|
yield pkgutil.ModuleInfo(importer, prefix + entry, is_pkg)
|
else:
|
# Declare SYS_PREFIX locally, to avoid clash with eponymous global symbol from pyi_rth_pkgutil hook.
|
#
|
# Use os.path.realpath() to fully resolve any symbolic links in sys._MEIPASS, in order to avoid path mis-matches
|
# when the given search paths also contain symbolic links and are already fully resolved. See #6537 for an
|
# example of such a problem with onefile build on macOS, where the temporary directory is placed under /var,
|
# which is actually a symbolic link to /private/var.
|
SYS_PREFIX = os.path.realpath(sys._MEIPASS) + os.path.sep
|
SYS_PREFIXLEN = len(SYS_PREFIX)
|
|
for pkg_path in path:
|
pkg_path = os.path.realpath(pkg_path) # Fully resolve the given path, in case it contains symbolic links.
|
if not pkg_path.startswith(SYS_PREFIX):
|
# if the path does not start with sys._MEIPASS then it cannot be a bundled package.
|
continue
|
# Construct package prefix from path...
|
pkg_prefix = pkg_path[SYS_PREFIXLEN:]
|
pkg_prefix = pkg_prefix.replace(os.path.sep, '.')
|
# ... and ensure it ends with a dot (so we can directly filter out the package itself).
|
if not pkg_prefix.endswith('.'):
|
pkg_prefix += '.'
|
pkg_prefix_len = len(pkg_prefix)
|
|
for entry in importer.toc:
|
if not entry.startswith(pkg_prefix):
|
continue
|
name = entry[pkg_prefix_len:]
|
if name.count('.') != 0:
|
continue
|
is_pkg = importer.is_package(entry)
|
yield pkgutil.ModuleInfo(importer, prefix + name, is_pkg)
|
|
|
pkgutil.iter_modules = _pyi_pkgutil_iter_modules
|