mit neuen venv und exe-Files
This commit is contained in:
367
venv3_12/Lib/site-packages/cx_Freeze/parser.py
Normal file
367
venv3_12/Lib/site-packages/cx_Freeze/parser.py
Normal file
@@ -0,0 +1,367 @@
|
||||
"""Implements `Parser` interface to create an abstraction to parse binary
|
||||
files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from cx_Freeze._compat import IS_MINGW, IS_WINDOWS
|
||||
from cx_Freeze.exception import PlatformError
|
||||
|
||||
# In Windows, to get dependencies, the default is to use lief package,
|
||||
# but LIEF can be disabled with:
|
||||
# set CX_FREEZE_BIND=imagehlp
|
||||
if IS_WINDOWS or IS_MINGW:
|
||||
import lief
|
||||
|
||||
with suppress(ImportError):
|
||||
from .util import BindError, GetDependentFiles
|
||||
try:
|
||||
# LIEF 0.15+
|
||||
lief.logging.set_level(lief.logging.LEVEL.ERROR)
|
||||
except AttributeError:
|
||||
lief.logging.set_level(lief.logging.LOGGING_LEVEL.ERROR)
|
||||
|
||||
LIEF_DISABLED = os.environ.get("CX_FREEZE_BIND", "") == "imagehlp"
|
||||
PE_EXT = (".exe", ".dll", ".pyd")
|
||||
MAGIC_ELF = b"\x7fELF"
|
||||
NON_ELF_EXT = ".a:.c:.h:.py:.pyc:.pyi:.pyx:.pxd:.txt:.html:.xml".split(":")
|
||||
NON_ELF_EXT += ".png:.jpg:.gif:.jar:.json".split(":")
|
||||
|
||||
|
||||
class Parser(ABC):
|
||||
"""`Parser` interface."""
|
||||
|
||||
def __init__(
|
||||
self, path: list[str], bin_path_includes: list[str], silent: int = 0
|
||||
) -> None:
|
||||
self._path: list[str] = path
|
||||
self._bin_path_includes: list[str] = bin_path_includes
|
||||
self._silent: int = silent
|
||||
|
||||
self.dependent_files: dict[Path, set[Path]] = {}
|
||||
self.linker_warnings: dict = {}
|
||||
|
||||
@property
|
||||
def search_path(self) -> list[Path]:
|
||||
"""The default search path."""
|
||||
# This cannot be cached because os.environ["PATH"] can be changed in
|
||||
# freeze module before the call to get_dependent_files.
|
||||
env_path = os.environ["PATH"].split(os.pathsep)
|
||||
new_path = []
|
||||
for path in self._path + self._bin_path_includes + env_path:
|
||||
resolved_path = Path(path).resolve()
|
||||
if resolved_path not in new_path and resolved_path.is_dir():
|
||||
new_path.append(resolved_path)
|
||||
return new_path
|
||||
|
||||
def find_library(
|
||||
self, name: str, search_path: list[str | Path] | None = None
|
||||
) -> Path | None:
|
||||
if search_path is None:
|
||||
search_path = self.search_path
|
||||
for directory in map(Path, search_path):
|
||||
library = directory / name
|
||||
if library.is_file():
|
||||
return library.resolve()
|
||||
return None
|
||||
|
||||
def get_dependent_files(self, filename: str | Path) -> set[Path]:
|
||||
"""Return the file's dependencies using platform-specific tools."""
|
||||
filename = Path(filename).resolve()
|
||||
|
||||
with suppress(KeyError):
|
||||
return self.dependent_files[filename]
|
||||
|
||||
if not self._is_binary(filename):
|
||||
return set()
|
||||
|
||||
dependent_files: set[Path] = self._get_dependent_files(filename)
|
||||
self.dependent_files[filename] = dependent_files
|
||||
return dependent_files
|
||||
|
||||
@abstractmethod
|
||||
def _get_dependent_files(self, filename: Path) -> set[Path]:
|
||||
"""Return the file's dependencies using platform-specific tools
|
||||
(lief package or the imagehlp library on Windows, otool on Mac OS X or
|
||||
ldd on Linux); limit this list by the exclusion lists as needed.
|
||||
(Implemented separately for each platform).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _is_binary(filename: Path) -> bool:
|
||||
"""Determines whether the file is a binary (executable, shared library)
|
||||
file. (Overridden in each platform).
|
||||
"""
|
||||
return filename.is_file()
|
||||
|
||||
|
||||
class PEParser(Parser):
|
||||
"""`PEParser` is based on the `lief` package. If it is disabled,
|
||||
use the old friend `cx_Freeze.util` extension module.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, path: list[str], bin_path_includes: list[str], silent: int = 0
|
||||
) -> None:
|
||||
super().__init__(path, bin_path_includes, silent)
|
||||
if hasattr(lief.PE, "ParserConfig"):
|
||||
# LIEF 0.14+
|
||||
imports_only = lief.PE.ParserConfig()
|
||||
imports_only.parse_exports = False
|
||||
imports_only.parse_imports = True
|
||||
imports_only.parse_reloc = False
|
||||
imports_only.parse_rsrc = False
|
||||
imports_only.parse_signature = False
|
||||
self.imports_only = imports_only
|
||||
resource_only = lief.PE.ParserConfig()
|
||||
resource_only.parse_exports = False
|
||||
resource_only.parse_imports = False
|
||||
resource_only.parse_reloc = False
|
||||
resource_only.parse_rsrc = True
|
||||
resource_only.parse_signature = False
|
||||
self.resource_only = resource_only
|
||||
else:
|
||||
self.imports_only = None
|
||||
self.resource_only = None
|
||||
|
||||
@staticmethod
|
||||
def is_pe(filename: str | Path) -> bool:
|
||||
"""Determines whether the file is a PE file."""
|
||||
if isinstance(filename, str):
|
||||
filename = Path(filename)
|
||||
return filename.suffix.lower().endswith(PE_EXT) and filename.is_file()
|
||||
|
||||
_is_binary = is_pe
|
||||
|
||||
def _get_dependent_files_lief(self, filename: Path) -> set[Path]:
|
||||
with filename.open("rb", buffering=0) as raw:
|
||||
binary = lief.PE.parse(raw, self.imports_only or filename.name)
|
||||
if not binary:
|
||||
return set()
|
||||
|
||||
libraries: list[str] = []
|
||||
if binary.has_imports:
|
||||
libraries += binary.libraries
|
||||
for delay_import in binary.delay_imports:
|
||||
libraries.append(delay_import.name)
|
||||
|
||||
dependent_files: set[Path] = set()
|
||||
search_path = [filename.parent, *self.search_path]
|
||||
for name in libraries:
|
||||
library = self.find_library(name, search_path)
|
||||
if library:
|
||||
dependent_files.add(library)
|
||||
if name in self.linker_warnings:
|
||||
self.linker_warnings[name] = False
|
||||
elif name not in self.linker_warnings:
|
||||
self.linker_warnings[name] = True
|
||||
return dependent_files
|
||||
|
||||
def _get_dependent_files_imagehlp(self, filename: Path) -> set[Path]:
|
||||
env_path = os.environ["PATH"]
|
||||
os.environ["PATH"] = os.pathsep.join(
|
||||
[os.path.normpath(path) for path in self.search_path]
|
||||
)
|
||||
try:
|
||||
return {Path(dep) for dep in GetDependentFiles(filename)}
|
||||
except BindError as exc:
|
||||
# Sometimes this gets called when filename is not actually
|
||||
# a library (See issue 88).
|
||||
if self._silent < 3:
|
||||
print("WARNING: ignoring error during ", end="")
|
||||
print(f"GetDependentFiles({filename}):", exc)
|
||||
finally:
|
||||
os.environ["PATH"] = env_path
|
||||
return set()
|
||||
|
||||
if LIEF_DISABLED:
|
||||
_get_dependent_files = _get_dependent_files_imagehlp
|
||||
else:
|
||||
_get_dependent_files = _get_dependent_files_lief
|
||||
|
||||
def read_manifest(self, filename: str | Path) -> str:
|
||||
""":return: the XML schema of the manifest included in the executable
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if isinstance(filename, str):
|
||||
filename = Path(filename)
|
||||
with filename.open("rb", buffering=0) as raw:
|
||||
binary = lief.PE.parse(raw, self.resource_only or filename.name)
|
||||
resources_manager = binary.resources_manager
|
||||
return (
|
||||
resources_manager.manifest
|
||||
if resources_manager.has_manifest
|
||||
else None
|
||||
)
|
||||
|
||||
def write_manifest(self, filename: str | Path, manifest: str) -> None:
|
||||
""":return: write the XML schema of the manifest into the executable
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if isinstance(filename, str):
|
||||
filename = Path(filename)
|
||||
with filename.open("rb", buffering=0) as raw:
|
||||
binary = lief.PE.parse(raw, self.resource_only or filename.name)
|
||||
resources_manager = binary.resources_manager
|
||||
resources_manager.manifest = manifest
|
||||
builder = lief.PE.Builder(binary)
|
||||
builder.build_resources(True)
|
||||
builder.build()
|
||||
with TemporaryDirectory(prefix="cxfreeze-") as tmp_dir:
|
||||
tmp_path = Path(tmp_dir, filename.name)
|
||||
builder.write(os.fspath(tmp_path))
|
||||
shutil.move(tmp_path, filename)
|
||||
|
||||
|
||||
class ELFParser(Parser):
|
||||
"""`ELFParser` is based on the logic around invoking `patchelf` and
|
||||
`ldd`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, path: list[str], bin_path_includes: list[str], silent: int = 0
|
||||
) -> None:
|
||||
super().__init__(path, bin_path_includes, silent)
|
||||
self._patchelf = shutil.which("patchelf")
|
||||
self._verify_patchelf()
|
||||
|
||||
@staticmethod
|
||||
def is_elf(filename: str | Path) -> bool:
|
||||
"""Check if the executable is an ELF."""
|
||||
if isinstance(filename, str):
|
||||
filename = Path(filename)
|
||||
if (
|
||||
filename.suffix in NON_ELF_EXT
|
||||
or filename.is_symlink()
|
||||
or not filename.is_file()
|
||||
):
|
||||
return False
|
||||
with open(filename, "rb") as binary:
|
||||
four_bytes = binary.read(4)
|
||||
return bool(four_bytes == MAGIC_ELF)
|
||||
|
||||
_is_binary = is_elf
|
||||
|
||||
def _get_dependent_files(self, filename: Path) -> set[Path]:
|
||||
dependent_files: set[Path] = set()
|
||||
split_string = " => "
|
||||
dependent_file_index = 1
|
||||
args = ("ldd", filename)
|
||||
env = os.environ.copy()
|
||||
env.pop("LD_PRELOAD", None)
|
||||
process = subprocess.run(
|
||||
args, check=False, capture_output=True, encoding="utf_8", env=env
|
||||
)
|
||||
for line in process.stdout.splitlines():
|
||||
parts = line.expandtabs().strip().split(split_string)
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
partname = parts[dependent_file_index].strip()
|
||||
if partname == filename.name:
|
||||
continue
|
||||
if partname in ("not found", "(file not found)"):
|
||||
partname = Path(parts[0])
|
||||
for bin_path in self._bin_path_includes:
|
||||
partname = Path(bin_path, partname)
|
||||
if partname.is_file():
|
||||
dependent_files.add(partname)
|
||||
name = partname.name
|
||||
if name in self.linker_warnings:
|
||||
self.linker_warnings[name] = False
|
||||
break
|
||||
if not partname.is_file():
|
||||
name = partname.name
|
||||
if name not in self.linker_warnings:
|
||||
self.linker_warnings[name] = True
|
||||
continue
|
||||
if partname.startswith("("):
|
||||
continue
|
||||
pos = partname.find(" (")
|
||||
if pos >= 0:
|
||||
partname = partname[:pos].strip()
|
||||
if partname:
|
||||
dependent_files.add(Path(partname))
|
||||
if process.returncode and self._silent < 3:
|
||||
print("WARNING:", *args, "returns:")
|
||||
print(process.stderr, end="")
|
||||
return dependent_files
|
||||
|
||||
def get_rpath(self, filename: str | Path) -> str:
|
||||
"""Gets the rpath of the executable."""
|
||||
with suppress(subprocess.CalledProcessError):
|
||||
return self.run_patchelf(["--print-rpath", filename]).strip()
|
||||
return ""
|
||||
|
||||
def replace_needed(
|
||||
self, filename: str | Path, so_name: str, new_so_name: str
|
||||
) -> None:
|
||||
"""Replace DT_NEEDED entry in the dynamic table."""
|
||||
self._set_write_mode(filename)
|
||||
self.run_patchelf(["--replace-needed", so_name, new_so_name, filename])
|
||||
|
||||
def set_rpath(self, filename: str | Path, rpath: str) -> None:
|
||||
"""Sets the rpath of the executable."""
|
||||
self._set_write_mode(filename)
|
||||
if rpath == "$ORIGIN/.":
|
||||
rpath = "$ORIGIN"
|
||||
if rpath == self.get_rpath(filename):
|
||||
return
|
||||
try:
|
||||
self.run_patchelf(["--set-rpath", rpath, filename])
|
||||
except subprocess.CalledProcessError:
|
||||
self.run_patchelf(["--remove-rpath", filename])
|
||||
self.run_patchelf(["--add-rpath", rpath, filename])
|
||||
|
||||
def set_soname(self, filename: str | Path, new_so_name: str) -> None:
|
||||
"""Sets DT_SONAME entry in the dynamic table."""
|
||||
self._set_write_mode(filename)
|
||||
self.run_patchelf(["--set-soname", new_so_name, filename])
|
||||
|
||||
def run_patchelf(self, args: list[str]) -> str:
|
||||
process = subprocess.run(
|
||||
[self._patchelf, *args], check=True, capture_output=True, text=True
|
||||
)
|
||||
if self._silent < 1:
|
||||
print("patchelf", *args, "returns:", repr(process.stdout))
|
||||
if process.stderr:
|
||||
print("patchelf errors:", repr(process.stderr))
|
||||
return process.stdout
|
||||
|
||||
@staticmethod
|
||||
def _set_write_mode(filename: str | Path) -> None:
|
||||
filename = Path(filename)
|
||||
mode = filename.stat().st_mode
|
||||
if mode & stat.S_IWUSR == 0:
|
||||
filename.chmod(mode | stat.S_IWUSR)
|
||||
|
||||
def _verify_patchelf(self) -> None:
|
||||
"""Looks for the ``patchelf`` external binary in the PATH, checks for
|
||||
the required version, and throws an exception if a proper version
|
||||
can't be found. Otherwise, silence is golden.
|
||||
"""
|
||||
if not self._patchelf:
|
||||
msg = "Cannot find required utility `patchelf` in PATH"
|
||||
raise PlatformError(msg)
|
||||
try:
|
||||
version = self.run_patchelf(["--version"])
|
||||
except subprocess.CalledProcessError:
|
||||
msg = "Could not call `patchelf` binary"
|
||||
raise PlatformError(msg) from None
|
||||
|
||||
mobj = re.match(r"patchelf\s+(\d+(.\d+)?)", version)
|
||||
if mobj and tuple(map(int, mobj.group(1).split("."))) >= (0, 14):
|
||||
return
|
||||
msg = f"patchelf {version} found. cx_Freeze requires patchelf >= 0.14."
|
||||
raise ValueError(msg)
|
||||
Reference in New Issue
Block a user