Skip to content

Commit

Permalink
bdist_mac: move set_relative_reference_paths to build_exe (#2106)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelotduarte committed Oct 20, 2023
1 parent 64ed399 commit 66b6608
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 95 deletions.
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 @@ -997,11 +997,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

0 comments on commit 66b6608

Please sign in to comment.