Files
aufbau2csv/venv3_12/Lib/site-packages/cx_Freeze/darwintools.py

720 lines
28 KiB
Python

# ruff: noqa
from __future__ import annotations
import sysconfig
import os
import shutil
import stat
import subprocess
from tempfile import TemporaryDirectory
from collections.abc import Iterable
from pathlib import Path
from cx_Freeze.exception import PlatformError
# In a MachO file, need to deal specially with links that use @executable_path,
# @loader_path, @rpath
#
# @executable_path - where ultimate calling executable is
# @loader_path - directory of current object
# @rpath - list of paths to check
# (earlier rpaths have higher priority, i believe)
#
# Resolving these variables (particularly @rpath) requires tracing through the
# sequence linked MachO files leading the the current file, to determine which
# directories are included in the current rpath.
def isMachOFile(path: Path) -> bool:
"""Determines whether the file is a Mach-O file."""
if not path.is_file():
return False
if b"Mach-O" in subprocess.check_output(("file", path)):
return True
return False
class MachOReference:
"""Represents a linking reference from MachO file to another file."""
def __init__(
self,
source_file: DarwinFile,
raw_path: str,
resolved_path: Path | None,
):
""":param source_file: DarwinFile object for file in which the reference
was found
:param raw_path: The load path that appears in the file
(may include @rpath, etc.)
:param resolved_path: The path resolved to an explicit path to a file
on system. Or None, if the path could not be resolved at the time the
DarwinFile was processed.
"""
self.source_file: DarwinFile = source_file
self.raw_path: str = raw_path
self.resolved_path: Path | None = resolved_path
# True if the referenced file is copied into the frozen package
# (i.e., not a non-copied system file)
self.is_copied = False
# reference to target DarwinFile (but only if file is copied into app)
self.target_file: DarwinFile | None = None
def isResolved(self) -> bool:
return self.resolved_path is not None
def setTargetFile(self, darwin_file: DarwinFile):
self.target_file = darwin_file
self.is_copied = True
class DarwinFile:
"""A DarwinFile object represents a file that will be copied into the
application, and record where it was ultimately moved to in the application
bundle. Mostly used to provide special handling for copied files that are
Mach-O files.
"""
def __init__(
self,
path: str | Path,
referencing_file: DarwinFile | None = None,
strict: bool = False,
):
""":param path: The original path of the DarwinFile
(before copying into app)
:param referencing_file: DarwinFile object representing the referencing
source file
:param strict: Do not make guesses about rpath resolution. If the
load does not resolve, throw an Exception.
"""
self.path = Path(path).resolve()
self.referencing_file: DarwinFile | None = None
self.strict = strict
# path to file in build directory (set as part of freeze process)
self._build_path: Path | None = None
# commands in a Mach-O file
self.commands: list[MachOCommand] = []
self.loadCommands: list[MachOLoadCommand] = []
self.rpathCommands: list[MachORPathCommand] = []
# note: if file gets referenced twice (or more), it will only be the
# first reference that gets recorded.
# mapping of raw load paths to absolute resolved paths
# (or None, if no resolution was determined)
self.libraryPathResolution: dict[str, Path | None] = {}
# the is of entries in the rpath in effect for this file.
self._rpath: list[Path] | None = None
# dictionary of MachOReference objects, by their paths.
# Path used is the resolved path, if available, and otherwise the
# unresolved load path.
self.machOReferenceForTargetPath: dict[Path, MachOReference] = {}
if not isMachOFile(self.path):
self.isMachO = False
return
# if this is a MachO file, extract linking information from it
self.isMachO = True
self.commands = MachOCommand._getMachOCommands(self.path)
self.loadCommands = [
c for c in self.commands if isinstance(c, MachOLoadCommand)
]
self.rpathCommands = [
c for c in self.commands if isinstance(c, MachORPathCommand)
]
self.referencing_file = referencing_file
self.getRPath()
self.resolveLibraryPaths()
# Create MachOReference objects for all the binaries referenced form
# this file.
for raw_path, resolved_path in self.libraryPathResolution.items():
# the path to use for storing in dictionary
if resolved_path is None:
dict_path = Path(raw_path)
else:
dict_path = resolved_path
if dict_path in self.machOReferenceForTargetPath:
if self.strict:
raise PlatformError(
f"ERROR: Multiple dynamic libraries from {self.path}"
f" resolved to the same file ({dict_path})."
)
print(
f"WARNING: Multiple dynamic libraries from {self.path}"
f" resolved to the same file ({dict_path})."
)
continue
reference = MachOReference(
source_file=self,
raw_path=raw_path,
resolved_path=resolved_path,
)
self.machOReferenceForTargetPath[dict_path] = reference
def __str__(self):
parts = []
# parts.append("RPath Commands: {}".format(self.rpathCommands))
# parts.append("Load commands: {}".format(self.loadCommands))
parts.append(f"Mach-O File: {self.path}")
parts.append("Resolved rpath:")
for rpath in self.getRPath():
parts.append(f" {rpath}")
parts.append("Loaded libraries:")
for rpath in self.libraryPathResolution:
parts.append(f" {rpath} -> {self.libraryPathResolution[rpath]}")
return "\n".join(parts)
def fileReferenceDepth(self) -> int:
"""Returns how deep this Mach-O file is in the dynamic load order."""
if self.referencing_file is not None:
return self.referencing_file.fileReferenceDepth() + 1
return 0
def printFileInformation(self):
"""Prints information about the Mach-O file."""
print(f"[{self.fileReferenceDepth()}] File: {self.path}")
print(" Commands:")
if len(self.commands) > 0:
for cmd in self.commands:
print(f" {cmd}")
else:
print(" [None]")
# This can be included for even more detail on the problem file.
# print(" Load commands:")
# if len(self.loadCommands) > 0:
# for cmd in self.loadCommands: print(f' {cmd}')
# else: print(" [None]")
print(" RPath commands:")
if len(self.rpathCommands) > 0:
for rpc in self.rpathCommands:
print(f" {rpc}")
else:
print(" [None]")
print(" Calculated RPath:")
rpath = self.getRPath()
if len(rpath) > 0:
for path in rpath:
print(f" {path}")
else:
print(" [None]")
if self.referencing_file is not None:
print("Referenced from:")
self.referencing_file.printFileInformation()
def setBuildPath(self, path: Path):
self._build_path = path
def getBuildPath(self) -> Path | None:
return self._build_path
@staticmethod
def isExecutablePath(path: str) -> bool:
return path.startswith("@executable_path")
@staticmethod
def isLoaderPath(path: str) -> bool:
return path.startswith("@loader_path")
@staticmethod
def isRPath(path: str) -> bool:
return path.startswith("@rpath")
def resolveLoader(self, path: str) -> Path | None:
"""Resolve a path that includes @loader_path. @loader_path represents
the directory in which the DarwinFile is located.
"""
if self.isLoaderPath(path):
return self.path.parent / Path(path).relative_to("@loader_path")
raise PlatformError(f"resolveLoader() called on bad path: {path}")
def resolveExecutable(self, path: str) -> Path:
"""@executable_path should resolve to the directory where the original
executable was located. By default, we set that to the directory of
the library, so it would resolve in the same was as if linked from an
executable in the same directory.
"""
# consider making this resolve to the directory of the python
# interpreter? Apparently not a big issue in practice, since the
# code has been like this forever.
if self.isExecutablePath(path):
return self.path.parent / Path(path).relative_to(
"@executable_path/"
)
raise PlatformError(f"resolveExecutable() called on bad path: {path}")
def resolveRPath(self, path: str) -> Path | None:
for rpath in self.getRPath():
test_path = rpath / Path(path).relative_to("@rpath")
if isMachOFile(test_path):
return test_path
if not self.strict:
# If not strictly enforcing rpath, return None here, and leave any
# error to .finalizeReferences() instead.
return None
print(f"\nERROR: Problem resolving RPath [{path}] in file:")
self.printFileInformation()
raise PlatformError(f"resolveRPath() failed to resolve path: {path}")
def getRPath(self) -> list[Path]:
"""Returns the rpath in effect for this file. Determined by rpath
commands in this file and (recursively) the chain of files that
referenced this file.
"""
if self._rpath is not None:
return self._rpath
raw_paths = [c.rpath for c in self.rpathCommands]
rpath = []
for raw_path in raw_paths:
test_rp = Path(raw_path)
if test_rp.is_absolute():
rpath.append(test_rp)
elif self.isLoaderPath(raw_path):
rpath.append(self.resolveLoader(raw_path).resolve())
elif self.isExecutablePath(raw_path):
rpath.append(self.resolveExecutable(raw_path).resolve())
rpath = [raw_path for raw_path in rpath if raw_path.exists()]
if self.referencing_file is not None:
rpath = self.referencing_file.getRPath() + rpath
self._rpath = rpath
return rpath
def resolvePath(self, path: str) -> Path | None:
"""Resolves any @executable_path, @loader_path, and @rpath references
in a path.
"""
if self.isLoaderPath(path): # replace @loader_path
return self.resolveLoader(path)
if self.isExecutablePath(path): # replace @executable_path
return self.resolveExecutable(path)
if self.isRPath(path): # replace @rpath
return self.resolveRPath(path)
test_path = Path(path)
if test_path.is_absolute(): # just use the path, if it is absolute
return test_path
test_path = self.path.parent / path
if isMachOFile(test_path):
return test_path.resolve()
if self.strict:
raise PlatformError(
f"Could not resolve path: {path} from file {self.path}."
)
print(
f"WARNING: Unable to resolve reference to {path} from "
f"file {self.path}. Frozen application may not "
f"function correctly."
)
return None
def resolveLibraryPaths(self):
for cmd in self.loadCommands:
raw_path = cmd.load_path
resolved_path = self.resolvePath(raw_path)
self.libraryPathResolution[raw_path] = resolved_path
def getDependentFilePaths(self) -> set[Path]:
"""Returns a list the available resolved paths to dependencies."""
dependents: set[Path] = set()
for ref in self.machOReferenceForTargetPath.values():
# skip load references that could not be resolved
if ref.isResolved():
dependents.add(ref.resolved_path)
return dependents
def getMachOReferenceList(self) -> list[MachOReference]:
return list(self.machOReferenceForTargetPath.values())
def getMachOReferenceForPath(self, path: Path) -> MachOReference:
"""Returns the reference pointing to the specified path, baed on paths
stored in self.machOReferenceTargetPath. Raises Exception if not
available.
"""
try:
return self.machOReferenceForTargetPath[path]
except KeyError:
raise PlatformError(
f"Path {path} is not a path referenced from DarwinFile"
) from None
class MachOCommand:
"""Represents a load command in a MachO file."""
def __init__(self, lines: list[str]):
self.lines = lines
def displayString(self) -> str:
parts: list[str] = []
if len(self.lines) > 0:
parts.append(self.lines[0].strip())
if len(self.lines) > 1:
parts.append(self.lines[1].strip())
return " / ".join(parts)
def __repr__(self):
return f"<MachOCommand ({self.displayString()})>"
@staticmethod
def _getMachOCommands(path: Path) -> list[MachOCommand]:
"""Returns a list of load commands in the specified file, using
otool.
"""
shell_command = ("otool", "-l", path)
commands: list[MachOCommand] = []
current_command_lines = None
# split the output into separate load commands
out = subprocess.check_output(shell_command, encoding="utf_8")
for raw_line in out.splitlines():
line = raw_line.strip()
if line[:12] == "Load command":
if current_command_lines is not None:
commands.append(
MachOCommand.parseLines(current_command_lines)
)
current_command_lines = []
if current_command_lines is not None:
current_command_lines.append(line)
if current_command_lines is not None:
commands.append(MachOCommand.parseLines(current_command_lines))
return commands
@staticmethod
def parseLines(lines: list[str]) -> MachOCommand:
if len(lines) < 2:
return MachOCommand(lines)
parts = lines[1].split(" ")
if parts[0] != "cmd":
return MachOCommand(lines)
if parts[1] == "LC_LOAD_DYLIB":
return MachOLoadCommand(lines)
if parts[1] == "LC_RPATH":
return MachORPathCommand(lines)
return MachOCommand(lines)
class MachOLoadCommand(MachOCommand):
def __init__(self, lines: list[str]):
super().__init__(lines)
self.load_path = None
if len(self.lines) < 4:
return
pathline = self.lines[3]
pathline = pathline.strip()
if not pathline.startswith("name "):
return
pathline = pathline[4:].strip()
pathline = pathline.split("(offset")[0].strip()
self.load_path = pathline
def getPath(self):
return self.load_path
def __repr__(self):
return f"<LoadCommand path={self.load_path!r}>"
class MachORPathCommand(MachOCommand):
def __init__(self, lines: list[str]):
super().__init__(lines)
self.rpath = None
if len(self.lines) < 4:
return
pathline = self.lines[3]
pathline = pathline.strip()
if not pathline.startswith("path "):
return
pathline = pathline[4:].strip()
pathline = pathline.split("(offset")[0].strip()
self.rpath = pathline
def __repr__(self):
return f"<RPath path={self.rpath!r}>"
def _printFile(
darwinFile: DarwinFile,
seenFiles: set[DarwinFile],
level: int,
noRecurse=False,
):
"""Utility function to prints details about a DarwinFile and (optionally)
recursively any other DarwinFiles that it references.
"""
print("{}{}".format(level * "| ", os.fspath(darwinFile.path)), end="")
print(" (already seen)" if noRecurse else "")
if noRecurse:
return
for ref in darwinFile.machOReferenceForTargetPath.values():
if not ref.is_copied:
continue
file = ref.target_file
_printFile(
file,
seenFiles=seenFiles,
level=level + 1,
noRecurse=(file in seenFiles),
)
seenFiles.add(file)
return
def printMachOFiles(fileList: list[DarwinFile]):
seenFiles = set()
for file in fileList:
if file not in seenFiles:
seenFiles.add(file)
_printFile(file, seenFiles=seenFiles, level=0)
def change_load_reference(
filename: str, old_reference: str, new_reference: str, verbose: bool = True
):
"""Utility function that uses install_name_tool to change old_reference to
new_reference in the machO file specified by filename.
"""
if verbose:
print("Redirecting load reference for ", end="")
print(f"<{filename}> {old_reference} -> {new_reference}")
original = os.stat(filename).st_mode
new_mode = original | stat.S_IWUSR
if new_mode != original:
os.chmod(filename, new_mode)
subprocess.call(
(
"install_name_tool",
"-change",
old_reference,
new_reference,
filename,
)
)
if new_mode != original:
os.chmod(filename, original)
def apply_adhoc_signature(filename: str):
if sysconfig.get_platform().endswith("x86_64"):
return
# Apply for universal2 and arm64 machines
print("Applying AdHocSignature")
args = (
"codesign",
"--sign",
"-",
"--force",
"--preserve-metadata=entitlements,requirements,flags,runtime",
filename,
)
if subprocess.call(args):
# It may be a bug in Apple's codesign utility
# The workaround is to copy the file to another inode, then move it
# back erasing the previous file. The sign again.
with TemporaryDirectory(prefix="cxfreeze-") as tmp_dir:
tempname = os.path.join(tmp_dir, os.path.basename(filename))
shutil.copy(filename, tempname)
shutil.move(tempname, filename)
subprocess.call(args)
class DarwinFileTracker:
"""Object to track the DarwinFiles that have been added during a freeze."""
def __init__(self, strict: bool = False):
self.strict = strict
# list of DarwinFile objects for files being copied into project
self._copied_file_list: list[DarwinFile] = []
# mapping of (build directory) target paths to DarwinFile objects
self._darwin_file_for_build_path: dict[Path, DarwinFile] = {}
# mapping of (source location) paths to DarwinFile objects
self._darwin_file_for_source_path: dict[Path, DarwinFile] = {}
# a cache of MachOReference objects pointing to a given source path
self._reference_cache: dict[Path, MachOReference] = {}
def __iter__(self) -> Iterable[DarwinFile]:
return iter(self._copied_file_list)
def pathIsAlreadyCopiedTo(self, target_path: Path) -> bool:
"""Check if the given target_path has already has a file copied to
it.
"""
if target_path in self._darwin_file_for_build_path:
return True
return False
def getDarwinFile(
self, source_path: Path, target_path: Path
) -> DarwinFile:
"""Gets the DarwinFile for file copied from source_path to target_path.
If either (i) nothing, or (ii) a different file has been copied to
targetPath, raises a PlatformError.
"""
# check that the target file came from the specified source
targetDarwinFile: DarwinFile
try:
targetDarwinFile = self._darwin_file_for_build_path[target_path]
except KeyError:
raise PlatformError(
f"File {target_path} already copied to, "
"but no DarwinFile object found for it."
) from None
real_source = source_path.resolve()
target_real_source = targetDarwinFile.path.resolve()
if real_source != target_real_source:
# raise PlatformError(
print(
"*** WARNING ***\n"
f"Attempting to copy two files to {target_path}\n"
f"source 1: {targetDarwinFile.path} "
f"(real: {target_real_source})\n"
f"source 2: {source_path} (real: {real_source})\n"
"(This may be caused by including modules in the zip file "
"that rely on binary libraries with the same name.)"
"\nUsing only source 1."
)
return targetDarwinFile
def recordCopiedFile(self, target_path: Path, darwin_file: DarwinFile):
"""Record that a DarwinFile is being copied to a given path. If a
file has been copied to that path, raise a PlatformError.
"""
if self.pathIsAlreadyCopiedTo(target_path):
raise PlatformError(
"addFile() called with target_path already copied to "
f"(target_path={target_path})"
)
self._copied_file_list.append(darwin_file)
self._darwin_file_for_build_path[target_path] = darwin_file
self._darwin_file_for_source_path[darwin_file.path] = darwin_file
def cacheReferenceTo(self, source_path: Path, reference: MachOReference):
self._reference_cache[source_path] = reference
def getCachedReferenceTo(self, source_path: Path) -> MachOReference | None:
return self._reference_cache.get(source_path)
def findDarwinFileForFilename(self, filename: str) -> DarwinFile | None:
"""Attempts to locate a copied DarwinFile with the specified filename
and returns that. Otherwise returns None.
"""
basename = Path(filename).name
for file in self._copied_file_list:
if file.path.name == basename:
return file
return None
def finalizeReferences(self):
"""This function does a final pass through the references for all the
copied DarwinFiles and attempts to clean up any remaining references
that are not already marked as copied. It covers two cases where the
reference might not be marked as copied:
1) Files where _CopyFile was called without copyDependentFiles=True
(in which the information would not have been added to the
references at that time).
2) Files with broken @rpath references. We try to fix that up here by
seeing if the relevant file was located *anywhere* as part of the
freeze process.
"""
copied_file: DarwinFile
reference: MachOReference
for copied_file in self._copied_file_list:
for reference in copied_file.getMachOReferenceList():
if not reference.is_copied:
if reference.isResolved():
# if reference is resolve, simply check if the resolved
# path was otherwise copied and lookup the DarwinFile
# object.
target_path = reference.resolved_path.resolve()
if target_path in self._darwin_file_for_source_path:
reference.setTargetFile(
self._darwin_file_for_source_path[target_path]
)
else:
# if reference is not resolved, look through the copied
# files and try to find a candidate, and use it if
# found.
potential_target = self.findDarwinFileForFilename(
reference.raw_path
)
if potential_target is None:
# If we cannot find any likely candidate, fail.
if self.strict:
copied_file.printFileInformation()
raise PlatformError(
f"finalizeReferences() failed to resolve"
f" path [{reference.raw_path}] in file "
f"[{copied_file.path}]."
)
print(
"\nWARNING: Could not resolve dynamic link to "
f"[{reference.raw_path}] in file "
f"[{copied_file.path}], and could "
"not find any likely intended target."
)
continue
print(
f"WARNING: In file [{copied_file.path}]"
f" guessing that {reference.raw_path} "
f"resolved to {potential_target.path}."
)
reference.resolved_path = potential_target.path
reference.setTargetFile(potential_target)
def set_relative_reference_paths(self, build_dir: str, bin_dir: str):
"""Make all the references from included Mach-O files to other included
Mach-O files relative.
"""
darwin_file: DarwinFile
for darwin_file in self._copied_file_list:
# Skip text files
if darwin_file.path.suffix == ".txt":
continue
# get the relative path to darwin_file in build directory
print(f"Setting relative_reference_path for: {darwin_file}")
relative_copy_dest = os.path.relpath(
darwin_file.getBuildPath(), build_dir
)
# figure out directory where it will go in binary directory for
# .app bundle, this would be the Content/MacOS subdirectory in
# bundle. This is the file that needs to have its dynamic load
# references updated.
file_path_in_bin_dir = os.path.join(bin_dir, relative_copy_dest)
# for each file that this darwin_file references, update the
# reference as necessary; if the file is copied into the binary
# package, change the reference to be relative to @executable_path
# (so an .app bundle will work wherever it is moved)
for reference in darwin_file.getMachOReferenceList():
if not reference.is_copied:
# referenced file not copied -- assume this is a system
# file that will also be present on the user's machine,
# and do not change reference
continue
# this is the reference in the machO file that needs to be
# updated
raw_path = reference.raw_path
ref_target_file: DarwinFile = reference.target_file
# this is where file copied in build dir
abs_build_dest = ref_target_file.getBuildPath()
rel_build_dest = os.path.relpath(abs_build_dest, build_dir)
exe_path = f"@executable_path/{rel_build_dest}"
change_load_reference(
file_path_in_bin_dir, raw_path, exe_path, verbose=False
)
apply_adhoc_signature(file_path_in_bin_dir)