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
This commit is contained in:
Bruce B. Lacey 2017-02-18 19:16:12 -08:00
parent 845d6766f9
commit b52b94ae85
2 changed files with 69 additions and 27 deletions

View File

@ -196,6 +196,7 @@ if(APPLE)
${CMAKE_INSTALL_PREFIX}/${PACKAGE_NAME}.app/Contents) ${CMAKE_INSTALL_PREFIX}/${PACKAGE_NAME}.app/Contents)
set(CMAKE_INSTALL_LIBDIR ${CMAKE_INSTALL_PREFIX}/lib) set(CMAKE_INSTALL_LIBDIR ${CMAKE_INSTALL_PREFIX}/lib)
endif(FREECAD_CREATE_MAC_APP) endif(FREECAD_CREATE_MAC_APP)
set(CMAKE_MACOSX_RPATH 1)
endif(APPLE) endif(APPLE)
OPTION(BUILD_FEM "Build the FreeCAD FEM module" ON) OPTION(BUILD_FEM "Build the FreeCAD FEM module" ON)

95
src/Tools/MakeMacBundleRelocatable.py Normal file → Executable file
View File

@ -3,6 +3,7 @@ import sys
from subprocess import Popen, PIPE, check_call, check_output from subprocess import Popen, PIPE, check_call, check_output
import pprint import pprint
import re import re
import logging
# This script is intended to help copy dynamic libraries used by FreeCAD into # 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 # a Mac application bundle and change dyld commands as appropriate. There are
@ -44,13 +45,15 @@ class Node:
return not self.__eq__(other) return not self.__eq__(other)
def __hash__(self): def __hash__(self):
return hash(self.name) return hash(self.name)
def __str__(self):
return self.name + " path: " + self.path + " num children: " + str(len(self.children))
class DepsGraph: class DepsGraph:
graph = {} graph = {}
def in_graph(self, node): def in_graph(self, node):
return node.name in self.graph.keys() return node.name in self.graph.keys()
def add_node(self, node): def add_node(self, node):
self.graph[node.name] = node self.graph[node.name] = node
@ -65,10 +68,10 @@ class DepsGraph:
on each node. on each node.
""" """
stack = [] stack = []
for k in self.graph.keys(): for k in self.graph.keys():
self.graph[k]._marked = False self.graph[k]._marked = False
for k in self.graph.keys(): for k in self.graph.keys():
if not self.graph[k]._marked: if not self.graph[k]._marked:
stack.append(k) stack.append(k)
@ -85,7 +88,7 @@ def is_macho(path):
output = check_output(["file", path]) output = check_output(["file", path])
if output.count("Mach-O") != 0: if output.count("Mach-O") != 0:
return True return True
return False return False
def is_system_lib(lib): def is_system_lib(lib):
@ -94,8 +97,8 @@ def is_system_lib(lib):
return True return True
for p in warnPaths: for p in warnPaths:
if lib.startswith(p): if lib.startswith(p):
print "WARNING: library %s will not be bundled!" % lib logging.warning("WARNING: library %s will not be bundled!" % lib)
print "See MakeMacRelocatable.py for more information." logging.warning("See MakeMacRelocatable.py for more information.")
return True return True
return False return False
@ -128,7 +131,7 @@ def library_paths(install_names, search_paths):
for name in install_names: for name in install_names:
path = os.path.dirname(name) path = os.path.dirname(name)
lib_name = os.path.basename(name) lib_name = os.path.basename(name)
if path == "" or name[0] == "@": if path == "" or name[0] == "@":
#not absolute -- we need to find the path of this lib #not absolute -- we need to find the path of this lib
path = get_path(lib_name, search_paths) path = get_path(lib_name, search_paths)
@ -145,22 +148,22 @@ def create_dep_nodes(install_names, search_paths):
for lib in install_names: for lib in install_names:
install_path = os.path.dirname(lib) install_path = os.path.dirname(lib)
lib_name = os.path.basename(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 #searching search_paths, so that we have control over what library
#location to use #location to use
path = get_path(lib_name, search_paths) path = get_path(lib_name, search_paths)
if install_path != "" and lib[0] != "@": if install_path != "" and lib[0] != "@":
#we have an absolute path install name #we have an absolute path install name
if not path: if not path:
path = install_path path = install_path
if not path: if not path:
raise LibraryNotFound(lib_name + "not found in given paths") raise LibraryNotFound(lib_name + "not found in given paths")
nodes.append(Node(lib_name, path)) nodes.append(Node(lib_name, path))
return nodes return nodes
def paths_at_depth(prefix, paths, depth): def paths_at_depth(prefix, paths, depth):
@ -170,11 +173,11 @@ def paths_at_depth(prefix, paths, depth):
if len(dirs) == depth: if len(dirs) == depth:
filtered.append(p) filtered.append(p)
return filtered return filtered
def should_visit(prefix, path_filters, path): def should_visit(prefix, path_filters, path):
s_path = path.strip('/').split('/') s_path = path.strip('/').split('/')
filters = [] 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: for rel_pf in path_filters:
pf = os.path.join(prefix, rel_pf) pf = os.path.join(prefix, rel_pf)
if os.path.split(pf)[0] == os.path.split(path)[0]: if os.path.split(pf)[0] == os.path.split(path)[0]:
@ -192,7 +195,7 @@ def should_visit(prefix, path_filters, path):
matched += 1 matched += 1
if matched == length or matched == len(s_path): if matched == length or matched == len(s_path):
return True return True
return False return False
def build_deps_graph(graph, bundle_path, dirs_filter=None, search_paths=[]): 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 #make a local copy since we add to it
s_paths = list(search_paths) s_paths = list(search_paths)
visited = {} visited = {}
for root, dirs, files in os.walk(bundle_path): 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))] os.path.join(root, d))]
s_paths.insert(0, root) s_paths.insert(0, root)
for f in files: for f in files:
fpath = os.path.join(root, f) fpath = os.path.join(root, f)
ext = os.path.splitext(f)[1] 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)) node = Node(os.path.basename(k2), os.path.dirname(k2))
if not graph.in_graph(node): if not graph.in_graph(node):
graph.add_node(node) graph.add_node(node)
deps = create_dep_nodes(list_install_names(k2), s_paths) deps = create_dep_nodes(list_install_names(k2), s_paths)
for d in deps: for d in deps:
if d.name not in node.children: 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): def copy_into_bundle(graph, node, bundle_path):
if not in_bundle(node.path, bundle_path): if not in_bundle(node.path, bundle_path):
check_call([ "cp", "-L", os.path.join(node.path, node.name), source = os.path.join(node.path, node.name)
os.path.join(bundle_path, "lib", 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 #fix permissions
check_call([ "chmod", "a+w", check_output([ "chmod", "a+w", target ])
os.path.join(bundle_path, "lib", node.name) ])
#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): def get_rpaths(library):
"Returns a list of rpaths specified within 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) install_names = list_install_names(lib)
rpaths = [] rpaths = []
logging.debug(lib)
for install_name in install_names: for install_name in install_names:
name = os.path.basename(install_name) name = os.path.basename(install_name)
#change install names to use rpaths #change install names to use rpaths
logging.debug(" ~ " + name + " => @rpath/" + name)
check_call([ "install_name_tool", "-change", check_call([ "install_name_tool", "-change",
install_name, "@rpath/" + name, lib ]) install_name, "@rpath/" + name, lib ])
dep_node = node.children[node.children.index(name)] dep_node = node.children[node.children.index(name)]
rel_path = os.path.relpath(graph.get_node(dep_node).path, rel_path = os.path.relpath(graph.get_node(dep_node).path,
node.path) node.path)
@ -305,12 +315,30 @@ def add_rpaths(graph, node, bundle_path):
if rpath not in rpaths: if rpath not in rpaths:
rpaths.append(rpath) 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: for rpath in rpaths:
# Ensure that lib has rpath set # Ensure that lib has rpath set
if not rpath in get_rpaths(lib): if not rpath in get_rpaths(lib):
logging.debug(" + rpath: " + rpath + " to library " + lib)
check_output([ "install_name_tool", check_output([ "install_name_tool",
"-add_rpath", rpath, lib ]) "-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(): def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print "Usage " + sys.argv[0] + " path [additional search paths]" print "Usage " + sys.argv[0] + " path [additional search paths]"
@ -319,15 +347,28 @@ def main():
path = sys.argv[1] path = sys.argv[1]
bundle_path = os.path.abspath(os.path.join(path, "Contents")) bundle_path = os.path.abspath(os.path.join(path, "Contents"))
graph = DepsGraph() graph = DepsGraph()
dir_filter = ["bin", "lib", "Mod", "Mod/PartDesign", dir_filter = ["bin", "lib", "Mod", "Mod/PartDesign",
"lib/python2.7/site-packages", "lib/python2.7/site-packages",
"lib/python2.7/lib-dynload"] "lib/python2.7/lib-dynload"]
search_paths = [bundle_path + "/lib"] + sys.argv[2:] 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) 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]) graph.visit(copy_into_bundle, [bundle_path])
logging.info("Updating dynamic loader paths...")
graph.visit(add_rpaths, [bundle_path]) graph.visit(add_rpaths, [bundle_path])
logging.info("Done.")
if __name__ == "__main__": if __name__ == "__main__":
main() main()