368 lines
14 KiB
Python
368 lines
14 KiB
Python
"""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)
|