diff --git a/cx_Freeze/command/bdist_mac.py b/cx_Freeze/command/bdist_mac.py index 887a9617c..4a4050bab 100644 --- a/cx_Freeze/command/bdist_mac.py +++ b/cx_Freeze/command/bdist_mac.py @@ -11,8 +11,6 @@ from cx_Freeze.common import normalize_to_list from cx_Freeze.darwintools import ( - DarwinFile, - DarwinFileTracker, applyAdHocSignature, changeLoadReference, isMachOFile, @@ -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.", ), ( @@ -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: @@ -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. @@ -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] @@ -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. @@ -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! ) @@ -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 @@ -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, ()) diff --git a/cx_Freeze/command/build_exe.py b/cx_Freeze/command/build_exe.py index 2d8b36fcb..5a43c2ad1 100644 --- a/cx_Freeze/command/build_exe.py +++ b/cx_Freeze/command/build_exe.py @@ -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): diff --git a/cx_Freeze/darwintools.py b/cx_Freeze/darwintools.py index 9e403f1e6..8c1c95aba 100644 --- a/cx_Freeze/darwintools.py +++ b/cx_Freeze/darwintools.py @@ -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": @@ -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) diff --git a/cx_Freeze/freezer.py b/cx_Freeze/freezer.py index fea7b91d7..728e70149 100644 --- a/cx_Freeze/freezer.py +++ b/cx_Freeze/freezer.py @@ -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."""