3
from subprocess import check_call, check_output
7
# This script is intended to help copy dynamic libraries used by FreeCAD into
8
# a Mac application bundle and change dyld commands as appropriate. There are
9
# two key items that this currently does differently from other similar tools:
11
# * @rpath is used rather than @executable_path because the libraries need to
12
# be loadable through a Python interpreter and the FreeCAD binaries.
13
# * We need to be able to add multiple rpaths in some libraries.
15
# Assume any libraries in these paths don't need to be bundled
19
"/Library/Frameworks/3DconnexionClient.framework/",
22
# If a library is in these paths, but not systemPaths, a warning will be
23
# issued and it will NOT be bundled. Generally, libraries installed by
24
# MacPorts or Homebrew won't end up in /Library/Frameworks, so we assume
25
# that libraries found there aren't meant to be bundled.
26
warnPaths = ["/Library/Frameworks/"]
29
class LibraryNotFound(Exception):
35
self.path should be an absolute path to self.name
38
def __init__(self, name, path="", children=None):
43
self.children = children
46
def __eq__(self, other):
47
if not isinstance(other, Node):
49
return self.name == other.name
51
def __ne__(self, other):
52
return not self.__eq__(other)
55
return hash(self.name)
58
return self.name + " path: " + self.path + " num children: " + str(len(self.children))
64
def in_graph(self, node):
65
return node.name in list(self.graph)
67
def add_node(self, node):
68
self.graph[node.name] = node
70
def get_node(self, name):
71
if name in self.graph:
72
return self.graph[name]
75
def visit(self, operation, op_args=[]):
77
Perform a depth first visit of the graph, calling operation
82
for k in list(self.graph):
83
self.graph[k]._marked = False
85
for k in list(self.graph):
86
if not self.graph[k]._marked:
89
node_key = stack.pop()
90
self.graph[node_key]._marked = True
91
for ck in self.graph[node_key].children:
92
if not self.graph[ck]._marked:
94
operation(self, self.graph[node_key], *op_args)
98
return b"Mach-O" in check_output(["file", path])
101
def get_token(txt, delimiter=" (", first=True):
102
result = txt.decode().split(delimiter)
109
def is_system_lib(lib):
110
for p in systemPaths:
111
if lib.startswith(p):
114
if lib.startswith(p):
115
logging.warning("WARNING: library %s will not be bundled!" % lib)
116
logging.warning("See MakeMacRelocatable.py for more information.")
121
def get_path(name, search_paths):
122
for path in search_paths:
123
if os.path.isfile(os.path.join(path, name)):
128
def list_install_names(path_macho):
129
output = check_output(["otool", "-L", path_macho])
130
lines = output.split(b"\t")
133
# first line is the filename, and if it is a library, the second line
134
# is the install name of it
135
if path_macho.endswith(os.path.basename(get_token(lines[1]))):
141
lib = get_token(line)
142
if not is_system_lib(lib):
147
def library_paths(install_names, search_paths):
149
for name in install_names:
150
path = os.path.dirname(name)
151
lib_name = os.path.basename(name)
153
if path == "" or name[0] == "@":
154
# not absolute -- we need to find the path of this lib
155
path = get_path(lib_name, search_paths)
157
paths.append(os.path.join(path, lib_name))
162
def create_dep_nodes(install_names, search_paths):
164
Return a list of Node objects from the provided install names.
167
for lib in install_names:
168
install_path = os.path.dirname(lib)
169
lib_name = os.path.basename(lib)
171
# even if install_path is absolute, see if library can be found by
172
# searching search_paths, so that we have control over what library
174
path = get_path(lib_name, search_paths)
176
if install_path != "" and lib[0] != "@":
177
# we have an absolute path install name
182
logging.error("Unable to find LC_DYLD_LOAD entry: " + lib)
183
raise LibraryNotFound(lib_name + " not found in given search paths")
185
nodes.append(Node(lib_name, path))
190
def paths_at_depth(prefix, paths, depth):
193
dirs = os.path.join(prefix, p).strip("/").split("/")
194
if len(dirs) == depth:
199
def should_visit(prefix, path_filters, path):
200
s_path = path.strip("/").split("/")
202
# we only want to use filters if they have the same parent as path
203
for rel_pf in path_filters:
204
pf = os.path.join(prefix, rel_pf)
205
if os.path.split(pf)[0] == os.path.split(path)[0]:
208
# no filter that applies to this path
212
s_filter = pf.strip("/").split("/")
213
length = len(s_filter)
215
for i in range(len(s_path)):
216
if s_path[i] == s_filter[i]:
218
if matched == length or matched == len(s_path):
224
def build_deps_graph(graph, bundle_path, dirs_filter=None, search_paths=[]):
226
Walk bundle_path and build a graph of the encountered Mach-O binaries
227
and there dependencies
229
# make a local copy since we add to it
230
s_paths = list(search_paths)
234
for root, dirs, files in os.walk(bundle_path):
235
if dirs_filter is not None:
237
d for d in dirs if should_visit(bundle_path, dirs_filter, os.path.join(root, d))
240
s_paths.insert(0, root)
243
fpath = os.path.join(root, f)
244
ext = os.path.splitext(f)[1]
245
if (ext == "" and is_macho(fpath)) or ext == ".so" or ext == ".dylib":
246
visited[fpath] = False
249
for k in list(visited):
256
node = Node(os.path.basename(k2), os.path.dirname(k2))
257
if not graph.in_graph(node):
261
deps = create_dep_nodes(list_install_names(k2), s_paths)
263
logging.error("Failed to resolve dependency in " + k2)
267
if d.name not in node.children:
268
node.children.append(d.name)
270
dk = os.path.join(d.path, d.name)
271
if dk not in list(visited):
277
def in_bundle(lib, bundle_path):
278
if lib.startswith(bundle_path):
283
def copy_into_bundle(graph, node, bundle_path):
284
if not in_bundle(node.path, bundle_path):
285
source = os.path.join(node.path, node.name)
286
target = os.path.join(bundle_path, "lib", node.name)
287
logging.info("Bundling {}".format(source))
289
check_call(["cp", "-L", source, target])
291
node.path = os.path.dirname(target)
294
check_call(["chmod", "a+w", target])
297
def get_rpaths(library):
298
"Returns a list of rpaths specified within library"
300
out = check_output(["otool", "-l", library])
302
pathRegex = r"^path (.*) \(offset \d+\)$"
303
expectingRpath = False
305
for line in get_token(out, "\n", False):
308
if "cmd LC_RPATH" in line:
309
expectingRpath = True
310
elif "Load command" in line:
311
expectingRpath = False
313
m = re.match(pathRegex, line)
315
rpaths.append(m.group(1))
316
expectingRpath = False
321
def add_rpaths(graph, node, bundle_path):
322
lib = os.path.join(node.path, node.name)
324
if in_bundle(lib, bundle_path):
327
# Remove existing rpaths that could take precedence
328
for rpath in get_rpaths(lib):
329
logging.debug(" - rpath: " + rpath)
330
check_call(["install_name_tool", "-delete_rpath", rpath, lib])
333
install_names = list_install_names(lib)
336
for install_name in install_names:
337
name = os.path.basename(install_name)
338
# change install names to use rpaths
339
logging.debug(" ~ rpath: " + name + " => @rpath/" + name)
350
dep_node = node.children[node.children.index(name)]
351
rel_path = os.path.relpath(graph.get_node(dep_node).path, node.path)
354
rpath = "@loader_path/"
356
rpath = "@loader_path/" + rel_path + "/"
357
if rpath not in rpaths:
361
# Ensure that lib has rpath set
362
if not rpath in get_rpaths(lib):
363
logging.debug(" + rpath: " + rpath)
364
check_call(["install_name_tool", "-add_rpath", rpath, lib])
367
def change_libid(graph, node, bundle_path):
368
lib = os.path.join(node.path, node.name)
372
if in_bundle(lib, bundle_path):
373
logging.debug(" ~ id: " + node.name)
375
check_call(["install_name_tool", "-id", node.name, lib])
377
logging.warning("Failed to change bundle id {} in lib {}".format(node.name, lib))
380
def print_child(graph, node, path):
381
logging.debug(" >" + str(node))
384
def print_node(graph, node, path):
386
graph.visit(print_child, [node])
390
if len(sys.argv) < 2:
391
print("Usage " + sys.argv[0] + " path [additional search paths]")
395
bundle_path = os.path.abspath(os.path.join(path, "Contents"))
397
dir_filter = ["MacOS", "lib", "Mod"]
398
search_paths = [bundle_path + "/lib"] + sys.argv[2:]
400
# change to level to logging.DEBUG for diagnostic messages
402
stream=sys.stdout, level=logging.INFO, format="-- %(levelname)s: %(message)s"
405
logging.info("Analyzing bundle dependencies...")
406
build_deps_graph(graph, bundle_path, dir_filter, search_paths)
408
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
409
graph.visit(print_node, [bundle_path])
411
logging.info("Copying external dependencies to bundle...")
412
graph.visit(copy_into_bundle, [bundle_path])
414
logging.info("Updating dynamic loader paths...")
415
graph.visit(add_rpaths, [bundle_path])
417
logging.info("Setting bundled library IDs...")
418
graph.visit(change_libid, [bundle_path])
420
logging.info("Done.")
423
if __name__ == "__main__":