Download src/Tools/MakeMacBundleRelocatable.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 17.8 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Tools/MakeMacBundleRelocatable.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Tools/MakeMacBundleRelocatable.py
-
curl -L -o MakeMacBundleRelocatable.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Tools/MakeMacBundleRelocatable.py
17.8 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| import os | |
| import sys | |
| from subprocess import check_call, check_output | |
| import re | |
| import logging | |
| # This script is intended to help copy dynamic libraries used by FreeCAD into | |
| # a Mac application bundle and change dyld commands as appropriate. There are | |
| # two key items that this currently does differently from other similar tools: | |
| # | |
| # * @rpath is used rather than @executable_path because the libraries need to | |
| # be loadable through a Python interpreter and the FreeCAD binaries. | |
| # * We need to be able to add multiple rpaths in some libraries. | |
| # Assume any libraries in these paths don't need to be bundled | |
| systemPaths = [ | |
| "/System/", | |
| "/usr/lib/", | |
| "/Library/Frameworks/3DconnexionClient.framework/", | |
| ] | |
| # If a library is in these paths, but not systemPaths, a warning will be | |
| # issued and it will NOT be bundled. Generally, libraries installed by | |
| # MacPorts or Homebrew won't end up in /Library/Frameworks, so we assume | |
| # that libraries found there aren't meant to be bundled. | |
| warnPaths = ["/Library/Frameworks/"] | |
| # dynamically get homebrew prefix ie. `brew --prefix` | |
| brew_prefix = check_output(["brew", "--prefix"], text=True).strip() | |
| class LibraryNotFound(Exception): | |
| pass | |
| class Node: | |
| """ | |
| self.path should be an absolute path to self.name | |
| """ | |
| def __init__(self, name, path="", children=None): | |
| self.name = name | |
| self.path = path | |
| if not children: | |
| children = list() | |
| self.children = children | |
| self._marked = False | |
| def __eq__(self, other): | |
| if not isinstance(other, Node): | |
| return False | |
| return self.name == other.name | |
| def __ne__(self, other): | |
| return not self.__eq__(other) | |
| def __hash__(self): | |
| return hash(self.name) | |
| def __str__(self): | |
| return self.name + " path: " + self.path + " num children: " + str(len(self.children)) | |
| class DepsGraph: | |
| graph = {} | |
| def in_graph(self, node): | |
| return node.name in list(self.graph) | |
| def add_node(self, node): | |
| self.graph[node.name] = node | |
| def get_node(self, name): | |
| if name in self.graph: | |
| return self.graph[name] | |
| return None | |
| def visit(self, operation, op_args=[]): | |
| """ " | |
| Perform a depth first visit of the graph, calling operation | |
| on each node. | |
| """ | |
| stack = [] | |
| for k in list(self.graph): | |
| self.graph[k]._marked = False | |
| for k in list(self.graph): | |
| if not self.graph[k]._marked: | |
| stack.append(k) | |
| while stack: | |
| node_key = stack.pop() | |
| self.graph[node_key]._marked = True | |
| for ck in self.graph[node_key].children: | |
| if not self.graph[ck]._marked: | |
| stack.append(ck) | |
| operation(self, self.graph[node_key], *op_args) | |
| def is_macho(path): | |
| return b"Mach-O" in check_output(["file", path]) | |
| def get_token(txt, delimiter=" (", first=True): | |
| result = txt.decode().split(delimiter) | |
| if first: | |
| return result[0] | |
| else: | |
| return result | |
| def is_system_lib(lib): | |
| for p in systemPaths: | |
| if lib.startswith(p): | |
| return True | |
| for p in warnPaths: | |
| if lib.startswith(p): | |
| logging.warning("WARNING: library %s will not be bundled!" % lib) | |
| logging.warning("See MakeMacRelocatable.py for more information.") | |
| return True | |
| return False | |
| def get_path(name, search_paths): | |
| for path in search_paths: | |
| full_path = os.path.join(path, name) | |
| if os.path.isfile(full_path): | |
| return path | |
| # also check if it's a symlink and resolve it | |
| if os.path.islink(full_path): | |
| real_path = os.path.realpath(full_path) | |
| if os.path.isfile(real_path): | |
| return path | |
| return None | |
| def resolve_loader_path(lib_path, referencing_lib_path): | |
| """ | |
| resolve @loader_path in lib_path relative to referencing_lib_path | |
| """ | |
| if lib_path.startswith("@loader_path/"): | |
| # get directory containing the referencing library | |
| referencing_dir = os.path.dirname(referencing_lib_path) | |
| # replace @loader_path with referencing directory | |
| resolved_path = lib_path.replace("@loader_path/", referencing_dir + "/") | |
| return resolved_path | |
| return lib_path | |
| def get_rpaths_for_resolution(library_path): | |
| """get rpaths from a library for resolving @rpath dependencies""" | |
| try: | |
| rpaths = get_rpaths(library_path) | |
| resolved_rpaths = [] | |
| for rpath in rpaths: | |
| if rpath.startswith("@loader_path"): | |
| # resolve @loader_path in rpath | |
| lib_dir = os.path.dirname(library_path) | |
| resolved = rpath.replace("@loader_path", lib_dir) | |
| resolved_rpaths.append(resolved) | |
| else: | |
| resolved_rpaths.append(rpath) | |
| return resolved_rpaths | |
| except: | |
| return [] | |
| def resolve_rpath(lib_path, search_paths, referencing_lib_path=None): | |
| """ | |
| resolve @rpath is lib_path by searching in search_paths and rpaths from referencing library | |
| """ | |
| if lib_path.startswith("@rpath/"): | |
| lib_name = lib_path.replace("@rpath/", "") | |
| # first check rpaths from the referencing library | |
| if referencing_lib_path: | |
| rpaths = get_rpaths_for_resolution(referencing_lib_path) | |
| for rpath in rpaths: | |
| full_path = os.path.join(rpath, lib_name) | |
| if os.path.isfile(full_path): | |
| return full_path | |
| # then check search paths as fallback | |
| # search for the library in all search paths | |
| for search_path in search_paths: | |
| full_path = os.path.join(search_path, lib_name) | |
| if os.path.isfile(full_path): | |
| return full_path | |
| if os.path.islink(full_path): | |
| real_path = os.path.realpath(full_path) | |
| if os.path.isfile(real_path): | |
| return full_path | |
| return lib_path | |
| def list_install_names(path_macho): | |
| output = check_output(["otool", "-L", path_macho]) | |
| lines = output.split(b"\t") | |
| libs = [] | |
| # first line is the filename, and if it is a library, the second line | |
| # is the install name of it | |
| if path_macho.endswith(os.path.basename(get_token(lines[1]))): | |
| lines = lines[2:] | |
| else: | |
| lines = lines[1:] | |
| for line in lines: | |
| lib = get_token(line) | |
| if not is_system_lib(lib): | |
| libs.append(lib) | |
| return libs | |
| def library_paths(install_names, search_paths): | |
| paths = [] | |
| for name in install_names: | |
| path = os.path.dirname(name) | |
| lib_name = os.path.basename(name) | |
| if path == "" or name[0] == "@": | |
| # not absolute -- we need to find the path of this lib | |
| path = get_path(lib_name, search_paths) | |
| paths.append(os.path.join(path, lib_name)) | |
| return paths | |
| def create_dep_nodes(install_names, search_paths, referencing_lib_path=None): | |
| """ | |
| Return a list of Node objects from the provided install names. | |
| referencing_lib_path: path to the library that references these dependencies | |
| """ | |
| nodes = [] | |
| for lib in install_names: | |
| original_lib = lib | |
| # resolve @loader_path if present | |
| if referencing_lib_path and lib.startswith("@loader_path/"): | |
| lib = resolve_loader_path(lib, referencing_lib_path) | |
| logging.debug( | |
| f"Resolved {original_lib} to {lib} (referencing from {referencing_lib_path})" | |
| ) | |
| # resolve @rpath if present | |
| elif lib.startswith("@rpath/"): | |
| resolved_lib = resolve_rpath(lib, search_paths) | |
| if resolved_lib != lib: | |
| lib = resolved_lib | |
| logging.debug(f"resolved {original_lib} to {lib}") | |
| install_path = os.path.dirname(lib) | |
| lib_name = os.path.basename(lib) | |
| path = get_path(lib_name, search_paths) | |
| if install_path != "" and lib[0] != "@": | |
| # we have an absolute path install name | |
| if not path: | |
| path = install_path | |
| if not path: | |
| logging.error("unable to find LC_DYLD_LOAD entry: " + original_lib) | |
| if referencing_lib_path: | |
| logging.error(f" referenced from: {referencing_lib_path}") | |
| logging.error(f" resolved to: {lib}") | |
| logging.error(f" searching for: {lib_name}") | |
| logging.error(f" search paths: {search_paths}") | |
| raise LibraryNotFound(lib_name + " not found in given search paths:") | |
| nodes.append(Node(lib_name, path)) | |
| return nodes | |
| def paths_at_depth(prefix, paths, depth): | |
| filtered = [] | |
| for p in paths: | |
| dirs = os.path.join(prefix, p).strip("/").split("/") | |
| if len(dirs) == depth: | |
| filtered.append(p) | |
| return filtered | |
| def should_visit(prefix, path_filters, path): | |
| s_path = path.strip("/").split("/") | |
| filters = [] | |
| # we only want to use filters if they have the same parent as path | |
| for rel_pf in path_filters: | |
| pf = os.path.join(prefix, rel_pf) | |
| if os.path.split(pf)[0] == os.path.split(path)[0]: | |
| filters.append(pf) | |
| if not filters: | |
| # no filter that applies to this path | |
| return True | |
| for pf in filters: | |
| s_filter = pf.strip("/").split("/") | |
| length = len(s_filter) | |
| matched = 0 | |
| for i in range(len(s_path)): | |
| if s_path[i] == s_filter[i]: | |
| matched += 1 | |
| if matched == length or matched == len(s_path): | |
| return True | |
| return False | |
| def build_deps_graph(graph, bundle_path, dirs_filter=None, search_paths=[]): | |
| """ | |
| Walk bundle_path and build a graph of the encountered Mach-O binaries | |
| and there dependencies | |
| """ | |
| # make a local copy since we add to it | |
| s_paths = list(search_paths) | |
| visited = {} | |
| for root, dirs, files in os.walk(bundle_path): | |
| if dirs_filter is not None: | |
| dirs[:] = [ | |
| d for d in dirs if should_visit(bundle_path, dirs_filter, os.path.join(root, d)) | |
| ] | |
| s_paths.insert(0, root) | |
| # Automatically add Homebrew Cellar lib directories to search paths | |
| homebrew_cellar = os.path.join(brew_prefix, "Cellar") | |
| if os.path.exists(homebrew_cellar): | |
| for cellar_dir in os.listdir(homebrew_cellar): | |
| cellar_path = os.path.join(homebrew_cellar, cellar_dir) | |
| if os.path.isdir(cellar_path): | |
| # Look for version directories | |
| for version_dir in os.listdir(cellar_path): | |
| version_path = os.path.join(cellar_path, version_dir) | |
| lib_path = os.path.join(version_path, "lib") | |
| if os.path.isdir(lib_path): | |
| if lib_path not in s_paths: | |
| s_paths.append(lib_path) | |
| logging.debug(f"Auto-discovered Homebrew lib path: {lib_path}") | |
| for f in files: | |
| fpath = os.path.join(root, f) | |
| ext = os.path.splitext(f)[1] | |
| if (ext == "" and is_macho(fpath)) or ext == ".so" or ext == ".dylib": | |
| visited[fpath] = False | |
| stack = [] | |
| for k in list(visited): | |
| if not visited[k]: | |
| stack.append(k) | |
| while stack: | |
| k2 = stack.pop() | |
| visited[k2] = True | |
| node = Node(os.path.basename(k2), os.path.dirname(k2)) | |
| if not graph.in_graph(node): | |
| graph.add_node(node) | |
| try: | |
| deps = create_dep_nodes(list_install_names(k2), s_paths, k2) | |
| except Exception: | |
| logging.error("Failed to resolve dependency in " + k2) | |
| raise | |
| for d in deps: | |
| if d.name not in node.children: | |
| node.children.append(d.name) | |
| dk = os.path.join(d.path, d.name) | |
| if dk not in list(visited): | |
| visited[dk] = False | |
| if not visited[dk]: | |
| stack.append(dk) | |
| def in_bundle(lib, bundle_path): | |
| if lib.startswith(bundle_path): | |
| return True | |
| return False | |
| def copy_into_bundle(graph, node, bundle_path): | |
| if not in_bundle(node.path, bundle_path): | |
| source = os.path.join(node.path, node.name) | |
| target = os.path.join(bundle_path, "lib", node.name) | |
| logging.info("Bundling {}".format(source)) | |
| check_call(["cp", "-L", source, target]) | |
| node.path = os.path.dirname(target) | |
| # fix permissions | |
| check_call(["chmod", "a+w", target]) | |
| def get_rpaths(library): | |
| "Returns a list of rpaths specified within library" | |
| out = check_output(["otool", "-l", library]) | |
| pathRegex = r"^path (.*) \(offset \d+\)$" | |
| expectingRpath = False | |
| rpaths = [] | |
| for line in get_token(out, "\n", False): | |
| line = line.strip() | |
| if "cmd LC_RPATH" in line: | |
| expectingRpath = True | |
| elif "Load command" in line: | |
| expectingRpath = False | |
| elif expectingRpath: | |
| m = re.match(pathRegex, line) | |
| if m: | |
| rpaths.append(m.group(1)) | |
| expectingRpath = False | |
| return rpaths | |
| def add_rpaths(graph, node, bundle_path): | |
| lib = os.path.join(node.path, node.name) | |
| if in_bundle(lib, bundle_path): | |
| logging.debug(lib) | |
| # Remove existing rpaths that could take precedence | |
| for rpath in get_rpaths(lib): | |
| logging.debug(" - rpath: " + rpath) | |
| check_call(["install_name_tool", "-delete_rpath", rpath, lib]) | |
| if node.children: | |
| install_names = list_install_names(lib) | |
| rpaths = [] | |
| for install_name in install_names: | |
| name = os.path.basename(install_name) | |
| # change install names to use rpaths | |
| logging.debug(" ~ rpath: " + name + " => @rpath/" + name) | |
| check_call( | |
| [ | |
| "install_name_tool", | |
| "-change", | |
| install_name, | |
| "@rpath/" + name, | |
| lib, | |
| ] | |
| ) | |
| dep_node = node.children[node.children.index(name)] | |
| rel_path = os.path.relpath(graph.get_node(dep_node).path, node.path) | |
| rpath = "" | |
| if rel_path == ".": | |
| rpath = "@loader_path/" | |
| else: | |
| rpath = "@loader_path/" + rel_path + "/" | |
| if rpath not in rpaths: | |
| rpaths.append(rpath) | |
| for rpath in rpaths: | |
| # Ensure that lib has rpath set | |
| if not rpath in get_rpaths(lib): | |
| logging.debug(" + rpath: " + rpath) | |
| check_call(["install_name_tool", "-add_rpath", rpath, lib]) | |
| def change_libid(graph, node, bundle_path): | |
| lib = os.path.join(node.path, node.name) | |
| logging.debug(lib) | |
| if in_bundle(lib, bundle_path): | |
| logging.debug(" ~ id: " + node.name) | |
| try: | |
| check_call(["install_name_tool", "-id", node.name, lib]) | |
| except Exception: | |
| logging.warning("Failed to change bundle id {} in lib {}".format(node.name, lib)) | |
| def print_child(graph, node, path): | |
| logging.debug(" >" + str(node)) | |
| def print_node(graph, node, path): | |
| logging.debug(node) | |
| graph.visit(print_child, [node]) | |
| def main(): | |
| if len(sys.argv) < 2: | |
| print("Usage " + sys.argv[0] + " path [additional search paths]") | |
| quit() | |
| path = sys.argv[1] | |
| bundle_path = os.path.abspath(os.path.join(path, "Contents")) | |
| graph = DepsGraph() | |
| dir_filter = ["MacOS", "lib", "Mod"] | |
| # get the initial search paths | |
| initial_search_paths = [bundle_path + "/lib"] + sys.argv[2:] | |
| # add additional search paths if required | |
| additional_search_paths = [os.path.join(brew_prefix, "lib", "gcc", "current")] | |
| # combine the initial + additional search paths | |
| search_paths = initial_search_paths + [ | |
| p for p in additional_search_paths if p not in initial_search_paths | |
| ] | |
| # change to level to logging.DEBUG for diagnostic messages | |
| logging.basicConfig( | |
| stream=sys.stdout, level=logging.INFO, format="-- %(levelname)s: %(message)s" | |
| ) | |
| logging.info("Analyzing bundle dependencies...") | |
| build_deps_graph(graph, bundle_path, dir_filter, search_paths) | |
| if logging.getLogger().getEffectiveLevel() == logging.DEBUG: | |
| graph.visit(print_node, [bundle_path]) | |
| logging.info("Copying external dependencies to bundle...") | |
| graph.visit(copy_into_bundle, [bundle_path]) | |
| logging.info("Updating dynamic loader paths...") | |
| graph.visit(add_rpaths, [bundle_path]) | |
| logging.info("Setting bundled library IDs...") | |
| graph.visit(change_libid, [bundle_path]) | |
| logging.info("Done.") | |
| if __name__ == "__main__": | |
| main() | |