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:
parent
845d6766f9
commit
b52b94ae85
|
@ -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
95
src/Tools/MakeMacBundleRelocatable.py
Normal file → Executable 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()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user