import os import sys from subprocess import Popen, PIPE, check_call, check_output import pprint import re # 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/"] 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) 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 def get_node(self, name): if name in self.graph: return self.graph[name] return None def visit(self, operation, op_args=[]): """" Preform a depth first visit of the graph, calling operation 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) 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): output = check_output(["file", path]) if output.count("Mach-O") != 0: return True return False def is_system_lib(lib): for p in systemPaths: if lib.startswith(p): 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." return True return False def get_path(name, search_paths): for path in search_paths: if os.path.isfile(os.path.join(path, name)): return path return None def list_install_names(path_macho): output = check_output(["otool", "-L", path_macho]) lines = output.split("\t") libs = [] #first line is the the filename, and if it is a library, the second line #is the install name of it if path_macho.endswith(os.path.basename(lines[1].split(" (")[0])): lines = lines[2:] else: lines = lines[1:] for line in lines: lib = line.split(" (")[0] 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): """ Return a list of Node objects from the provided install names. """ nodes = [] 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 #searching search_paths, so that we have control over what library #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): 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 != None: dirs[:] = [d for d in dirs if should_visit(bundle_path, dirs_filter, 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] if ( (ext == "" and is_macho(fpath)) or ext == ".so" or ext == ".dylib" ): visited[fpath] = False stack = [] for k in visited.keys(): 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) deps = create_dep_nodes(list_install_names(k2), s_paths) 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 visited.keys(): 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): check_call([ "cp", "-L", os.path.join(node.path, node.name), os.path.join(bundle_path, "lib", node.name) ]) node.path = os.path.join(bundle_path, "lib") #fix permissions check_call([ "chmod", "a+w", os.path.join(bundle_path, "lib", node.name) ]) 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 out.split('\n'): 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): if node.children: lib = os.path.join(node.path, node.name) if in_bundle(lib, bundle_path): 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 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): check_output([ "install_name_tool", "-add_rpath", rpath, lib ]) 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 = ["bin", "lib", "Mod", "Mod/PartDesign", "lib/python2.7/site-packages", "lib/python2.7/lib-dynload"] search_paths = [bundle_path + "/lib"] + sys.argv[2:] build_deps_graph(graph, bundle_path, dir_filter, search_paths) graph.visit(copy_into_bundle, [bundle_path]) graph.visit(add_rpaths, [bundle_path]) if __name__ == "__main__": main()