From b52b94ae8518bb346250a46af3603f54bb5a3779 Mon Sep 17 00:00:00 2001 From: "Bruce B. Lacey" Date: Sat, 18 Feb 2017 19:16:12 -0800 Subject: [PATCH] Packaging: Set macOS dynamic loader paths * DYLD paths are set properly to prevent loading libraries external to the bundle a. LC_ID_DYLD is set to the basename of the library name (i.e. not the absolute path) when it is copied into the bundle b. Existing LC_RPATH entries in libraries are removed before adding the bundle-relative RPATH * Added configurable diagnostic logging to aid in debugging Fixes 0002886 --- CMakeLists.txt | 1 + src/Tools/MakeMacBundleRelocatable.py | 95 +++++++++++++++++++-------- 2 files changed, 69 insertions(+), 27 deletions(-) mode change 100644 => 100755 src/Tools/MakeMacBundleRelocatable.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a3ba6de201d..22d0d6d64efb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -196,6 +196,7 @@ if(APPLE) ${CMAKE_INSTALL_PREFIX}/${PACKAGE_NAME}.app/Contents) set(CMAKE_INSTALL_LIBDIR ${CMAKE_INSTALL_PREFIX}/lib) endif(FREECAD_CREATE_MAC_APP) + set(CMAKE_MACOSX_RPATH 1) endif(APPLE) OPTION(BUILD_FEM "Build the FreeCAD FEM module" ON) diff --git a/src/Tools/MakeMacBundleRelocatable.py b/src/Tools/MakeMacBundleRelocatable.py old mode 100644 new mode 100755 index f73ae0cc8ed6..6bcfd14177a4 --- a/src/Tools/MakeMacBundleRelocatable.py +++ b/src/Tools/MakeMacBundleRelocatable.py @@ -3,6 +3,7 @@ from subprocess import Popen, PIPE, check_call, check_output import pprint 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 @@ -44,13 +45,15 @@ 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 self.graph.keys() - + def add_node(self, node): self.graph[node.name] = node @@ -65,10 +68,10 @@ def visit(self, operation, op_args=[]): on each node. """ stack = [] - + for k in self.graph.keys(): self.graph[k]._marked = False - + for k in self.graph.keys(): if not self.graph[k]._marked: stack.append(k) @@ -85,7 +88,7 @@ def is_macho(path): output = check_output(["file", path]) if output.count("Mach-O") != 0: return True - + return False def is_system_lib(lib): @@ -94,8 +97,8 @@ def is_system_lib(lib): return True for p in warnPaths: if lib.startswith(p): - print "WARNING: library %s will not be bundled!" % lib - print "See MakeMacRelocatable.py for more information." + logging.warning("WARNING: library %s will not be bundled!" % lib) + logging.warning("See MakeMacRelocatable.py for more information.") return True return False @@ -128,7 +131,7 @@ def library_paths(install_names, search_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) @@ -145,22 +148,22 @@ def create_dep_nodes(install_names, search_paths): for lib in install_names: install_path = os.path.dirname(lib) lib_name = os.path.basename(lib) - - #even if install_path is absolute, see if library can be found by + + #even if install_path is absolute, see if library can be found by #searching search_paths, so that we have control over what library - #location to use + #location to use 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: raise LibraryNotFound(lib_name + "not found in given paths") nodes.append(Node(lib_name, path)) - + return nodes def paths_at_depth(prefix, paths, depth): @@ -170,11 +173,11 @@ def paths_at_depth(prefix, paths, depth): 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 + #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]: @@ -192,7 +195,7 @@ def should_visit(prefix, path_filters, path): 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=[]): @@ -202,7 +205,7 @@ def build_deps_graph(graph, bundle_path, dirs_filter=None, search_paths=[]): """ #make a local copy since we add to it s_paths = list(search_paths) - + visited = {} for root, dirs, files in os.walk(bundle_path): @@ -211,7 +214,7 @@ def build_deps_graph(graph, bundle_path, dirs_filter=None, search_paths=[]): os.path.join(root, d))] s_paths.insert(0, root) - + for f in files: fpath = os.path.join(root, f) ext = os.path.splitext(f)[1] @@ -230,7 +233,7 @@ def build_deps_graph(graph, bundle_path, dirs_filter=None, search_paths=[]): node = Node(os.path.basename(k2), os.path.dirname(k2)) if not graph.in_graph(node): graph.add_node(node) - + deps = create_dep_nodes(list_install_names(k2), s_paths) for d in deps: if d.name not in node.children: @@ -249,14 +252,19 @@ def in_bundle(lib, bundle_path): def copy_into_bundle(graph, node, bundle_path): if not in_bundle(node.path, bundle_path): - check_call([ "cp", "-L", os.path.join(node.path, node.name), - os.path.join(bundle_path, "lib", node.name) ]) + source = os.path.join(node.path, node.name) + target = os.path.join(bundle_path, "lib", node.name) + logging.info("Bundling {}".format(source)) + + check_output([ "cp", "-L", source, target ]) + + node.path = os.path.dirname(target) - node.path = os.path.join(bundle_path, "lib") - #fix permissions - check_call([ "chmod", "a+w", - os.path.join(bundle_path, "lib", node.name) ]) + check_output([ "chmod", "a+w", target ]) + + #Change the loader ID_DYLIB to a bundle-local name (i.e. non-absolute) + check_output([ "install_name_tool", "-id", node.name, target ]) def get_rpaths(library): "Returns a list of rpaths specified within library" @@ -288,12 +296,14 @@ def add_rpaths(graph, node, bundle_path): install_names = list_install_names(lib) rpaths = [] + logging.debug(lib) for install_name in install_names: name = os.path.basename(install_name) #change install names to use rpaths + logging.debug(" ~ " + 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) @@ -305,12 +315,30 @@ def add_rpaths(graph, node, bundle_path): if rpath not in rpaths: rpaths.append(rpath) + for rpath in get_rpaths(lib): + # Remove existing rpaths because the libraries copied into the + # bundle will point to a location outside the bundle + logging.debug(" - rpath: " + rpath) + check_output(["install_name_tool", "-delete_rpath", rpath, lib]) + for rpath in rpaths: # Ensure that lib has rpath set if not rpath in get_rpaths(lib): + logging.debug(" + rpath: " + rpath + " to library " + lib) check_output([ "install_name_tool", "-add_rpath", rpath, lib ]) + #Change the loader ID_DYLIB to a bundle-local name (i.e. non-absolute) + logging.debug(" ~ id: " + node.name) + check_output([ "install_name_tool", "-id", 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]" @@ -319,15 +347,28 @@ def main(): path = sys.argv[1] bundle_path = os.path.abspath(os.path.join(path, "Contents")) graph = DepsGraph() - dir_filter = ["bin", "lib", "Mod", "Mod/PartDesign", + dir_filter = ["bin", "lib", "Mod", "Mod/PartDesign", "lib/python2.7/site-packages", "lib/python2.7/lib-dynload"] search_paths = [bundle_path + "/lib"] + sys.argv[2:] + #change to level to logging.DEBUG for diagnostic messages + logging.basicConfig(stream=sys.stdout, level=logging.INFO, + format="%(asctime)s %(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("Done.") + if __name__ == "__main__": main()