Remove hardcoded libpython binaries and add debug step
All checks were successful
build / build-linux (push) Successful in 16s
All checks were successful
build / build-linux (push) Successful in 16s
This commit is contained in:
1
venv/lib/python3.12/site-packages/PyInstaller/depend/__init__.py
Executable file
1
venv/lib/python3.12/site-packages/PyInstaller/depend/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
#
|
||||
1025
venv/lib/python3.12/site-packages/PyInstaller/depend/analysis.py
Executable file
1025
venv/lib/python3.12/site-packages/PyInstaller/depend/analysis.py
Executable file
File diff suppressed because it is too large
Load Diff
1131
venv/lib/python3.12/site-packages/PyInstaller/depend/bindepend.py
Executable file
1131
venv/lib/python3.12/site-packages/PyInstaller/depend/bindepend.py
Executable file
File diff suppressed because it is too large
Load Diff
366
venv/lib/python3.12/site-packages/PyInstaller/depend/bytecode.py
Executable file
366
venv/lib/python3.12/site-packages/PyInstaller/depend/bytecode.py
Executable file
@@ -0,0 +1,366 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# Copyright (c) 2021-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)
|
||||
#-----------------------------------------------------------------------------
|
||||
"""
|
||||
Tools for searching bytecode for key statements that indicate the need for additional resources, such as data files
|
||||
and package metadata.
|
||||
|
||||
By *bytecode* I mean the ``code`` object given by ``compile()``, accessible from the ``__code__`` attribute of any
|
||||
non-builtin function or, in PyInstallerLand, the ``PyiModuleGraph.node("some.module").code`` attribute. The best
|
||||
guide for bytecode format I have found is the disassembler reference: https://docs.python.org/3/library/dis.html
|
||||
|
||||
This parser implementation aims to combine the flexibility and speed of regex with the clarity of the output of
|
||||
``dis.dis(code)``. It has not achieved the 2nd, but C'est la vie...
|
||||
|
||||
The biggest clarity killer here is the ``EXTENDED_ARG`` opcode which can appear almost anywhere and therefore needs
|
||||
to be tiptoed around at every step. If this code needs to expand significantly, I would recommend an upgrade to a
|
||||
regex-based grammar parsing library such as Reparse. This way, little steps like unpacking ``EXTENDED_ARGS`` can be
|
||||
defined once then simply referenced forming a nice hierarchy rather than copied everywhere its needed.
|
||||
"""
|
||||
|
||||
import dis
|
||||
import re
|
||||
from types import CodeType
|
||||
from typing import Pattern
|
||||
|
||||
from PyInstaller import compat
|
||||
|
||||
# opcode name -> opcode map
|
||||
# Python 3.11 introduced specialized opcodes that are not covered by opcode.opmap (and equivalent dis.opmap), but dis
|
||||
# has a private map of all opcodes called _all_opmap. So use the latter, if available.
|
||||
opmap = getattr(dis, '_all_opmap', dis.opmap)
|
||||
|
||||
|
||||
def _instruction_to_regex(x: str):
|
||||
"""
|
||||
Get a regex-escaped opcode byte from its human readable name.
|
||||
"""
|
||||
return re.escape(bytes([opmap[x]]))
|
||||
|
||||
|
||||
def bytecode_regex(pattern: bytes, flags=re.VERBOSE | re.DOTALL):
|
||||
"""
|
||||
A regex-powered Python bytecode matcher.
|
||||
|
||||
``bytecode_regex`` provides a very thin wrapper around :func:`re.compile`.
|
||||
|
||||
* Any opcode names wrapped in backticks are substituted for their corresponding opcode bytes.
|
||||
* Patterns are compiled in VERBOSE mode by default so that whitespace and comments may be used.
|
||||
|
||||
This aims to mirror the output of :func:`dis.dis`, which is far more readable than looking at raw byte strings.
|
||||
"""
|
||||
assert isinstance(pattern, bytes)
|
||||
|
||||
# Replace anything wrapped in backticks with regex-escaped opcodes.
|
||||
pattern = re.sub(
|
||||
rb"`(\w+)`",
|
||||
lambda m: _instruction_to_regex(m[1].decode()),
|
||||
pattern,
|
||||
)
|
||||
return re.compile(pattern, flags=flags)
|
||||
|
||||
|
||||
def finditer(pattern: Pattern, string: bytes):
|
||||
"""
|
||||
Call ``pattern.finditer(string)``, but remove any matches beginning on an odd byte (i.e., matches where
|
||||
match.start() is not a multiple of 2).
|
||||
|
||||
This should be used to avoid false positive matches where a bytecode pair's argument is mistaken for an opcode.
|
||||
"""
|
||||
assert isinstance(string, bytes)
|
||||
string = _cleanup_bytecode_string(string)
|
||||
matches = pattern.finditer(string)
|
||||
while True:
|
||||
for match in matches:
|
||||
if match.start() % 2 == 0:
|
||||
# All is good. This match starts on an OPCODE.
|
||||
yield match
|
||||
else:
|
||||
# This match has started on an odd byte, meaning that it is a false positive and should be skipped.
|
||||
# There is a very slim chance that a genuine match overlaps this one and, because re.finditer() does not
|
||||
# allow overlapping matches, it would be lost. To avoid that, restart the regex scan, starting at the
|
||||
# next even byte.
|
||||
matches = pattern.finditer(string, match.start() + 1)
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
# Opcodes involved in function calls with constant arguments. The differences between python versions are handled by
|
||||
# variables below, which are then used to construct the _call_function_bytecode regex.
|
||||
# NOTE1: the _OPCODES_* entries are typically used in (non-capturing) groups that match the opcode plus an arbitrary
|
||||
# argument. But because the entries themselves may contain more than on opcode (with OR operator between them), they
|
||||
# themselves need to be enclosed in another (non-capturing) group. E.g., "(?:(?:_OPCODES_FUNCTION_GLOBAL).)".
|
||||
# NOTE2: _OPCODES_EXTENDED_ARG2 is an exception, as it is used as a list of opcodes to exclude, i.e.,
|
||||
# "[^_OPCODES_EXTENDED_ARG2]". Therefore, multiple opcodes are not separated by the OR operator.
|
||||
if not compat.is_py311:
|
||||
# Python 3.7 introduced two new function-related opcodes, LOAD_METHOD and CALL_METHOD
|
||||
_OPCODES_EXTENDED_ARG = rb"`EXTENDED_ARG`"
|
||||
_OPCODES_EXTENDED_ARG2 = _OPCODES_EXTENDED_ARG
|
||||
_OPCODES_FUNCTION_GLOBAL = rb"`LOAD_NAME`|`LOAD_GLOBAL`|`LOAD_FAST`"
|
||||
_OPCODES_FUNCTION_LOAD = rb"`LOAD_ATTR`|`LOAD_METHOD`"
|
||||
_OPCODES_FUNCTION_ARGS = rb"`LOAD_CONST`"
|
||||
_OPCODES_FUNCTION_CALL = rb"`CALL_FUNCTION`|`CALL_METHOD`|`CALL_FUNCTION_EX`"
|
||||
|
||||
def _cleanup_bytecode_string(bytecode):
|
||||
return bytecode # Nothing to do here
|
||||
elif not compat.is_py312:
|
||||
# Python 3.11 removed CALL_FUNCTION and CALL_METHOD, and replaced them with PRECALL + CALL instruction sequence.
|
||||
# As both PRECALL and CALL have the same parameter (the argument count), we need to match only up to the PRECALL.
|
||||
# The CALL_FUNCTION_EX is still present.
|
||||
# From Python 3.11b1 on, there is an EXTENDED_ARG_QUICK specialization opcode present.
|
||||
_OPCODES_EXTENDED_ARG = rb"`EXTENDED_ARG`|`EXTENDED_ARG_QUICK`"
|
||||
_OPCODES_EXTENDED_ARG2 = rb"`EXTENDED_ARG``EXTENDED_ARG_QUICK`" # Special case; see note above the if/else block!
|
||||
_OPCODES_FUNCTION_GLOBAL = rb"`LOAD_NAME`|`LOAD_GLOBAL`|`LOAD_FAST`"
|
||||
_OPCODES_FUNCTION_LOAD = rb"`LOAD_ATTR`|`LOAD_METHOD`"
|
||||
_OPCODES_FUNCTION_ARGS = rb"`LOAD_CONST`"
|
||||
_OPCODES_FUNCTION_CALL = rb"`PRECALL`|`CALL_FUNCTION_EX`"
|
||||
|
||||
# Starting with python 3.11, the bytecode is peppered with CACHE instructions (which dis module conveniently hides
|
||||
# unless show_caches=True is used). Dealing with these CACHE instructions in regex rules is going to render them
|
||||
# unreadable, so instead we pre-process the bytecode and filter the offending opcodes out.
|
||||
_cache_instruction_filter = bytecode_regex(rb"(`CACHE`.)|(..)")
|
||||
|
||||
def _cleanup_bytecode_string(bytecode):
|
||||
return _cache_instruction_filter.sub(rb"\2", bytecode)
|
||||
else:
|
||||
# Python 3.12 merged EXTENDED_ARG_QUICK back in to EXTENDED_ARG, and LOAD_METHOD in to LOAD_ATTR
|
||||
# PRECALL is no longer a valid key
|
||||
_OPCODES_EXTENDED_ARG = rb"`EXTENDED_ARG`"
|
||||
_OPCODES_EXTENDED_ARG2 = _OPCODES_EXTENDED_ARG
|
||||
if compat.is_py314:
|
||||
# Python 3.14.0a7 added LOAD_FAST_BORROW.
|
||||
_OPCODES_FUNCTION_GLOBAL = rb"`LOAD_NAME`|`LOAD_GLOBAL`|`LOAD_FAST`|`LOAD_FAST_BORROW`"
|
||||
else:
|
||||
_OPCODES_FUNCTION_GLOBAL = rb"`LOAD_NAME`|`LOAD_GLOBAL`|`LOAD_FAST`"
|
||||
_OPCODES_FUNCTION_LOAD = rb"`LOAD_ATTR`"
|
||||
if compat.is_py314:
|
||||
# Python 3.14.0a2 split LOAD_CONST into LOAD_CONST, LOAD_IMMORTAL_CONST, and LOAD_SMALL_INT.
|
||||
# https://github.com/python/cpython/commit/faa3272fb8d63d481a136cc0467a0cba6ed7b264
|
||||
_OPCODES_FUNCTION_ARGS = rb"`LOAD_CONST`|`LOAD_SMALL_INT`|`LOAD_CONST_IMMORTAL`"
|
||||
else:
|
||||
_OPCODES_FUNCTION_ARGS = rb"`LOAD_CONST`"
|
||||
_OPCODES_FUNCTION_CALL = rb"`CALL`|`CALL_FUNCTION_EX`"
|
||||
|
||||
# In Python 3.13, PUSH_NULL opcode is emitted after the LOAD_NAME (and after LOAD_ATTR opcode(s), if applicable).
|
||||
# In python 3.11 and 3.12, it was emitted before the LOAD_NAME, and thus fell outside of our regex matching; now,
|
||||
# we have to deal with it. But, instead of trying to add it to matching rules and adjusting the post-processing
|
||||
# to deal with it, we opt to filter them out (at the same time as we filter out CACHE opcodes), and leave the rest
|
||||
# of processing untouched.
|
||||
if compat.is_py313:
|
||||
_cache_instruction_filter = bytecode_regex(rb"(`CACHE`.)|(`PUSH_NULL`.)|(..)")
|
||||
|
||||
def _cleanup_bytecode_string(bytecode):
|
||||
return _cache_instruction_filter.sub(rb"\3", bytecode)
|
||||
else:
|
||||
_cache_instruction_filter = bytecode_regex(rb"(`CACHE`.)|(..)")
|
||||
|
||||
def _cleanup_bytecode_string(bytecode):
|
||||
return _cache_instruction_filter.sub(rb"\2", bytecode)
|
||||
|
||||
|
||||
# language=PythonVerboseRegExp
|
||||
_call_function_bytecode = bytecode_regex(
|
||||
rb"""
|
||||
# Matches `global_function('some', 'constant', 'arguments')`.
|
||||
|
||||
# Load the global function. In code with >256 of names, this may require extended name references.
|
||||
(
|
||||
(?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*
|
||||
(?:(?:""" + _OPCODES_FUNCTION_GLOBAL + rb""").)
|
||||
)
|
||||
|
||||
# For foo.bar.whizz(), the above is the 'foo', below is the 'bar.whizz' (one opcode per name component, each
|
||||
# possibly preceded by name reference extension).
|
||||
(
|
||||
(?:
|
||||
(?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*
|
||||
(?:""" + _OPCODES_FUNCTION_LOAD + rb""").
|
||||
)*
|
||||
)
|
||||
|
||||
# Load however many arguments it takes. These (for now) must all be constants.
|
||||
# Again, code with >256 constants may need extended enumeration.
|
||||
(
|
||||
(?:
|
||||
(?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*
|
||||
(?:""" + _OPCODES_FUNCTION_ARGS + rb""").
|
||||
)*
|
||||
)
|
||||
|
||||
# Call the function. If opcode is CALL_FUNCTION_EX, the parameter are flags. For other opcodes, the parameter
|
||||
# is the argument count (which may be > 256).
|
||||
(
|
||||
(?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*
|
||||
(?:""" + _OPCODES_FUNCTION_CALL + rb""").
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# language=PythonVerboseRegExp
|
||||
_extended_arg_bytecode = bytecode_regex(
|
||||
rb"""(
|
||||
# Arbitrary number of EXTENDED_ARG pairs.
|
||||
(?:(?:""" + _OPCODES_EXTENDED_ARG + rb""").)*
|
||||
|
||||
# Followed by some other instruction (usually a LOAD).
|
||||
[^""" + _OPCODES_EXTENDED_ARG2 + rb"""].
|
||||
)"""
|
||||
)
|
||||
|
||||
|
||||
def extended_arguments(extended_args: bytes):
|
||||
"""
|
||||
Unpack the (extended) integer used to reference names or constants.
|
||||
|
||||
The input should be a bytecode snippet of the following form::
|
||||
|
||||
EXTENDED_ARG ? # Repeated 0-4 times.
|
||||
LOAD_xxx ? # Any of LOAD_NAME/LOAD_CONST/LOAD_METHOD/...
|
||||
|
||||
Each ? byte combined together gives the number we want.
|
||||
"""
|
||||
return int.from_bytes(extended_args[1::2], "big")
|
||||
|
||||
|
||||
def load(raw: bytes, code: CodeType) -> str:
|
||||
"""
|
||||
Parse an (extended) LOAD_xxx instruction.
|
||||
"""
|
||||
# Get the enumeration.
|
||||
index = extended_arguments(raw)
|
||||
|
||||
# Work out what that enumeration was for (constant/local var/global var).
|
||||
|
||||
# If the last instruction byte is a LOAD_FAST:
|
||||
if raw[-2] == opmap["LOAD_FAST"]:
|
||||
# Then this is a local variable.
|
||||
return code.co_varnames[index]
|
||||
# Or if it is a LOAD_CONST:
|
||||
if raw[-2] == opmap["LOAD_CONST"]:
|
||||
# Then this is a literal.
|
||||
return code.co_consts[index]
|
||||
# Otherwise, it is a global name.
|
||||
if compat.is_py311 and raw[-2] == opmap["LOAD_GLOBAL"]:
|
||||
# In python 3.11, namei>>1 is pushed on stack...
|
||||
return code.co_names[index >> 1]
|
||||
if compat.is_py312 and raw[-2] == opmap["LOAD_ATTR"]:
|
||||
# In python 3.12, namei>>1 is pushed on stack...
|
||||
return code.co_names[index >> 1]
|
||||
if compat.is_py314 and raw[-2] == opmap["LOAD_SMALL_INT"]:
|
||||
# python 3.14 introduced LOAD_SMALL_INT, which pushes its argument (int value < 256) on the stack
|
||||
return index
|
||||
if compat.is_py314 and raw[-2] == opmap["LOAD_CONST_IMMORTAL"]:
|
||||
# python 3.14 introduced LOAD_CONST_IMMORTAL, which pushes co_consts[consti] on the stack. This is intended to
|
||||
# be a variant of LOAD_CONST for constants that are known to be immortal.
|
||||
return code.co_consts[index]
|
||||
if compat.is_py314 and raw[-2] == opmap["LOAD_FAST_BORROW"]:
|
||||
# python 3.14 introduced LOAD_FAST_BORROW, which pushes a borrowed reference to the local co_varnames[var_num]
|
||||
# onto the stack.
|
||||
return code.co_varnames[index]
|
||||
|
||||
return code.co_names[index]
|
||||
|
||||
|
||||
def loads(raw: bytes, code: CodeType) -> list:
|
||||
"""
|
||||
Parse multiple consecutive LOAD_xxx instructions. Or load() in a for loop.
|
||||
|
||||
May be used to unpack a function's parameters or nested attributes ``(foo.bar.pop.whack)``.
|
||||
"""
|
||||
return [load(i, code) for i in _extended_arg_bytecode.findall(raw)]
|
||||
|
||||
|
||||
def function_calls(code: CodeType) -> list:
|
||||
"""
|
||||
Scan a code object for all function calls on constant arguments.
|
||||
"""
|
||||
match: re.Match
|
||||
out = []
|
||||
|
||||
for match in finditer(_call_function_bytecode, code.co_code):
|
||||
function_root, methods, args, function_call = match.groups()
|
||||
|
||||
# For foo():
|
||||
# `function_root` contains 'foo' and `methods` is empty.
|
||||
# For foo.bar.whizz():
|
||||
# `function_root` contains 'foo' and `methods` contains the rest.
|
||||
function_root = load(function_root, code)
|
||||
methods = loads(methods, code)
|
||||
function = ".".join([function_root] + methods)
|
||||
|
||||
args = loads(args, code)
|
||||
if function_call[0] == opmap['CALL_FUNCTION_EX']:
|
||||
flags = extended_arguments(function_call)
|
||||
if flags != 0:
|
||||
# Keyword arguments present. Unhandled at the moment.
|
||||
continue
|
||||
# In calls with const arguments, args contains a single
|
||||
# tuple with all values.
|
||||
if len(args) != 1 or not isinstance(args[0], tuple):
|
||||
continue
|
||||
args = list(args[0])
|
||||
else:
|
||||
arg_count = extended_arguments(function_call)
|
||||
|
||||
if arg_count != len(args):
|
||||
# This happens if there are variable or keyword arguments. Bail out in either case.
|
||||
continue
|
||||
|
||||
out.append((function, args))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def search_recursively(search: callable, code: CodeType, _memo=None) -> dict:
|
||||
"""
|
||||
Apply a search function to a code object, recursing into child code objects (function definitions).
|
||||
"""
|
||||
if _memo is None:
|
||||
_memo = {}
|
||||
if code not in _memo:
|
||||
_memo[code] = search(code)
|
||||
for const in code.co_consts:
|
||||
if isinstance(const, CodeType):
|
||||
search_recursively(search, const, _memo)
|
||||
return _memo
|
||||
|
||||
|
||||
def recursive_function_calls(code: CodeType) -> dict:
|
||||
"""
|
||||
Scan a code object for function calls on constant arguments, recursing into function definitions and bodies of
|
||||
comprehension loops.
|
||||
"""
|
||||
return search_recursively(function_calls, code)
|
||||
|
||||
|
||||
def any_alias(full_name: str):
|
||||
"""List possible aliases of a fully qualified Python name.
|
||||
|
||||
>>> list(any_alias("foo.bar.wizz"))
|
||||
['foo.bar.wizz', 'bar.wizz', 'wizz']
|
||||
|
||||
This crudely allows us to capture uses of wizz() under any of
|
||||
::
|
||||
import foo
|
||||
foo.bar.wizz()
|
||||
::
|
||||
from foo import bar
|
||||
bar.wizz()
|
||||
::
|
||||
from foo.bar import wizz
|
||||
wizz()
|
||||
|
||||
However, it will fail for any form of aliases and quite likely find false matches.
|
||||
"""
|
||||
parts = full_name.split('.')
|
||||
while parts:
|
||||
yield ".".join(parts)
|
||||
parts = parts[1:]
|
||||
378
venv/lib/python3.12/site-packages/PyInstaller/depend/dylib.py
Executable file
378
venv/lib/python3.12/site-packages/PyInstaller/depend/dylib.py
Executable file
@@ -0,0 +1,378 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-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)
|
||||
#-----------------------------------------------------------------------------
|
||||
"""
|
||||
Manipulating with dynamic libraries.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
from PyInstaller import compat
|
||||
import PyInstaller.log as logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Ignoring some system libraries speeds up packaging process
|
||||
_excludes = {
|
||||
# Ignore annoying warnings with Windows system DLLs.
|
||||
#
|
||||
# 'W: library kernel32.dll required via ctypes not found'
|
||||
# 'W: library coredll.dll required via ctypes not found'
|
||||
#
|
||||
# These these dlls has to be ignored for all operating systems because they might be resolved when scanning code for
|
||||
# ctypes dependencies.
|
||||
r'advapi32\.dll',
|
||||
r'ws2_32\.dll',
|
||||
r'gdi32\.dll',
|
||||
r'oleaut32\.dll',
|
||||
r'shell32\.dll',
|
||||
r'ole32\.dll',
|
||||
r'coredll\.dll',
|
||||
r'crypt32\.dll',
|
||||
r'kernel32',
|
||||
r'kernel32\.dll',
|
||||
r'msvcrt\.dll',
|
||||
r'rpcrt4\.dll',
|
||||
r'user32\.dll',
|
||||
# Some modules tries to import the Python library. e.g. pyreadline.console.console
|
||||
r'python\%s\%s',
|
||||
}
|
||||
|
||||
# Regex includes - overrides excludes. Include list is used only to override specific libraries from exclude list.
|
||||
_includes = set()
|
||||
|
||||
_win_includes = {
|
||||
# We need to allow collection of Visual Studio C++ (VC) runtime DLLs from system directories in order to avoid
|
||||
# missing DLL errors when the frozen application is run on a system that does not have the corresponding VC
|
||||
# runtime installed. The VC runtime DLLs may be dependencies of python shared library itself or of extension
|
||||
# modules provided by 3rd party packages.
|
||||
|
||||
# Visual Studio 2010 (VC10) runtime
|
||||
# http://msdn.microsoft.com/en-us/library/8kche8ah(v=vs.100).aspx
|
||||
r'atl100\.dll',
|
||||
r'msvcr100\.dll',
|
||||
r'msvcp100\.dll',
|
||||
r'mfc100\.dll',
|
||||
r'mfc100u\.dll',
|
||||
r'mfcmifc80\.dll',
|
||||
r'mfcm100\.dll',
|
||||
r'mfcm100u\.dll',
|
||||
|
||||
# Visual Studio 2012 (VC11) runtime
|
||||
# https://docs.microsoft.com/en-us/visualstudio/releases/2013/2012-redistribution-vs
|
||||
#
|
||||
# VC110.ATL
|
||||
r'atl110\.dll',
|
||||
# VC110.CRT
|
||||
r'msvcp110\.dll',
|
||||
r'msvcr110\.dll',
|
||||
r'vccorlib110\.dll',
|
||||
# VC110.CXXAMP
|
||||
r'vcamp110\.dll',
|
||||
# VC110.MFC
|
||||
r'mfc110\.dll',
|
||||
r'mfc110u\.dll',
|
||||
r'mfcm110\.dll',
|
||||
r'mfcm110u\.dll',
|
||||
# VC110.MFCLOC
|
||||
r'mfc110chs\.dll',
|
||||
r'mfc110cht\.dll',
|
||||
r'mfc110enu\.dll',
|
||||
r'mfc110esn\.dll',
|
||||
r'mfc110deu\.dll',
|
||||
r'mfc110fra\.dll',
|
||||
r'mfc110ita\.dll',
|
||||
r'mfc110jpn\.dll',
|
||||
r'mfc110kor\.dll',
|
||||
r'mfc110rus\.dll',
|
||||
# VC110.OpenMP
|
||||
r'vcomp110\.dll',
|
||||
# DIA SDK
|
||||
r'msdia110\.dll',
|
||||
|
||||
# Visual Studio 2013 (VC12) runtime
|
||||
# https://docs.microsoft.com/en-us/visualstudio/releases/2013/2013-redistribution-vs
|
||||
#
|
||||
# VC120.CRT
|
||||
r'msvcp120\.dll',
|
||||
r'msvcr120\.dll',
|
||||
r'vccorlib120\.dll',
|
||||
# VC120.CXXAMP
|
||||
r'vcamp120\.dll',
|
||||
# VC120.MFC
|
||||
r'mfc120\.dll',
|
||||
r'mfc120u\.dll',
|
||||
r'mfcm120\.dll',
|
||||
r'mfcm120u\.dll',
|
||||
# VC120.MFCLOC
|
||||
r'mfc120chs\.dll',
|
||||
r'mfc120cht\.dll',
|
||||
r'mfc120deu\.dll',
|
||||
r'mfc120enu\.dll',
|
||||
r'mfc120esn\.dll',
|
||||
r'mfc120fra\.dll',
|
||||
r'mfc120ita\.dll',
|
||||
r'mfc120jpn\.dll',
|
||||
r'mfc120kor\.dll',
|
||||
r'mfc120rus\.dll',
|
||||
# VC120.OPENMP
|
||||
r'vcomp120\.dll',
|
||||
# DIA SDK
|
||||
r'msdia120\.dll',
|
||||
# Cpp REST Windows SDK
|
||||
r'casablanca120.winrt\.dll',
|
||||
# Mobile Services Cpp Client
|
||||
r'zumosdk120.winrt\.dll',
|
||||
# Cpp REST SDK
|
||||
r'casablanca120\.dll',
|
||||
|
||||
# Universal C Runtime Library (since Visual Studio 2015)
|
||||
#
|
||||
# NOTE: these should be put under a switch, as they need not to be bundled if deployment target is Windows 10
|
||||
# and later, as "UCRT is now a system component in Windows 10 and later, managed by Windows Update".
|
||||
# (https://docs.microsoft.com/en-us/cpp/windows/determining-which-dlls-to-redistribute?view=msvc-170)
|
||||
# And as discovered in #6326, Windows prefers system-installed version over the bundled one, anyway
|
||||
# (see https://docs.microsoft.com/en-us/cpp/windows/universal-crt-deployment?view=msvc-170#local-deployment).
|
||||
r'api-ms-win-core.*',
|
||||
r'api-ms-win-crt.*',
|
||||
r'ucrtbase\.dll',
|
||||
|
||||
# Visual Studio 2015/2017/2019/2022 (VC14) runtime
|
||||
# https://docs.microsoft.com/en-us/visualstudio/releases/2022/redistribution
|
||||
#
|
||||
# VC141.CRT/VC142.CRT/VC143.CRT
|
||||
r'concrt140\.dll',
|
||||
r'msvcp140\.dll',
|
||||
r'msvcp140_1\.dll',
|
||||
r'msvcp140_2\.dll',
|
||||
r'msvcp140_atomic_wait\.dll',
|
||||
r'msvcp140_codecvt_ids\.dll',
|
||||
r'vccorlib140\.dll',
|
||||
r'vcruntime140\.dll',
|
||||
r'vcruntime140_1\.dll',
|
||||
# VC141.CXXAMP/VC142.CXXAMP/VC143.CXXAMP
|
||||
r'vcamp140\.dll',
|
||||
# VC141.OpenMP/VC142.OpenMP/VC143.OpenMP
|
||||
r'vcomp140\.dll',
|
||||
# DIA SDK
|
||||
r'msdia140\.dll',
|
||||
|
||||
# Allow pythonNN.dll, pythoncomNN.dll, pywintypesNN.dll
|
||||
r'py(?:thon(?:com(?:loader)?)?|wintypes)\d+\.dll',
|
||||
}
|
||||
|
||||
_win_excludes = {
|
||||
# On Windows, only .dll files can be loaded.
|
||||
r'.*\.so',
|
||||
r'.*\.dylib',
|
||||
|
||||
# MS assembly excludes
|
||||
r'Microsoft\.Windows\.Common-Controls',
|
||||
}
|
||||
|
||||
_unix_excludes = {
|
||||
r'libc\.so(\..*)?',
|
||||
r'libdl\.so(\..*)?',
|
||||
r'libm\.so(\..*)?',
|
||||
r'libpthread\.so(\..*)?',
|
||||
r'librt\.so(\..*)?',
|
||||
r'libthread_db\.so(\..*)?',
|
||||
# glibc regex excludes.
|
||||
r'ld-linux\.so(\..*)?',
|
||||
r'libBrokenLocale\.so(\..*)?',
|
||||
r'libanl\.so(\..*)?',
|
||||
r'libcidn\.so(\..*)?',
|
||||
r'libcrypt\.so(\..*)?',
|
||||
r'libnsl\.so(\..*)?',
|
||||
r'libnss_compat.*\.so(\..*)?',
|
||||
r'libnss_dns.*\.so(\..*)?',
|
||||
r'libnss_files.*\.so(\..*)?',
|
||||
r'libnss_hesiod.*\.so(\..*)?',
|
||||
r'libnss_nis.*\.so(\..*)?',
|
||||
r'libnss_nisplus.*\.so(\..*)?',
|
||||
r'libresolv\.so(\..*)?',
|
||||
r'libutil\.so(\..*)?',
|
||||
# graphical interface libraries come with graphical stack (see libglvnd)
|
||||
r'libE?(Open)?GLX?(ESv1_CM|ESv2)?(dispatch)?\.so(\..*)?',
|
||||
r'libdrm\.so(\..*)?',
|
||||
# a subset of libraries included as part of the Nvidia Linux Graphics Driver as of 520.56.06:
|
||||
# https://download.nvidia.com/XFree86/Linux-x86_64/520.56.06/README/installedcomponents.html
|
||||
r'nvidia_drv\.so',
|
||||
r'libglxserver_nvidia\.so(\..*)?',
|
||||
r'libnvidia-egl-(gbm|wayland)\.so(\..*)?',
|
||||
r'libnvidia-(cfg|compiler|e?glcore|glsi|glvkspirv|rtcore|allocator|tls|ml)\.so(\..*)?',
|
||||
r'lib(EGL|GLX)_nvidia\.so(\..*)?',
|
||||
# libcuda.so, libcuda.so.1, and libcuda.so.{version} are run-time part of NVIDIA driver, and should not be
|
||||
# collected, as they need to match the rest of driver components on the target system.
|
||||
r'libcuda\.so(\..*)?',
|
||||
r'libcudadebugger\.so(\..*)?',
|
||||
# libxcb-dri changes ABI frequently (e.g.: between Ubuntu LTS releases) and is usually installed as dependency of
|
||||
# the graphics stack anyway. No need to bundle it.
|
||||
r'libxcb\.so(\..*)?',
|
||||
r'libxcb-dri.*\.so(\..*)?',
|
||||
# system running a Wayland compositor should already have these libraries
|
||||
# in versions that should not conflict with system drivers, unlike bundled
|
||||
r'libwayland.*\.so(\..*)?',
|
||||
}
|
||||
|
||||
_aix_excludes = {
|
||||
r'libbz2\.a',
|
||||
r'libc\.a',
|
||||
r'libC\.a',
|
||||
r'libcrypt\.a',
|
||||
r'libdl\.a',
|
||||
r'libintl\.a',
|
||||
r'libpthreads\.a',
|
||||
r'librt\\.a',
|
||||
r'librtl\.a',
|
||||
r'libz\.a',
|
||||
}
|
||||
|
||||
_solaris_excludes = {
|
||||
r'libsocket\.so(\..*)?',
|
||||
}
|
||||
|
||||
_cygwin_excludes = {
|
||||
r'cygwin1\.dll',
|
||||
}
|
||||
|
||||
if compat.is_win:
|
||||
_includes |= _win_includes
|
||||
_excludes |= _win_excludes
|
||||
elif compat.is_cygwin:
|
||||
_excludes |= _cygwin_excludes
|
||||
elif compat.is_aix:
|
||||
# The exclude list for AIX differs from other *nix platforms.
|
||||
_excludes |= _aix_excludes
|
||||
elif compat.is_solar:
|
||||
# The exclude list for Solaris differs from other *nix platforms.
|
||||
_excludes |= _solaris_excludes
|
||||
_excludes |= _unix_excludes
|
||||
elif compat.is_unix:
|
||||
# Common excludes for *nix platforms -- except AIX.
|
||||
_excludes |= _unix_excludes
|
||||
|
||||
|
||||
class MatchList:
|
||||
def __init__(self, entries):
|
||||
self._regex = re.compile('|'.join(entries), re.I) if entries else None
|
||||
|
||||
def check_library(self, libname):
|
||||
if self._regex:
|
||||
return self._regex.match(os.path.basename(libname))
|
||||
return False
|
||||
|
||||
|
||||
if compat.is_darwin:
|
||||
import macholib.util
|
||||
|
||||
class MacExcludeList(MatchList):
|
||||
def __init__(self, entries):
|
||||
super().__init__(entries)
|
||||
|
||||
def check_library(self, libname):
|
||||
# Try the global exclude list.
|
||||
result = super().check_library(libname)
|
||||
if result:
|
||||
return result
|
||||
|
||||
# Exclude libraries in standard system locations.
|
||||
return macholib.util.in_system_path(libname)
|
||||
|
||||
exclude_list = MacExcludeList(_excludes)
|
||||
include_list = MatchList(_includes)
|
||||
|
||||
elif compat.is_win:
|
||||
from PyInstaller.utils.win32 import winutils
|
||||
|
||||
class WinExcludeList(MatchList):
|
||||
def __init__(self, entries):
|
||||
super().__init__(entries)
|
||||
|
||||
self._windows_dir = pathlib.Path(winutils.get_windows_dir()).resolve()
|
||||
|
||||
# When running as SYSTEM user, the home directory is `%WINDIR%\system32\config\systemprofile`.
|
||||
self._home_dir = pathlib.Path.home().resolve()
|
||||
self._system_home = self._windows_dir in self._home_dir.parents
|
||||
|
||||
def check_library(self, libname):
|
||||
# Try the global exclude list. The global exclude list contains lower-cased names, so lower-case the input
|
||||
# for case-normalized comparison.
|
||||
result = super().check_library(libname.lower())
|
||||
if result:
|
||||
return result
|
||||
|
||||
# Exclude everything from the Windows directory by default; but allow contents of user's gome directory if
|
||||
# that happens to be rooted under Windows directory (e.g., when running PyInstaller as SYSTEM user).
|
||||
lib_fullpath = pathlib.Path(libname).resolve()
|
||||
exclude = self._windows_dir in lib_fullpath.parents
|
||||
if exclude and self._system_home and self._home_dir in lib_fullpath.parents:
|
||||
exclude = False
|
||||
return exclude
|
||||
|
||||
exclude_list = WinExcludeList(_excludes)
|
||||
include_list = MatchList(_includes)
|
||||
else:
|
||||
exclude_list = MatchList(_excludes)
|
||||
include_list = MatchList(_includes)
|
||||
|
||||
_seen_wine_dlls = set() # Used for warning tracking in include_library()
|
||||
|
||||
|
||||
def include_library(libname):
|
||||
"""
|
||||
Check if the dynamic library should be included with application or not.
|
||||
"""
|
||||
if exclude_list.check_library(libname) and not include_list.check_library(libname):
|
||||
# Library is excluded and is not overridden by include list. It should be excluded.
|
||||
return False
|
||||
|
||||
# If we are running under Wine and the library is a Wine built-in DLL, ensure that it is always excluded. Typically,
|
||||
# excluding a DLL leads to an incomplete bundle and run-time errors when the said DLL is not installed on the target
|
||||
# system. However, having Wine built-in DLLs collected is even more detrimental, as they usually provide Wine's
|
||||
# implementation of low-level functionality, and therefore cannot be used on actual Windows (i.e., system libraries
|
||||
# from the C:\Windows\system32 directory that might end up collected due to ``_win_includes`` list; a prominent
|
||||
# example are VC runtime DLLs, for which Wine provides their own implementation, unless user explicitly installs
|
||||
# Microsoft's VC redistributable package in their Wine environment). Therefore, excluding the Wine built-in DLLs
|
||||
# actually improves the chances of the bundle running on Windows, or at least makes the issue easier to debug by
|
||||
# turning it into the "standard" missing DLL problem. Exclusion should not affect the bundle's ability to run under
|
||||
# Wine itself, as the excluded DLLs are available there.
|
||||
if compat.is_win_wine and compat.is_wine_dll(libname):
|
||||
# Display warning message only once per DLL. Note that it is also displayed only if the DLL were to be included
|
||||
# in the first place.
|
||||
if libname not in _seen_wine_dlls:
|
||||
logger.warning("Excluding Wine built-in DLL: %s", libname)
|
||||
_seen_wine_dlls.add(libname)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Patterns for suppressing warnings about missing dynamically linked libraries
|
||||
_warning_suppressions = []
|
||||
|
||||
# On some systems (e.g., openwrt), libc.so might point to ldd. Suppress warnings about it.
|
||||
if compat.is_linux:
|
||||
_warning_suppressions.append(r'ldd')
|
||||
|
||||
# Suppress warnings about unresolvable UCRT DLLs (see issue #1566) on Windows 10 and 11.
|
||||
if compat.is_win_10 or compat.is_win_11:
|
||||
_warning_suppressions.append(r'api-ms-win-.*\.dll')
|
||||
|
||||
missing_lib_warning_suppression_list = MatchList(_warning_suppressions)
|
||||
|
||||
|
||||
def warn_missing_lib(libname):
|
||||
"""
|
||||
Check if a missing-library warning should be displayed for the given library name (or full path).
|
||||
"""
|
||||
return not missing_lib_warning_suppression_list.check_library(libname)
|
||||
582
venv/lib/python3.12/site-packages/PyInstaller/depend/imphook.py
Executable file
582
venv/lib/python3.12/site-packages/PyInstaller/depend/imphook.py
Executable file
@@ -0,0 +1,582 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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)
|
||||
#-----------------------------------------------------------------------------
|
||||
"""
|
||||
Code related to processing of import hooks.
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os.path
|
||||
import sys
|
||||
import weakref
|
||||
import re
|
||||
|
||||
from PyInstaller import log as logging
|
||||
from PyInstaller.building.utils import format_binaries_and_datas
|
||||
from PyInstaller.compat import importlib_load_source
|
||||
from PyInstaller.depend.imphookapi import PostGraphAPI
|
||||
from PyInstaller.exceptions import ImportErrorWhenRunningHook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModuleHookCache(dict):
|
||||
"""
|
||||
Cache of lazily loadable hook script objects.
|
||||
|
||||
This cache is implemented as a `dict` subclass mapping from the fully-qualified names of all modules with at
|
||||
least one hook script to lists of `ModuleHook` instances encapsulating these scripts. As a `dict` subclass,
|
||||
all cached module names and hook scripts are accessible via standard dictionary operations.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
module_graph : ModuleGraph
|
||||
Current module graph.
|
||||
_hook_module_name_prefix : str
|
||||
String prefixing the names of all in-memory modules lazily loaded from cached hook scripts. See also the
|
||||
`hook_module_name_prefix` parameter passed to the `ModuleHook.__init__()` method.
|
||||
"""
|
||||
|
||||
_cache_id_next = 0
|
||||
"""
|
||||
0-based identifier unique to the next `ModuleHookCache` to be instantiated.
|
||||
|
||||
This identifier is incremented on each instantiation of a new `ModuleHookCache` to isolate in-memory modules of
|
||||
lazily loaded hook scripts in that cache to the same cache-specific namespace, preventing edge-case collisions
|
||||
with existing in-memory modules in other caches.
|
||||
|
||||
"""
|
||||
def __init__(self, module_graph, hook_dirs):
|
||||
"""
|
||||
Cache all hook scripts in the passed directories.
|
||||
|
||||
**Order of caching is significant** with respect to hooks for the same module, as the values of this
|
||||
dictionary are lists. Hooks for the same module will be run in the order in which they are cached. Previously
|
||||
cached hooks are always preserved rather than overridden.
|
||||
|
||||
By default, official hooks are cached _before_ user-defined hooks. For modules with both official and
|
||||
user-defined hooks, this implies that the former take priority over and hence will be loaded _before_ the
|
||||
latter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module_graph : ModuleGraph
|
||||
Current module graph.
|
||||
hook_dirs : list
|
||||
List of the absolute or relative paths of all directories containing **hook scripts** (i.e.,
|
||||
Python scripts with filenames matching `hook-{module_name}.py`, where `{module_name}` is the module
|
||||
hooked by that script) to be cached.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# To avoid circular references and hence increased memory consumption, a weak rather than strong reference is
|
||||
# stored to the passed graph. Since this graph is guaranteed to live longer than this cache,
|
||||
# this is guaranteed to be safe.
|
||||
self.module_graph = weakref.proxy(module_graph)
|
||||
|
||||
# String unique to this cache prefixing the names of all in-memory modules lazily loaded from cached hook
|
||||
# scripts, privatized for safety.
|
||||
self._hook_module_name_prefix = '__PyInstaller_hooks_{}_'.format(ModuleHookCache._cache_id_next)
|
||||
ModuleHookCache._cache_id_next += 1
|
||||
|
||||
# Cache all hook scripts in the passed directories.
|
||||
self._cache_hook_dirs(hook_dirs)
|
||||
|
||||
def _cache_hook_dirs(self, hook_dirs):
|
||||
"""
|
||||
Cache all hook scripts in the passed directories.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hook_dirs : list
|
||||
List of the absolute or relative paths of all directories containing hook scripts to be cached.
|
||||
"""
|
||||
|
||||
for hook_dir, default_priority in hook_dirs:
|
||||
# Canonicalize this directory's path and validate its existence.
|
||||
hook_dir = os.path.abspath(hook_dir)
|
||||
if not os.path.isdir(hook_dir):
|
||||
raise FileNotFoundError('Hook directory "{}" not found.'.format(hook_dir))
|
||||
|
||||
# For each hook script in this directory...
|
||||
hook_filenames = glob.glob(os.path.join(hook_dir, 'hook-*.py'))
|
||||
for hook_filename in hook_filenames:
|
||||
# Fully-qualified name of this hook's corresponding module, constructed by removing the "hook-" prefix
|
||||
# and ".py" suffix.
|
||||
module_name = os.path.basename(hook_filename)[5:-3]
|
||||
|
||||
# Lazily loadable hook object.
|
||||
module_hook = ModuleHook(
|
||||
module_graph=self.module_graph,
|
||||
module_name=module_name,
|
||||
hook_filename=hook_filename,
|
||||
hook_module_name_prefix=self._hook_module_name_prefix,
|
||||
default_priority=default_priority,
|
||||
)
|
||||
|
||||
# Add this hook to this module's list of hooks.
|
||||
module_hooks = self.setdefault(module_name, [])
|
||||
module_hooks.append(module_hook)
|
||||
|
||||
# Post-processing: we allow only one instance of hook per module. Currently, the priority order is defined
|
||||
# implicitly, via order of hook directories, so the first hook in the list has the highest priority.
|
||||
for module_name in self.keys():
|
||||
hooks = self[module_name]
|
||||
if len(hooks) == 1:
|
||||
self[module_name] = hooks[0]
|
||||
else:
|
||||
# Order by priority value, in descending order.
|
||||
sorted_hooks = sorted(hooks, key=lambda hook: hook.priority, reverse=True)
|
||||
self[module_name] = sorted_hooks[0]
|
||||
|
||||
def remove_modules(self, *module_names):
|
||||
"""
|
||||
Remove the passed modules and all hook scripts cached for these modules from this cache.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module_names : list
|
||||
List of all fully-qualified module names to be removed.
|
||||
"""
|
||||
|
||||
for module_name in module_names:
|
||||
# Unload this module's hook script modules from memory. Since these are top-level pure-Python modules cached
|
||||
# only in the "sys.modules" dictionary, popping these modules from this dictionary suffices to garbage
|
||||
# collect them.
|
||||
module_hook = self.pop(module_name, None) # Remove our reference, if available.
|
||||
if module_hook is not None:
|
||||
sys.modules.pop(module_hook.hook_module_name, None)
|
||||
|
||||
|
||||
def _module_collection_mode_sanitizer(value):
|
||||
if isinstance(value, dict):
|
||||
# Hook set a dictionary; use it as-is
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
# Hook set a mode string; convert to a dictionary and assign the string to `None` (= the hooked module).
|
||||
return {None: value}
|
||||
|
||||
raise ValueError(f"Invalid module collection mode setting value: {value!r}")
|
||||
|
||||
|
||||
def _bindepend_symlink_suppression_sanitizer(value):
|
||||
if isinstance(value, (list, set)):
|
||||
# Hook set a list or a set; use it as-is
|
||||
return set(value)
|
||||
elif isinstance(value, str):
|
||||
# Hook set a string; create a set with single element.
|
||||
return set([value])
|
||||
|
||||
raise ValueError(f"Invalid value for bindepend_symlink_suppression: {value!r}")
|
||||
|
||||
|
||||
# Dictionary mapping the names of magic attributes required by the "ModuleHook" class to 2-tuples "(default_type,
|
||||
# sanitizer_func)", where:
|
||||
#
|
||||
# * "default_type" is the type to which that attribute will be initialized when that hook is lazily loaded.
|
||||
# * "sanitizer_func" is the callable sanitizing the original value of that attribute defined by that hook into a
|
||||
# safer value consumable by "ModuleHook" callers if any or "None" if the original value requires no sanitization.
|
||||
#
|
||||
# To avoid subtleties in the ModuleHook.__getattr__() method, this dictionary is declared as a module rather than a
|
||||
# class attribute. If declared as a class attribute and then undefined (...for whatever reason), attempting to access
|
||||
# this attribute from that method would produce infinite recursion.
|
||||
_MAGIC_MODULE_HOOK_ATTRS = {
|
||||
# Collections in which order is insignificant. This includes:
|
||||
#
|
||||
# * "datas", sanitized from hook-style 2-tuple lists defined by hooks into TOC-style 2-tuple sets consumable by
|
||||
# "ModuleHook" callers.
|
||||
# * "binaries", sanitized in the same way.
|
||||
'datas': (set, format_binaries_and_datas),
|
||||
'binaries': (set, format_binaries_and_datas),
|
||||
'excludedimports': (set, None),
|
||||
|
||||
# Collections in which order is significant. This includes:
|
||||
#
|
||||
# * "hiddenimports", as order of importation is significant. On module importation, hook scripts are loaded and hook
|
||||
# functions declared by these scripts are called. As these scripts and functions can have side effects dependent
|
||||
# on module importation order, module importation itself can have side effects dependent on this order!
|
||||
'hiddenimports': (list, None),
|
||||
|
||||
# Flags
|
||||
'warn_on_missing_hiddenimports': (lambda: True, bool),
|
||||
|
||||
# Package/module collection mode dictionary.
|
||||
'module_collection_mode': (dict, _module_collection_mode_sanitizer),
|
||||
|
||||
# Path patterns for suppression of symbolic links created by binary dependency analysis.
|
||||
'bindepend_symlink_suppression': (set, _bindepend_symlink_suppression_sanitizer),
|
||||
}
|
||||
|
||||
|
||||
class ModuleHook:
|
||||
"""
|
||||
Cached object encapsulating a lazy loadable hook script.
|
||||
|
||||
This object exposes public attributes (e.g., `datas`) of the underlying hook script as attributes of the same
|
||||
name of this object. On the first access of any such attribute, this hook script is lazily loaded into an
|
||||
in-memory private module reused on subsequent accesses. These dynamic attributes are referred to as "magic." All
|
||||
other static attributes of this object (e.g., `hook_module_name`) are referred to as "non-magic."
|
||||
|
||||
Attributes (Magic)
|
||||
----------
|
||||
datas : set
|
||||
Set of `TOC`-style 2-tuples `(target_file, source_file)` for all external non-executable files required by
|
||||
the module being hooked, converted from the `datas` list of hook-style 2-tuples `(source_dir_or_glob,
|
||||
target_dir)` defined by this hook script.
|
||||
binaries : set
|
||||
Set of `TOC`-style 2-tuples `(target_file, source_file)` for all external executable files required by the
|
||||
module being hooked, converted from the `binaries` list of hook-style 2-tuples `(source_dir_or_glob,
|
||||
target_dir)` defined by this hook script.
|
||||
excludedimports : set
|
||||
Set of the fully-qualified names of all modules imported by the module being hooked to be ignored rather than
|
||||
imported from that module, converted from the `excludedimports` list defined by this hook script. These
|
||||
modules will only be "locally" rather than "globally" ignored. These modules will remain importable from all
|
||||
modules other than the module being hooked.
|
||||
hiddenimports : set
|
||||
Set of the fully-qualified names of all modules imported by the module being hooked that are _not_
|
||||
automatically detectable by PyInstaller (usually due to being dynamically imported in that module),
|
||||
converted from the `hiddenimports` list defined by this hook script.
|
||||
warn_on_missing_hiddenimports : bool
|
||||
Boolean flag indicating whether missing hidden imports from the hook should generate warnings or not. This
|
||||
behavior is enabled by default, but individual hooks can opt out of it.
|
||||
module_collection_mode : dict
|
||||
A dictionary of package/module names and their corresponding collection mode strings ('pyz', 'pyc', 'py',
|
||||
'pyz+py', 'py+pyz').
|
||||
bindepend_symlink_suppression : set
|
||||
A set of paths or path patterns corresponding to shared libraries for which binary dependency analysis should
|
||||
not create symbolic links into top-level application directory.
|
||||
|
||||
Attributes (Non-magic)
|
||||
----------
|
||||
module_graph : ModuleGraph
|
||||
Current module graph.
|
||||
module_name : str
|
||||
Name of the module hooked by this hook script.
|
||||
hook_filename : str
|
||||
Absolute or relative path of this hook script.
|
||||
hook_module_name : str
|
||||
Name of the in-memory module of this hook script's interpreted contents.
|
||||
_hook_module : module
|
||||
In-memory module of this hook script's interpreted contents, lazily loaded on the first call to the
|
||||
`_load_hook_module()` method _or_ `None` if this method has yet to be accessed.
|
||||
_default_priority : int
|
||||
Default (location-based) priority for this hook.
|
||||
priority : int
|
||||
Actual priority for this hook. Might be different from `_default_priority` if hook file specifies the hook
|
||||
priority override.
|
||||
"""
|
||||
|
||||
#-- Magic --
|
||||
|
||||
def __init__(self, module_graph, module_name, hook_filename, hook_module_name_prefix, default_priority):
|
||||
"""
|
||||
Initialize this metadata.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module_graph : ModuleGraph
|
||||
Current module graph.
|
||||
module_name : str
|
||||
Name of the module hooked by this hook script.
|
||||
hook_filename : str
|
||||
Absolute or relative path of this hook script.
|
||||
hook_module_name_prefix : str
|
||||
String prefixing the name of the in-memory module for this hook script. To avoid namespace clashes with
|
||||
similar modules created by other `ModuleHook` objects in other `ModuleHookCache` containers, this string
|
||||
_must_ be unique to the `ModuleHookCache` container containing this `ModuleHook` object. If this string
|
||||
is non-unique, an existing in-memory module will be erroneously reused when lazily loading this hook
|
||||
script, thus erroneously resanitizing previously sanitized hook script attributes (e.g., `datas`) with
|
||||
the `format_binaries_and_datas()` helper.
|
||||
default_priority : int
|
||||
Default, location-based priority for this hook. Used to select active hook when multiple hooks are defined
|
||||
for the same module.
|
||||
"""
|
||||
# Note that the passed module graph is already a weak reference, avoiding circular reference issues. See
|
||||
# ModuleHookCache.__init__(). TODO: Add a failure message
|
||||
assert isinstance(module_graph, weakref.ProxyTypes)
|
||||
self.module_graph = module_graph
|
||||
self.module_name = module_name
|
||||
self.hook_filename = hook_filename
|
||||
|
||||
# Default priority; used as fall-back for dynamic `hook_priority` attribute.
|
||||
self._default_priority = default_priority
|
||||
|
||||
# Name of the in-memory module fabricated to refer to this hook script.
|
||||
self.hook_module_name = hook_module_name_prefix + self.module_name.replace('.', '_')
|
||||
|
||||
# Attributes subsequently defined by the _load_hook_module() method.
|
||||
self._loaded = False
|
||||
self._has_hook_function = False
|
||||
self._hook_module = None
|
||||
|
||||
def __getattr__(self, attr_name):
|
||||
"""
|
||||
Get the magic attribute with the passed name (e.g., `datas`) from this lazily loaded hook script if any _or_
|
||||
raise `AttributeError` otherwise.
|
||||
|
||||
This special method is called only for attributes _not_ already defined by this object. This includes
|
||||
undefined attributes and the first attempt to access magic attributes.
|
||||
|
||||
This special method is _not_ called for subsequent attempts to access magic attributes. The first attempt to
|
||||
access magic attributes defines corresponding instance variables accessible via the `self.__dict__` instance
|
||||
dictionary (e.g., as `self.datas`) without calling this method. This approach also allows magic attributes to
|
||||
be deleted from this object _without_ defining the `__delattr__()` special method.
|
||||
|
||||
See Also
|
||||
----------
|
||||
Class docstring for supported magic attributes.
|
||||
"""
|
||||
|
||||
if attr_name == 'priority':
|
||||
# If attribute is part of hook metadata, read metadata from hook script and return the attribute value.
|
||||
self._load_hook_metadata()
|
||||
return getattr(self, attr_name)
|
||||
if attr_name in _MAGIC_MODULE_HOOK_ATTRS and not self._loaded:
|
||||
# If attribute is hook's magic attribute, load and run the hook script, and return the attribute value.
|
||||
self._load_hook_module()
|
||||
return getattr(self, attr_name)
|
||||
else:
|
||||
# This is an undefined attribute. Raise an exception.
|
||||
raise AttributeError(attr_name)
|
||||
|
||||
def __setattr__(self, attr_name, attr_value):
|
||||
"""
|
||||
Set the attribute with the passed name to the passed value.
|
||||
|
||||
If this is a magic attribute, this hook script will be lazily loaded before setting this attribute. Unlike
|
||||
`__getattr__()`, this special method is called to set _any_ attribute -- including magic, non-magic,
|
||||
and undefined attributes.
|
||||
|
||||
See Also
|
||||
----------
|
||||
Class docstring for supported magic attributes.
|
||||
"""
|
||||
|
||||
# If this is a magic attribute, initialize this attribute by lazy loading this hook script before overwriting
|
||||
# this attribute.
|
||||
if attr_name in _MAGIC_MODULE_HOOK_ATTRS:
|
||||
self._load_hook_module()
|
||||
|
||||
# Set this attribute to the passed value. To avoid recursion, the superclass method rather than setattr() is
|
||||
# called.
|
||||
return super().__setattr__(attr_name, attr_value)
|
||||
|
||||
#-- Loading --
|
||||
|
||||
def _load_hook_metadata(self):
|
||||
"""
|
||||
Load hook metadata from its source file.
|
||||
"""
|
||||
self.priority = self._default_priority
|
||||
|
||||
# Priority override pattern: `# $PyInstaller-Hook-Priority: <value>`
|
||||
priority_pattern = re.compile(r"^\s*#\s*\$PyInstaller-Hook-Priority:\s*(?P<value>[\S]+)")
|
||||
|
||||
with open(self.hook_filename, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
# Attempt to match and parse hook priority directive
|
||||
m = priority_pattern.match(line)
|
||||
if m is not None:
|
||||
try:
|
||||
self.priority = int(m.group('value'))
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to parse hook priority value string: %r!", m.group('value'), exc_info=True
|
||||
)
|
||||
# Currently, this is our only line of interest, so we can stop the search here.
|
||||
return
|
||||
|
||||
def _load_hook_module(self, keep_module_ref=False):
|
||||
"""
|
||||
Lazily load this hook script into an in-memory private module.
|
||||
|
||||
This method (and, indeed, this class) preserves all attributes and functions defined by this hook script as
|
||||
is, ensuring sane behaviour in hook functions _not_ expecting unplanned external modification. Instead,
|
||||
this method copies public attributes defined by this hook script (e.g., `binaries`) into private attributes
|
||||
of this object, which the special `__getattr__()` and `__setattr__()` methods safely expose to external
|
||||
callers. For public attributes _not_ defined by this hook script, the corresponding private attributes will
|
||||
be assigned sane defaults. For some public attributes defined by this hook script, the corresponding private
|
||||
attributes will be transformed into objects more readily and safely consumed elsewhere by external callers.
|
||||
|
||||
See Also
|
||||
----------
|
||||
Class docstring for supported attributes.
|
||||
"""
|
||||
|
||||
# If this hook script module has already been loaded, noop.
|
||||
if self._loaded and (self._hook_module is not None or not keep_module_ref):
|
||||
return
|
||||
|
||||
# Load and execute the hook script. Even if mechanisms from the import machinery are used, this does not import
|
||||
# the hook as the module.
|
||||
hook_path, hook_basename = os.path.split(self.hook_filename)
|
||||
logger.info('Processing standard module hook %r from %r', hook_basename, hook_path)
|
||||
try:
|
||||
self._hook_module = importlib_load_source(self.hook_module_name, self.hook_filename)
|
||||
except ImportError:
|
||||
logger.debug("Hook failed with:", exc_info=True)
|
||||
raise ImportErrorWhenRunningHook(self.hook_module_name, self.hook_filename)
|
||||
|
||||
# Mark as loaded
|
||||
self._loaded = True
|
||||
|
||||
# Check if module has hook() function.
|
||||
self._has_hook_function = hasattr(self._hook_module, 'hook')
|
||||
|
||||
# Copy hook script attributes into magic attributes exposed as instance variables of the current "ModuleHook"
|
||||
# instance.
|
||||
for attr_name, (default_type, sanitizer_func) in _MAGIC_MODULE_HOOK_ATTRS.items():
|
||||
# Unsanitized value of this attribute.
|
||||
attr_value = getattr(self._hook_module, attr_name, None)
|
||||
|
||||
# If this attribute is undefined, expose a sane default instead.
|
||||
if attr_value is None:
|
||||
attr_value = default_type()
|
||||
# Else if this attribute requires sanitization, do so.
|
||||
elif sanitizer_func is not None:
|
||||
attr_value = sanitizer_func(attr_value)
|
||||
# Else, expose the unsanitized value of this attribute.
|
||||
|
||||
# Expose this attribute as an instance variable of the same name.
|
||||
setattr(self, attr_name, attr_value)
|
||||
|
||||
# If module_collection_mode has an entry with None key, reassign it to the hooked module's name.
|
||||
setattr(
|
||||
self, 'module_collection_mode', {
|
||||
key if key is not None else self.module_name: value
|
||||
for key, value in getattr(self, 'module_collection_mode').items()
|
||||
}
|
||||
)
|
||||
|
||||
# Release the module if we do not need the reference. This is the case when hook is loaded during the analysis
|
||||
# rather as part of the post-graph operations.
|
||||
if not keep_module_ref:
|
||||
self._hook_module = None
|
||||
|
||||
#-- Hooks --
|
||||
|
||||
def post_graph(self, analysis):
|
||||
"""
|
||||
Call the **post-graph hook** (i.e., `hook()` function) defined by this hook script, if any.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
analysis: build_main.Analysis
|
||||
Analysis that calls the hook
|
||||
|
||||
This method is intended to be called _after_ the module graph for this application is constructed.
|
||||
"""
|
||||
|
||||
# Lazily load this hook script into an in-memory module.
|
||||
# The script might have been loaded before during modulegraph analysis; in that case, it needs to be reloaded
|
||||
# only if it provides a hook() function.
|
||||
if not self._loaded or self._has_hook_function:
|
||||
# Keep module reference when loading the hook, so we can call its hook function!
|
||||
self._load_hook_module(keep_module_ref=True)
|
||||
|
||||
# Call this hook script's hook() function, which modifies attributes accessed by subsequent methods and
|
||||
# hence must be called first.
|
||||
self._process_hook_func(analysis)
|
||||
|
||||
# Order is insignificant here.
|
||||
self._process_hidden_imports()
|
||||
|
||||
def _process_hook_func(self, analysis):
|
||||
"""
|
||||
Call this hook's `hook()` function if defined.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
analysis: build_main.Analysis
|
||||
Analysis that calls the hook
|
||||
"""
|
||||
|
||||
# If this hook script defines no hook() function, noop.
|
||||
if not hasattr(self._hook_module, 'hook'):
|
||||
return
|
||||
|
||||
# Call this hook() function.
|
||||
hook_api = PostGraphAPI(module_name=self.module_name, module_graph=self.module_graph, analysis=analysis)
|
||||
try:
|
||||
self._hook_module.hook(hook_api)
|
||||
except ImportError:
|
||||
logger.debug("Hook failed with:", exc_info=True)
|
||||
raise ImportErrorWhenRunningHook(self.hook_module_name, self.hook_filename)
|
||||
|
||||
# Update all magic attributes modified by the prior call.
|
||||
self.datas.update(set(hook_api._added_datas))
|
||||
self.binaries.update(set(hook_api._added_binaries))
|
||||
self.hiddenimports.extend(hook_api._added_imports)
|
||||
self.module_collection_mode.update(hook_api._module_collection_mode)
|
||||
self.bindepend_symlink_suppression.update(hook_api._bindepend_symlink_suppression)
|
||||
|
||||
# FIXME: `hook_api._deleted_imports` should be appended to `self.excludedimports` and used to suppress module
|
||||
# import during the modulegraph construction rather than handled here. However, for that to work, the `hook()`
|
||||
# function needs to be ran during modulegraph construction instead of in post-processing (and this in turn
|
||||
# requires additional code refactoring in order to be able to pass `analysis` to `PostGraphAPI` object at
|
||||
# that point). So once the modulegraph rewrite is complete, remove the code block below.
|
||||
for deleted_module_name in hook_api._deleted_imports:
|
||||
# Remove the graph link between the hooked module and item. This removes the 'item' node from the graph if
|
||||
# no other links go to it (no other modules import it)
|
||||
self.module_graph.removeReference(hook_api.node, deleted_module_name)
|
||||
|
||||
def _process_hidden_imports(self):
|
||||
"""
|
||||
Add all imports listed in this hook script's `hiddenimports` attribute to the module graph as if directly
|
||||
imported by this hooked module.
|
||||
|
||||
These imports are typically _not_ implicitly detectable by PyInstaller and hence must be explicitly defined
|
||||
by hook scripts.
|
||||
"""
|
||||
|
||||
# For each hidden import required by the module being hooked...
|
||||
for import_module_name in self.hiddenimports:
|
||||
try:
|
||||
# Graph node for this module. Do not implicitly create namespace packages for non-existent packages.
|
||||
caller = self.module_graph.find_node(self.module_name, create_nspkg=False)
|
||||
|
||||
# Manually import this hidden import from this module.
|
||||
self.module_graph.import_hook(import_module_name, caller)
|
||||
# If this hidden import is unimportable, print a non-fatal warning. Hidden imports often become
|
||||
# desynchronized from upstream packages and hence are only "soft" recommendations.
|
||||
except ImportError:
|
||||
if self.warn_on_missing_hiddenimports:
|
||||
logger.warning('Hidden import "%s" not found!', import_module_name)
|
||||
|
||||
|
||||
class AdditionalFilesCache:
|
||||
"""
|
||||
Cache for storing what binaries and datas were pushed by what modules when import hooks were processed.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._binaries = {}
|
||||
self._datas = {}
|
||||
|
||||
def add(self, modname, binaries, datas):
|
||||
|
||||
self._binaries.setdefault(modname, [])
|
||||
self._binaries[modname].extend(binaries or [])
|
||||
self._datas.setdefault(modname, [])
|
||||
self._datas[modname].extend(datas or [])
|
||||
|
||||
def __contains__(self, name):
|
||||
return name in self._binaries or name in self._datas
|
||||
|
||||
def binaries(self, modname):
|
||||
"""
|
||||
Return list of binaries for given module name.
|
||||
"""
|
||||
return self._binaries.get(modname, [])
|
||||
|
||||
def datas(self, modname):
|
||||
"""
|
||||
Return list of datas for given module name.
|
||||
"""
|
||||
return self._datas.get(modname, [])
|
||||
486
venv/lib/python3.12/site-packages/PyInstaller/depend/imphookapi.py
Executable file
486
venv/lib/python3.12/site-packages/PyInstaller/depend/imphookapi.py
Executable file
@@ -0,0 +1,486 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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)
|
||||
#-----------------------------------------------------------------------------
|
||||
"""
|
||||
Classes facilitating communication between PyInstaller and import hooks.
|
||||
|
||||
PyInstaller passes instances of classes defined by this module to corresponding functions defined by external import
|
||||
hooks, which commonly modify the contents of these instances before returning. PyInstaller then detects and converts
|
||||
these modifications into appropriate operations on the current `PyiModuleGraph` instance, thus modifying which
|
||||
modules will be frozen into the executable.
|
||||
"""
|
||||
|
||||
from PyInstaller.building.utils import format_binaries_and_datas
|
||||
from PyInstaller.lib.modulegraph.modulegraph import (RuntimeModule, RuntimePackage)
|
||||
|
||||
|
||||
class PreSafeImportModuleAPI:
|
||||
"""
|
||||
Metadata communicating changes made by the current **pre-safe import module hook** (i.e., hook run immediately
|
||||
_before_ a call to `ModuleGraph._safe_import_module()` recursively adding the hooked module, package,
|
||||
or C extension and all transitive imports thereof to the module graph) back to PyInstaller.
|
||||
|
||||
Pre-safe import module hooks _must_ define a `pre_safe_import_module()` function accepting an instance of this
|
||||
class, whose attributes describe the subsequent `ModuleGraph._safe_import_module()` call creating the hooked
|
||||
module's graph node.
|
||||
|
||||
Each pre-safe import module hook is run _only_ on the first attempt to create the hooked module's graph node and
|
||||
then subsequently ignored. If this hook successfully creates that graph node, the subsequent
|
||||
`ModuleGraph._safe_import_module()` call will observe this fact and silently return without attempting to
|
||||
recreate that graph node.
|
||||
|
||||
Pre-safe import module hooks are typically used to create graph nodes for **runtime modules** (i.e.,
|
||||
modules dynamically defined at runtime). Most modules are physically defined in external `.py`-suffixed scripts.
|
||||
Some modules, however, are dynamically defined at runtime (e.g., `six.moves`, dynamically defined by the
|
||||
physically defined `six.py` module). However, `ModuleGraph` only parses `import` statements residing in external
|
||||
scripts. `ModuleGraph` is _not_ a full-fledged, Turing-complete Python interpreter and hence has no means of
|
||||
parsing `import` statements performed by runtime modules existing only in-memory.
|
||||
|
||||
'With great power comes great responsibility.'
|
||||
|
||||
|
||||
Attributes (Immutable)
|
||||
----------------------------
|
||||
The following attributes are **immutable** (i.e., read-only). For safety, any attempts to change these attributes
|
||||
_will_ result in a raised exception:
|
||||
|
||||
module_graph : PyiModuleGraph
|
||||
Current module graph.
|
||||
parent_package : Package
|
||||
Graph node for the package providing this module _or_ `None` if this module is a top-level module.
|
||||
|
||||
Attributes (Mutable)
|
||||
-----------------------------
|
||||
The following attributes are editable.
|
||||
|
||||
module_basename : str
|
||||
Unqualified name of the module to be imported (e.g., `text`).
|
||||
module_name : str
|
||||
Fully-qualified name of this module (e.g., `email.mime.text`).
|
||||
"""
|
||||
def __init__(self, module_graph, module_basename, module_name, parent_package):
|
||||
self._module_graph = module_graph
|
||||
self.module_basename = module_basename
|
||||
self.module_name = module_name
|
||||
self._parent_package = parent_package
|
||||
|
||||
# Immutable properties. No corresponding setters are defined.
|
||||
@property
|
||||
def module_graph(self):
|
||||
"""
|
||||
Current module graph.
|
||||
"""
|
||||
return self._module_graph
|
||||
|
||||
@property
|
||||
def parent_package(self):
|
||||
"""
|
||||
Parent Package of this node.
|
||||
"""
|
||||
return self._parent_package
|
||||
|
||||
def add_runtime_module(self, module_name):
|
||||
"""
|
||||
Add a graph node representing a non-package Python module with the passed name dynamically defined at runtime.
|
||||
|
||||
Most modules are statically defined on-disk as standard Python files. Some modules, however, are dynamically
|
||||
defined in-memory at runtime (e.g., `gi.repository.Gst`, dynamically defined by the statically defined
|
||||
`gi.repository.__init__` module).
|
||||
|
||||
This method adds a graph node representing such a runtime module. Since this module is _not_ a package,
|
||||
all attempts to import submodules from this module in `from`-style import statements (e.g., the `queue`
|
||||
submodule in `from six.moves import queue`) will be silently ignored. To circumvent this, simply call
|
||||
`add_runtime_package()` instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module_name : str
|
||||
Fully-qualified name of this module (e.g., `gi.repository.Gst`).
|
||||
|
||||
Examples
|
||||
----------
|
||||
This method is typically called by `pre_safe_import_module()` hooks, e.g.:
|
||||
|
||||
def pre_safe_import_module(api):
|
||||
api.add_runtime_module(api.module_name)
|
||||
"""
|
||||
|
||||
self._module_graph.add_module(RuntimeModule(module_name))
|
||||
|
||||
def add_runtime_package(self, package_name):
|
||||
"""
|
||||
Add a graph node representing a non-namespace Python package with the passed name dynamically defined at
|
||||
runtime.
|
||||
|
||||
Most packages are statically defined on-disk as standard subdirectories containing `__init__.py` files. Some
|
||||
packages, however, are dynamically defined in-memory at runtime (e.g., `six.moves`, dynamically defined by
|
||||
the statically defined `six` module).
|
||||
|
||||
This method adds a graph node representing such a runtime package. All attributes imported from this package
|
||||
in `from`-style import statements that are submodules of this package (e.g., the `queue` submodule in `from
|
||||
six.moves import queue`) will be imported rather than ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
package_name : str
|
||||
Fully-qualified name of this package (e.g., `six.moves`).
|
||||
|
||||
Examples
|
||||
----------
|
||||
This method is typically called by `pre_safe_import_module()` hooks, e.g.:
|
||||
|
||||
def pre_safe_import_module(api):
|
||||
api.add_runtime_package(api.module_name)
|
||||
"""
|
||||
|
||||
self._module_graph.add_module(RuntimePackage(package_name))
|
||||
|
||||
def add_alias_module(self, real_module_name, alias_module_name):
|
||||
"""
|
||||
Alias the source module to the target module with the passed names.
|
||||
|
||||
This method ensures that the next call to findNode() given the target module name will resolve this alias.
|
||||
This includes importing and adding a graph node for the source module if needed as well as adding a reference
|
||||
from the target to the source module.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
real_module_name : str
|
||||
Fully-qualified name of the **existing module** (i.e., the module being aliased).
|
||||
alias_module_name : str
|
||||
Fully-qualified name of the **non-existent module** (i.e., the alias to be created).
|
||||
"""
|
||||
|
||||
self._module_graph.alias_module(real_module_name, alias_module_name)
|
||||
|
||||
def append_package_path(self, directory):
|
||||
"""
|
||||
Modulegraph does a good job at simulating Python's, but it cannot handle packagepath `__path__` modifications
|
||||
packages make at runtime.
|
||||
|
||||
Therefore there is a mechanism whereby you can register extra paths in this map for a package, and it will be
|
||||
honored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
directory : str
|
||||
Absolute or relative path of the directory to be appended to this package's `__path__` attribute.
|
||||
"""
|
||||
|
||||
self._module_graph.append_package_path(self.module_name, directory)
|
||||
|
||||
|
||||
class PreFindModulePathAPI:
|
||||
"""
|
||||
Metadata communicating changes made by the current **pre-find module path hook** (i.e., hook run immediately
|
||||
_before_ a call to `ModuleGraph._find_module_path()` finding the hooked module's absolute path) back to PyInstaller.
|
||||
|
||||
Pre-find module path hooks _must_ define a `pre_find_module_path()` function accepting an instance of this class,
|
||||
whose attributes describe the subsequent `ModuleGraph._find_module_path()` call to be performed.
|
||||
|
||||
Pre-find module path hooks are typically used to change the absolute path from which a module will be
|
||||
subsequently imported and thus frozen into the executable. To do so, hooks may overwrite the default
|
||||
`search_dirs` list of the absolute paths of all directories to be searched for that module: e.g.,
|
||||
|
||||
def pre_find_module_path(api):
|
||||
api.search_dirs = ['/the/one/true/package/providing/this/module']
|
||||
|
||||
Each pre-find module path hook is run _only_ on the first call to `ModuleGraph._find_module_path()` for the
|
||||
corresponding module.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
The following attributes are **mutable** (i.e., modifiable). All changes to these attributes will be immediately
|
||||
respected by PyInstaller:
|
||||
|
||||
search_dirs : list
|
||||
List of the absolute paths of all directories to be searched for this module (in order). Searching will halt
|
||||
at the first directory containing this module.
|
||||
|
||||
Attributes (Immutable)
|
||||
----------
|
||||
The following attributes are **immutable** (i.e., read-only). For safety, any attempts to change these attributes
|
||||
_will_ result in a raised exception:
|
||||
|
||||
module_name : str
|
||||
Fully-qualified name of this module.
|
||||
module_graph : PyiModuleGraph
|
||||
Current module graph. For efficiency, this attribute is technically mutable. To preserve graph integrity,
|
||||
this attribute should nonetheless _never_ be modified. While read-only `PyiModuleGraph` methods (e.g.,
|
||||
`findNode()`) are safely callable from within pre-find module path hooks, methods modifying the graph are
|
||||
_not_. If graph modifications are required, consider an alternative type of hook (e.g., pre-import module
|
||||
hooks).
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
module_graph,
|
||||
module_name,
|
||||
search_dirs,
|
||||
):
|
||||
# Mutable attributes.
|
||||
self.search_dirs = search_dirs
|
||||
|
||||
# Immutable attributes.
|
||||
self._module_graph = module_graph
|
||||
self._module_name = module_name
|
||||
|
||||
# Immutable properties. No corresponding setters are defined.
|
||||
@property
|
||||
def module_graph(self):
|
||||
"""
|
||||
Current module graph.
|
||||
"""
|
||||
return self._module_graph
|
||||
|
||||
@property
|
||||
def module_name(self):
|
||||
"""
|
||||
Fully-qualified name of this module.
|
||||
"""
|
||||
return self._module_name
|
||||
|
||||
|
||||
class PostGraphAPI:
|
||||
"""
|
||||
Metadata communicating changes made by the current **post-graph hook** (i.e., hook run for a specific module
|
||||
transitively imported by the current application _after_ the module graph of all `import` statements performed by
|
||||
this application has been constructed) back to PyInstaller.
|
||||
|
||||
Post-graph hooks may optionally define a `post_graph()` function accepting an instance of this class,
|
||||
whose attributes describe the current state of the module graph and the hooked module's graph node.
|
||||
|
||||
Attributes (Mutable)
|
||||
----------
|
||||
The following attributes are **mutable** (i.e., modifiable). All changes to these attributes will be immediately
|
||||
respected by PyInstaller:
|
||||
|
||||
module_graph : PyiModuleGraph
|
||||
Current module graph.
|
||||
module : Node
|
||||
Graph node for the currently hooked module.
|
||||
|
||||
'With great power comes great responsibility.'
|
||||
|
||||
Attributes (Immutable)
|
||||
----------
|
||||
The following attributes are **immutable** (i.e., read-only). For safety, any attempts to change these attributes
|
||||
_will_ result in a raised exception:
|
||||
|
||||
__name__ : str
|
||||
Fully-qualified name of this module (e.g., `six.moves.tkinter`).
|
||||
__file__ : str
|
||||
Absolute path of this module. If this module is:
|
||||
* A standard (rather than namespace) package, this is the absolute path of this package's directory.
|
||||
* A namespace (rather than standard) package, this is the abstract placeholder `-`. (Don't ask. Don't tell.)
|
||||
* A non-package module or C extension, this is the absolute path of the corresponding file.
|
||||
__path__ : list
|
||||
List of the absolute paths of all directories comprising this package if this module is a package _or_ `None`
|
||||
otherwise. If this module is a standard (rather than namespace) package, this list contains only the absolute
|
||||
path of this package's directory.
|
||||
co : code
|
||||
Code object compiled from the contents of `__file__` (e.g., via the `compile()` builtin).
|
||||
analysis: build_main.Analysis
|
||||
The Analysis that load the hook.
|
||||
|
||||
Attributes (Private)
|
||||
----------
|
||||
The following attributes are technically mutable but private, and hence should _never_ be externally accessed or
|
||||
modified by hooks. Call the corresponding public methods instead:
|
||||
|
||||
_added_datas : list
|
||||
List of the `(name, path)` 2-tuples or TOC objects of all external data files required by the current hook,
|
||||
defaulting to the empty list. This is equivalent to the global `datas` hook attribute.
|
||||
_added_imports : list
|
||||
List of the fully-qualified names of all modules imported by the current hook, defaulting to the empty list.
|
||||
This is equivalent to the global `hiddenimports` hook attribute.
|
||||
_added_binaries : list
|
||||
List of the `(name, path)` 2-tuples or TOC objects of all external C extensions imported by the current hook,
|
||||
defaulting to the empty list. This is equivalent to the global `binaries` hook attribute.
|
||||
_module_collection_mode : dict
|
||||
Dictionary of package/module names and their corresponding collection mode strings. This is equivalent to the
|
||||
global `module_collection_mode` hook attribute.
|
||||
_bindepend_symlink_suppression : set
|
||||
A set of paths or path patterns corresponding to shared libraries for which binary dependency analysis should
|
||||
not generate symbolic links into top-level application directory.
|
||||
"""
|
||||
def __init__(self, module_name, module_graph, analysis):
|
||||
# Mutable attributes.
|
||||
self.module_graph = module_graph
|
||||
self.module = module_graph.find_node(module_name)
|
||||
assert self.module is not None # should not occur
|
||||
|
||||
# Immutable attributes.
|
||||
self.___name__ = module_name
|
||||
self.___file__ = self.module.filename
|
||||
self._co = self.module.code
|
||||
self._analysis = analysis
|
||||
|
||||
# To enforce immutability, convert this module's package path if any into an immutable tuple.
|
||||
self.___path__ = tuple(self.module.packagepath) \
|
||||
if self.module.packagepath is not None else None
|
||||
|
||||
#FIXME: Refactor "_added_datas", "_added_binaries", and "_deleted_imports" into sets. Since order of
|
||||
#import is important, "_added_imports" must remain a list.
|
||||
|
||||
# Private attributes.
|
||||
self._added_binaries = []
|
||||
self._added_datas = []
|
||||
self._added_imports = []
|
||||
self._deleted_imports = []
|
||||
self._module_collection_mode = {}
|
||||
self._bindepend_symlink_suppression = set()
|
||||
|
||||
# Immutable properties. No corresponding setters are defined.
|
||||
@property
|
||||
def __file__(self):
|
||||
"""
|
||||
Absolute path of this module's file.
|
||||
"""
|
||||
return self.___file__
|
||||
|
||||
@property
|
||||
def __path__(self):
|
||||
"""
|
||||
List of the absolute paths of all directories comprising this package if this module is a package _or_ `None`
|
||||
otherwise. If this module is a standard (rather than namespace) package, this list contains only the absolute
|
||||
path of this package's directory.
|
||||
"""
|
||||
return self.___path__
|
||||
|
||||
@property
|
||||
def __name__(self):
|
||||
"""
|
||||
Fully-qualified name of this module (e.g., `six.moves.tkinter`).
|
||||
"""
|
||||
return self.___name__
|
||||
|
||||
@property
|
||||
def co(self):
|
||||
"""
|
||||
Code object compiled from the contents of `__file__` (e.g., via the `compile()` builtin).
|
||||
"""
|
||||
return self._co
|
||||
|
||||
@property
|
||||
def analysis(self):
|
||||
"""
|
||||
build_main.Analysis that calls the hook.
|
||||
"""
|
||||
return self._analysis
|
||||
|
||||
# Obsolete immutable properties provided to preserve backward compatibility.
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Fully-qualified name of this module (e.g., `six.moves.tkinter`).
|
||||
|
||||
**This property has been deprecated by the `__name__` property.**
|
||||
"""
|
||||
return self.___name__
|
||||
|
||||
@property
|
||||
def graph(self):
|
||||
"""
|
||||
Current module graph.
|
||||
|
||||
**This property has been deprecated by the `module_graph` property.**
|
||||
"""
|
||||
return self.module_graph
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
"""
|
||||
Graph node for the currently hooked module.
|
||||
|
||||
**This property has been deprecated by the `module` property.**
|
||||
"""
|
||||
return self.module
|
||||
|
||||
# TODO: This incorrectly returns the list of the graph nodes of all modules *TRANSITIVELY* (rather than directly)
|
||||
# imported by this module. Unfortunately, this implies that most uses of this property are currently broken
|
||||
# (e.g., "hook-PIL.SpiderImagePlugin.py"). We only require this for the aforementioned hook, so contemplate
|
||||
# alternative approaches.
|
||||
@property
|
||||
def imports(self):
|
||||
"""
|
||||
List of the graph nodes of all modules directly imported by this module.
|
||||
"""
|
||||
return self.module_graph.iter_graph(start=self.module)
|
||||
|
||||
def add_imports(self, *module_names):
|
||||
"""
|
||||
Add all Python modules whose fully-qualified names are in the passed list as "hidden imports" upon which the
|
||||
current module depends.
|
||||
|
||||
This is equivalent to appending such names to the hook-specific `hiddenimports` attribute.
|
||||
"""
|
||||
# Append such names to the current list of all such names.
|
||||
self._added_imports.extend(module_names)
|
||||
|
||||
def del_imports(self, *module_names):
|
||||
"""
|
||||
Remove the named fully-qualified modules from the set of imports (either hidden or visible) upon which the
|
||||
current module depends.
|
||||
|
||||
This is equivalent to appending such names to the hook-specific `excludedimports` attribute.
|
||||
"""
|
||||
self._deleted_imports.extend(module_names)
|
||||
|
||||
def add_binaries(self, binaries):
|
||||
"""
|
||||
Add all external dynamic libraries in the passed list of `(src_name, dest_name)` 2-tuples as dependencies of the
|
||||
current module. This is equivalent to adding to the global `binaries` hook attribute.
|
||||
|
||||
For convenience, the `binaries` may also be a list of TOC-style 3-tuples `(dest_name, src_name, typecode)`.
|
||||
"""
|
||||
|
||||
# Detect TOC 3-tuple list by checking the length of the first entry
|
||||
if binaries and len(binaries[0]) == 3:
|
||||
self._added_binaries.extend(entry[:2] for entry in binaries)
|
||||
else:
|
||||
# NOTE: `format_binaries_and_datas` changes tuples from input format `(src_name, dest_name)` to output
|
||||
# format `(dest_name, src_name)`.
|
||||
self._added_binaries.extend(format_binaries_and_datas(binaries))
|
||||
|
||||
def add_datas(self, datas):
|
||||
"""
|
||||
Add all external data files in the passed list of `(src_name, dest_name)` 2-tuples as dependencies of the
|
||||
current module. This is equivalent to adding to the global `datas` hook attribute.
|
||||
|
||||
For convenience, the `datas` may also be a list of TOC-style 3-tuples `(dest_name, src_name, typecode)`.
|
||||
"""
|
||||
|
||||
# Detect TOC 3-tuple list by checking the length of the first entry
|
||||
if datas and len(datas[0]) == 3:
|
||||
self._added_datas.extend(entry[:2] for entry in datas)
|
||||
else:
|
||||
# NOTE: `format_binaries_and_datas` changes tuples from input format `(src_name, dest_name)` to output
|
||||
# format `(dest_name, src_name)`.
|
||||
self._added_datas.extend(format_binaries_and_datas(datas))
|
||||
|
||||
def set_module_collection_mode(self, name, mode):
|
||||
""""
|
||||
Set the package/module collection mode for the specified module name. If `name` is `None`, the hooked
|
||||
module/package name is used. `mode` can be one of valid mode strings (`'pyz'`, `'pyc'`, `'py'`, `'pyz+py'`,
|
||||
`'py+pyz'`) or `None`, which clears the setting for the module/package - but only within this hook's context!
|
||||
"""
|
||||
if name is None:
|
||||
name = self.__name__
|
||||
if mode is None:
|
||||
self._module_collection_mode.pop(name)
|
||||
else:
|
||||
self._module_collection_mode[name] = mode
|
||||
|
||||
def add_bindepend_symlink_suppression_pattern(self, pattern):
|
||||
"""
|
||||
Add the given path or path pattern to the set of patterns that prevent binary dependency analysis from creating
|
||||
a symbolic link to the top-level application directory.
|
||||
"""
|
||||
self._bindepend_symlink_suppression.add(pattern)
|
||||
344
venv/lib/python3.12/site-packages/PyInstaller/depend/utils.py
Executable file
344
venv/lib/python3.12/site-packages/PyInstaller/depend/utils.py
Executable file
@@ -0,0 +1,344 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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)
|
||||
#-----------------------------------------------------------------------------
|
||||
"""
|
||||
Utility functions related to analyzing/bundling dependencies.
|
||||
"""
|
||||
|
||||
import ctypes.util
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from types import CodeType
|
||||
|
||||
from PyInstaller import compat
|
||||
from PyInstaller import log as logging
|
||||
from PyInstaller.depend import bytecode
|
||||
from PyInstaller.depend.dylib import include_library
|
||||
from PyInstaller.exceptions import ExecCommandFailed
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def scan_code_for_ctypes(co):
|
||||
binaries = __recursively_scan_code_objects_for_ctypes(co)
|
||||
|
||||
# If any of the libraries has been requested with anything else than the basename, drop that entry and warn the
|
||||
# user - PyInstaller would need to patch the compiled pyc file to make it work correctly!
|
||||
binaries = set(binaries)
|
||||
for binary in list(binaries):
|
||||
# 'binary' might be in some cases None. Some Python modules (e.g., PyObjC.objc._bridgesupport) might contain
|
||||
# code like this:
|
||||
# dll = ctypes.CDLL(None)
|
||||
if not binary:
|
||||
# None values have to be removed too.
|
||||
binaries.remove(binary)
|
||||
elif binary != os.path.basename(binary):
|
||||
# TODO make these warnings show up somewhere.
|
||||
try:
|
||||
filename = co.co_filename
|
||||
except Exception:
|
||||
filename = 'UNKNOWN'
|
||||
logger.warning(
|
||||
"Ignoring %s imported from %s - only basenames are supported with ctypes imports!", binary, filename
|
||||
)
|
||||
binaries.remove(binary)
|
||||
|
||||
binaries = _resolveCtypesImports(binaries)
|
||||
return binaries
|
||||
|
||||
|
||||
def __recursively_scan_code_objects_for_ctypes(code: CodeType):
|
||||
"""
|
||||
Detects ctypes dependencies, using reasonable heuristics that should cover most common ctypes usages; returns a
|
||||
list containing names of binaries detected as dependencies.
|
||||
"""
|
||||
from PyInstaller.depend.bytecode import any_alias, search_recursively
|
||||
|
||||
binaries = []
|
||||
ctypes_dll_names = {
|
||||
*any_alias("ctypes.CDLL"),
|
||||
*any_alias("ctypes.cdll.LoadLibrary"),
|
||||
*any_alias("ctypes.WinDLL"),
|
||||
*any_alias("ctypes.windll.LoadLibrary"),
|
||||
*any_alias("ctypes.OleDLL"),
|
||||
*any_alias("ctypes.oledll.LoadLibrary"),
|
||||
*any_alias("ctypes.PyDLL"),
|
||||
*any_alias("ctypes.pydll.LoadLibrary"),
|
||||
}
|
||||
find_library_names = {
|
||||
*any_alias("ctypes.util.find_library"),
|
||||
}
|
||||
|
||||
for calls in bytecode.recursive_function_calls(code).values():
|
||||
for (name, args) in calls:
|
||||
if not len(args) == 1 or not isinstance(args[0], str):
|
||||
continue
|
||||
if name in ctypes_dll_names:
|
||||
# ctypes.*DLL() or ctypes.*dll.LoadLibrary()
|
||||
binaries.append(*args)
|
||||
elif name in find_library_names:
|
||||
# ctypes.util.find_library() needs to be handled separately, because we need to resolve the library base
|
||||
# name given as the argument (without prefix and suffix, e.g. 'gs') into corresponding full name (e.g.,
|
||||
# 'libgs.so.9').
|
||||
libname = args[0]
|
||||
if libname:
|
||||
try: # this try was inserted due to the ctypes bug https://github.com/python/cpython/issues/93094
|
||||
libname = ctypes.util.find_library(libname)
|
||||
except FileNotFoundError:
|
||||
libname = None
|
||||
logger.warning(
|
||||
'ctypes.util.find_library raised a FileNotFoundError. '
|
||||
'Supressing and assuming no lib with the name "%s" was found.', args[0]
|
||||
)
|
||||
if libname:
|
||||
# On Windows, `find_library` may return a full pathname. See issue #1934.
|
||||
libname = os.path.basename(libname)
|
||||
binaries.append(libname)
|
||||
|
||||
# The above handles any flavour of function/class call. We still need to capture the (albeit rarely used) case of
|
||||
# loading libraries with ctypes.cdll's getattr.
|
||||
for i in search_recursively(_scan_code_for_ctypes_getattr, code).values():
|
||||
binaries.extend(i)
|
||||
|
||||
return binaries
|
||||
|
||||
|
||||
_ctypes_getattr_regex = bytecode.bytecode_regex(
|
||||
rb"""
|
||||
# Matches 'foo.bar' or 'foo.bar.whizz'.
|
||||
|
||||
# Load the 'foo'.
|
||||
(
|
||||
(?:(?:""" + bytecode._OPCODES_EXTENDED_ARG + rb""").)*
|
||||
(?:""" + bytecode._OPCODES_FUNCTION_GLOBAL + rb""").
|
||||
)
|
||||
|
||||
# Load the 'bar.whizz' (one opcode per name component, each possibly preceded by name reference extension).
|
||||
(
|
||||
(?:
|
||||
(?:(?:""" + bytecode._OPCODES_EXTENDED_ARG + rb""").)*
|
||||
(?:""" + bytecode._OPCODES_FUNCTION_LOAD + rb""").
|
||||
)+
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _scan_code_for_ctypes_getattr(code: CodeType):
|
||||
"""
|
||||
Detect uses of ``ctypes.cdll.library_name``, which implies that ``library_name.dll`` should be collected.
|
||||
"""
|
||||
|
||||
key_names = ("cdll", "oledll", "pydll", "windll")
|
||||
|
||||
for match in bytecode.finditer(_ctypes_getattr_regex, code.co_code):
|
||||
name, attrs = match.groups()
|
||||
name = bytecode.load(name, code)
|
||||
attrs = bytecode.loads(attrs, code)
|
||||
|
||||
if attrs and attrs[-1] == "LoadLibrary":
|
||||
continue
|
||||
|
||||
# Capture `from ctypes import ole; ole.dll_name`.
|
||||
if len(attrs) == 1:
|
||||
if name in key_names:
|
||||
yield attrs[0] + ".dll"
|
||||
# Capture `import ctypes; ctypes.ole.dll_name`.
|
||||
if len(attrs) == 2:
|
||||
if name == "ctypes" and attrs[0] in key_names:
|
||||
yield attrs[1] + ".dll"
|
||||
|
||||
|
||||
# TODO: reuse this code with modulegraph implementation.
|
||||
def _resolveCtypesImports(cbinaries):
|
||||
"""
|
||||
Completes ctypes BINARY entries for modules with their full path.
|
||||
|
||||
Input is a list of c-binary-names (as found by `scan_code_instruction_for_ctypes`). Output is a list of tuples
|
||||
ready to be appended to the ``binaries`` of a modules.
|
||||
|
||||
This function temporarily extents PATH, LD_LIBRARY_PATH or DYLD_LIBRARY_PATH (depending on the platform) by
|
||||
CONF['pathex'] so shared libs will be search there, too.
|
||||
|
||||
Example:
|
||||
>>> _resolveCtypesImports(['libgs.so'])
|
||||
[(libgs.so', ''/usr/lib/libgs.so', 'BINARY')]
|
||||
"""
|
||||
from ctypes.util import find_library
|
||||
|
||||
from PyInstaller.config import CONF
|
||||
|
||||
if compat.is_unix:
|
||||
envvar = "LD_LIBRARY_PATH"
|
||||
elif compat.is_darwin:
|
||||
envvar = "DYLD_LIBRARY_PATH"
|
||||
else:
|
||||
envvar = "PATH"
|
||||
|
||||
def _setPaths():
|
||||
path = os.pathsep.join(CONF['pathex'])
|
||||
old = compat.getenv(envvar)
|
||||
if old is not None:
|
||||
path = os.pathsep.join((path, old))
|
||||
compat.setenv(envvar, path)
|
||||
return old
|
||||
|
||||
def _restorePaths(old):
|
||||
if old is None:
|
||||
compat.unsetenv(envvar)
|
||||
else:
|
||||
compat.setenv(envvar, old)
|
||||
|
||||
ret = []
|
||||
|
||||
# Try to locate the shared library on the disk. This is done by calling ctypes.util.find_library with
|
||||
# ImportTracker's local paths temporarily prepended to the library search paths (and restored after the call).
|
||||
old = _setPaths()
|
||||
for cbin in cbinaries:
|
||||
try:
|
||||
# There is an issue with find_library() where it can run into errors trying to locate the library. See
|
||||
# #5734.
|
||||
cpath = find_library(os.path.splitext(cbin)[0])
|
||||
except FileNotFoundError:
|
||||
# In these cases, find_library() should return None.
|
||||
cpath = None
|
||||
if compat.is_unix or compat.is_cygwin:
|
||||
# CAVEAT: find_library() is not the correct function. ctype's documentation says that it is meant to resolve
|
||||
# only the filename (as a *compiler* does) not the full path. Anyway, it works well enough on Windows and
|
||||
# macOS. On Linux, we need to implement more code to find out the full path.
|
||||
if cpath is None:
|
||||
cpath = cbin
|
||||
# "man ld.so" says that we should first search LD_LIBRARY_PATH and then the ldcache.
|
||||
for d in compat.getenv(envvar, '').split(os.pathsep):
|
||||
if os.path.isfile(os.path.join(d, cpath)):
|
||||
cpath = os.path.join(d, cpath)
|
||||
break
|
||||
else:
|
||||
if LDCONFIG_CACHE is None:
|
||||
load_ldconfig_cache()
|
||||
if cpath in LDCONFIG_CACHE:
|
||||
cpath = LDCONFIG_CACHE[cpath]
|
||||
assert os.path.isfile(cpath)
|
||||
else:
|
||||
cpath = None
|
||||
if cpath is None:
|
||||
# Skip warning message if cbin (basename of library) is ignored. This prevents messages like:
|
||||
# 'W: library kernel32.dll required via ctypes not found'
|
||||
if not include_library(cbin):
|
||||
continue
|
||||
# On non-Windows, automatically ignore all ctypes-based referenes to DLL files. This complements the above
|
||||
# check, which might not match potential case variations (e.g., `KERNEL32.dll`, instead of `kernel32.dll`)
|
||||
# due to case-sensitivity of the matching that is in effect on non-Windows platforms.
|
||||
if (not compat.is_win and not compat.is_cygwin) and cbin.lower().endswith('.dll'):
|
||||
continue
|
||||
logger.warning("Library %s required via ctypes not found", cbin)
|
||||
else:
|
||||
if not include_library(cpath):
|
||||
continue
|
||||
ret.append((cbin, cpath, "BINARY"))
|
||||
_restorePaths(old)
|
||||
return ret
|
||||
|
||||
|
||||
LDCONFIG_CACHE = None # cache the output of `/sbin/ldconfig -p`
|
||||
|
||||
|
||||
def load_ldconfig_cache():
|
||||
"""
|
||||
Create a cache of the `ldconfig`-output to call it only once.
|
||||
It contains thousands of libraries and running it on every dylib is expensive.
|
||||
"""
|
||||
global LDCONFIG_CACHE
|
||||
|
||||
if LDCONFIG_CACHE is not None:
|
||||
return
|
||||
|
||||
if compat.is_cygwin:
|
||||
# Not available under Cygwin; but we might be re-using general POSIX codepaths, and end up here. So exit early.
|
||||
LDCONFIG_CACHE = {}
|
||||
return
|
||||
|
||||
if compat.is_musl:
|
||||
# Musl deliberately doesn't use ldconfig. The ldconfig executable either doesn't exist or it's a functionless
|
||||
# executable which, on calling with any arguments, simply tells you that those arguments are invalid.
|
||||
LDCONFIG_CACHE = {}
|
||||
return
|
||||
|
||||
ldconfig = shutil.which('ldconfig')
|
||||
if ldconfig is None:
|
||||
# If `ldconfig` is not found in $PATH, search for it in some fixed directories. Simply use a second call instead
|
||||
# of fiddling around with checks for empty env-vars and string-concat.
|
||||
ldconfig = shutil.which('ldconfig', path='/usr/sbin:/sbin:/usr/bin:/bin')
|
||||
|
||||
# If we still could not find the 'ldconfig' command...
|
||||
if ldconfig is None:
|
||||
LDCONFIG_CACHE = {}
|
||||
return
|
||||
|
||||
if compat.is_freebsd or compat.is_openbsd:
|
||||
# This has a quite different format than other Unixes:
|
||||
# [vagrant@freebsd-10 ~]$ ldconfig -r
|
||||
# /var/run/ld-elf.so.hints:
|
||||
# search directories: /lib:/usr/lib:/usr/lib/compat:...
|
||||
# 0:-lgeom.5 => /lib/libgeom.so.5
|
||||
# 184:-lpython2.7.1 => /usr/local/lib/libpython2.7.so.1
|
||||
ldconfig_arg = '-r'
|
||||
splitlines_count = 2
|
||||
pattern = re.compile(r'^\s+\d+:-l(\S+)(\s.*)? => (\S+)')
|
||||
else:
|
||||
# Skip first line of the library list because it is just an informative line and might contain localized
|
||||
# characters. Example of first line with locale set to cs_CZ.UTF-8:
|
||||
#$ /sbin/ldconfig -p
|
||||
#V keši „/etc/ld.so.cache“ nalezeno knihoven: 2799
|
||||
# libzvbi.so.0 (libc6,x86-64) => /lib64/libzvbi.so.0
|
||||
# libzvbi-chains.so.0 (libc6,x86-64) => /lib64/libzvbi-chains.so.0
|
||||
ldconfig_arg = '-p'
|
||||
splitlines_count = 1
|
||||
pattern = re.compile(r'^\s+(\S+)(\s.*)? => (\S+)')
|
||||
|
||||
try:
|
||||
text = compat.exec_command(ldconfig, ldconfig_arg)
|
||||
except ExecCommandFailed:
|
||||
logger.warning("Failed to execute ldconfig. Disabling LD cache.")
|
||||
LDCONFIG_CACHE = {}
|
||||
return
|
||||
|
||||
text = text.strip().splitlines()[splitlines_count:]
|
||||
|
||||
LDCONFIG_CACHE = {}
|
||||
for line in text:
|
||||
# :fixme: this assumes library names do not contain whitespace
|
||||
m = pattern.match(line)
|
||||
|
||||
# Sanitize away any abnormal lines of output.
|
||||
if m is None:
|
||||
# Warn about it then skip the rest of this iteration.
|
||||
if re.search("Cache generated by:", line):
|
||||
# See #5540. This particular line is harmless.
|
||||
pass
|
||||
else:
|
||||
logger.warning("Unrecognised line of output %r from ldconfig", line)
|
||||
continue
|
||||
|
||||
path = m.groups()[-1]
|
||||
if compat.is_freebsd or compat.is_openbsd:
|
||||
# Insert `.so` at the end of the lib's basename. soname and filename may have (different) trailing versions.
|
||||
# We assume the `.so` in the filename to mark the end of the lib's basename.
|
||||
bname = os.path.basename(path).split('.so', 1)[0]
|
||||
name = 'lib' + m.group(1)
|
||||
assert name.startswith(bname)
|
||||
name = bname + '.so' + name[len(bname):]
|
||||
else:
|
||||
name = m.group(1)
|
||||
# ldconfig may know about several versions of the same lib, e.g., different arch, different libc, etc.
|
||||
# Use the first entry.
|
||||
if name not in LDCONFIG_CACHE:
|
||||
LDCONFIG_CACHE[name] = path
|
||||
Reference in New Issue
Block a user