FreeCAD/src/Tools/MakeMacBundleRelocatable.py

334 lines
11 KiB
Python

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()