Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bdist_mac: move set_relative_reference_paths to build_exe #2106

Merged
merged 1 commit into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 24 additions & 90 deletions cx_Freeze/command/bdist_mac.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@

from cx_Freeze.common import normalize_to_list
from cx_Freeze.darwintools import (
DarwinFile,
DarwinFileTracker,
applyAdHocSignature,
changeLoadReference,
isMachOFile,
Expand Down Expand Up @@ -140,7 +138,7 @@ class BdistMac(Command):
(
"plist-items=",
None,
"A list of key-value pairs (type: List[Tuple[str, str]]) to "
"A list of key-value pairs (type: list[tuple[str, str]]) to "
"be added to the app bundle Info.plist file.",
),
(
Expand Down Expand Up @@ -244,6 +242,9 @@ def initialize_options(self):
self.iconfile = None
self.qt_menu_nib = False

self.build_base = None
self.build_dir = None

def finalize_options(self):
# Make sure all options of multiple values are lists
for option in self.list_options:
Expand All @@ -252,9 +253,23 @@ def finalize_options(self):
if not isinstance(item, tuple) or len(item) != 2:
raise OptionError(
"Error, plist_items must be a list of key, value pairs "
"(List[Tuple[str, str]]) (bad list item)."
"(list[tuple[str, str]]) (bad list item)."
)

# Define the paths within the application bundle
self.set_undefined_options(
"build_exe",
("build_base", "build_base"),
("build_exe", "build_dir"),
)
self.bundle_dir = os.path.join(
self.build_base, f"{self.bundle_name}.app"
)
self.contents_dir = os.path.join(self.bundle_dir, "Contents")
self.bin_dir = os.path.join(self.contents_dir, "MacOS")
self.frameworks_dir = os.path.join(self.contents_dir, "Frameworks")
self.resources_dir = os.path.join(self.contents_dir, "Resources")

def create_plist(self):
"""Create the Contents/Info.plist file."""
# Use custom plist if supplied, otherwise create a simple default.
Expand Down Expand Up @@ -301,7 +316,7 @@ def set_absolute_reference_paths(self, path=None):
continue

out = subprocess.check_output(
("otool", "-L", filepath), encoding="utf-8"
("otool", "-L", filepath), encoding="utf_8"
)
for line in out.splitlines()[1:]:
lib = line.lstrip("\t").split(" (compat")[0]
Expand All @@ -314,65 +329,9 @@ def set_absolute_reference_paths(self, path=None):
# see if we provide the referenced file;
# if so, change the reference
if name in files:
subprocess.call(
(
"install_name_tool",
"-change",
lib,
replacement,
filepath,
)
)
changeLoadReference(filepath, lib, replacement)
applyAdHocSignature(filepath)

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.darwin_tracker:
# 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}"
changeLoadReference(
file_path_in_bin_dir,
oldReference=raw_path,
newReference=exe_path,
VERBOSE=False,
)

applyAdHocSignature(file_path_in_bin_dir)

def find_qt_menu_nib(self):
"""Returns a location of a qt_menu.nib folder, or None if this is not
a Qt application.
Expand Down Expand Up @@ -430,20 +389,6 @@ def prepare_qt_app(self):

def run(self):
self.run_command("build_exe")
build_exe = self.get_finalized_command("build_exe")
freezer: freezer.Freezer = build_exe.freezer

# Define the paths within the application bundle
self.bundle_dir = os.path.join(
build_exe.build_base, self.bundle_name + ".app"
)
self.contents_dir = os.path.join(self.bundle_dir, "Contents")
self.resources_dir = os.path.join(self.contents_dir, "Resources")
self.resources_lib_dir = os.path.join(
self.contents_dir, "Resources", "lib"
)
self.bin_dir = os.path.join(self.contents_dir, "MacOS")
self.frameworks_dir = os.path.join(self.contents_dir, "Frameworks")

# Remove App if it already exists
# ( avoids confusing issues where prior builds persist! )
Expand All @@ -454,21 +399,20 @@ def run(self):
# Find the executable name
executable = self.distribution.executables[0].target_name
_, self.bundle_executable = os.path.split(executable)
print(f"Executable name: {executable} - {build_exe.build_exe}")
print(f"Executable name: {self.build_dir}/{executable}")

# Build the app directory structure
self.mkpath(self.resources_dir) # /Resources
self.mkpath(self.bin_dir) # /MacOS
self.mkpath(self.frameworks_dir) # /Frameworks
self.mkpath(self.resources_dir) # /Resources

# Copy the full build_exe to Contents/Resources
self.copy_tree(build_exe.build_exe, self.resources_dir)
self.copy_tree(self.build_dir, self.resources_dir)

# Move only executables in Contents/Resources to Contents/MacOS
for executable in self.distribution.executables:
source = os.path.join(self.resources_dir, executable.target_name)
target = os.path.join(self.bin_dir, executable.target_name)
print(f"moving {source} -> {target}")
self.move_file(source, target)

# Make symlink between Resources/lib and Contents/MacOS so we can use
Expand Down Expand Up @@ -525,16 +469,6 @@ def run(self):
# Create the Info.plist file
self.execute(self.create_plist, ())

# Make all references to libraries relative
self.darwin_tracker: DarwinFileTracker = freezer.darwin_tracker
self.execute(
self.set_relative_reference_paths,
(
os.path.abspath(build_exe.build_exe),
os.path.abspath(self.bin_dir),
),
)

# Make library references absolute if enabled
if self.absolute_reference_path:
self.execute(self.set_absolute_reference_paths, ())
Expand Down
2 changes: 0 additions & 2 deletions cx_Freeze/command/build_exe.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,6 @@ def run(self):
include_msvcr=self.include_msvcr,
)

# keep freezer around so that its data case be used in bdist_mac phase
self.freezer = freezer
freezer.freeze()

def set_source_location(self, name, *pathParts):
Expand Down
50 changes: 49 additions & 1 deletion cx_Freeze/darwintools.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def _getMachOCommands(path: Path) -> list[MachOCommand]:
current_command_lines = None

# split the output into separate load commands
out = subprocess.check_output(shell_command, encoding="utf-8")
out = subprocess.check_output(shell_command, encoding="utf_8")
for raw_line in out.splitlines():
line = raw_line.strip()
if line[:12] == "Load command":
Expand Down Expand Up @@ -664,3 +664,51 @@ def finalizeReferences(self):
)
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}"
changeLoadReference(
file_path_in_bin_dir,
oldReference=raw_path,
newReference=exe_path,
VERBOSE=False,
)

applyAdHocSignature(file_path_in_bin_dir)
7 changes: 5 additions & 2 deletions cx_Freeze/freezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,11 +993,14 @@ class DarwinFreezer(Freezer, Parser):
def __init__(self, *args, **kwargs):
Freezer.__init__(self, *args, **kwargs)
Parser.__init__(self, self.path, self.bin_path_includes, self.silent)
self.darwin_tracker: DarwinFileTracker | None = None
self.darwin_tracker = DarwinFileTracker()
self.darwin_tracker: DarwinFileTracker = DarwinFileTracker()

def _post_freeze_hook(self) -> None:
self.darwin_tracker.finalizeReferences()
# Make all references to libraries relative
self.darwin_tracker.set_relative_reference_paths(
self.target_dir, self.target_dir
)

def _pre_copy_hook(self, source: Path, target: Path) -> tuple[Path, Path]:
"""Prepare the source and target paths."""
Expand Down