Packaging: macOS relocation of libraries without children

* macOS install path must be <bundle>/MacOS in order for
    QLibrary to find qt.conf to set the correct bundle paths
  * Refactored to add an explicit graph traversal to set the
    dynamic loader id to handle the case where a bundled
    resource does not have any children
  * Fixed the case where rpaths were not removed from
    libraries without children
  * Improved diagnostics when bundling fail to finds
    a dependent library in the search path

Mantis: #0002886
Refs: #535
This commit is contained in:
Bruce B. Lacey 2017-02-20 12:09:51 -08:00
parent 402aef2e63
commit 21080af2a7
6 changed files with 68 additions and 51 deletions

View File

@ -144,7 +144,7 @@ before_install:
fi fi
export CMAKE_ARGS="${CMAKE_OPTS} -DFREECAD_USE_EXTERNAL_KDL=ON -DFREECAD_USE_EXTERNAL_PIVY=ON -DFREECAD_CREATE_MAC_APP=ON" export CMAKE_ARGS="${CMAKE_OPTS} -DFREECAD_USE_EXTERNAL_KDL=ON -DFREECAD_USE_EXTERNAL_PIVY=ON -DFREECAD_CREATE_MAC_APP=ON"
export INSTALLED_APP_PATH="/usr/local/FreeCAD.app/Contents/bin/FreeCAD" export INSTALLED_APP_PATH="/usr/local/FreeCAD.app/Contents/MacOS/FreeCAD"
;; ;;
*) *)

View File

@ -196,7 +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) set(CMAKE_MACOSX_RPATH TRUE)
endif(APPLE) endif(APPLE)
OPTION(BUILD_FEM "Build the FreeCAD FEM module" ON) OPTION(BUILD_FEM "Build the FreeCAD FEM module" ON)

View File

@ -1 +0,0 @@
../bin/FreeCAD

View File

@ -0,0 +1 @@
../MacOS/FreeCAD

View File

@ -49,11 +49,16 @@ if(BUILD_GUI)
RUNTIME DESTINATION bin RUNTIME DESTINATION bin
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
) )
else(WIN32) elseif(APPLE)
INSTALL(TARGETS FreeCADMain
RUNTIME DESTINATION MacOS
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
else()
INSTALL(TARGETS FreeCADMain INSTALL(TARGETS FreeCADMain
RUNTIME DESTINATION bin RUNTIME DESTINATION bin
) )
endif(WIN32) endif()
endif(BUILD_GUI) endif(BUILD_GUI)
######################## FreeCADMainCmd ######################## ######################## FreeCADMainCmd ########################

View File

@ -160,7 +160,8 @@ def create_dep_nodes(install_names, search_paths):
path = install_path path = install_path
if not path: if not path:
raise LibraryNotFound(lib_name + "not found in given paths") logging.error("Unable to find LC_DYLD_LOAD entry: " + lib)
raise LibraryNotFound(lib_name + " not found in given search paths")
nodes.append(Node(lib_name, path)) nodes.append(Node(lib_name, path))
@ -190,9 +191,9 @@ def should_visit(prefix, path_filters, path):
s_filter = pf.strip('/').split('/') s_filter = pf.strip('/').split('/')
length = len(s_filter) length = len(s_filter)
matched = 0 matched = 0
for i in range(len(s_path)): for i in range(len(s_path)):
if s_path[i] == s_filter[i]: if s_path[i] == s_filter[i]:
matched += 1 matched += 1
if matched == length or matched == len(s_path): if matched == length or matched == len(s_path):
return True return True
@ -234,7 +235,12 @@ def build_deps_graph(graph, bundle_path, dirs_filter=None, search_paths=[]):
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) try:
deps = create_dep_nodes(list_install_names(k2), s_paths)
except:
logging.error("Failed to resolve dependency in " + k2)
raise
for d in deps: for d in deps:
if d.name not in node.children: if d.name not in node.children:
node.children.append(d.name) node.children.append(d.name)
@ -256,15 +262,12 @@ def copy_into_bundle(graph, node, bundle_path):
target = os.path.join(bundle_path, "lib", node.name) target = os.path.join(bundle_path, "lib", node.name)
logging.info("Bundling {}".format(source)) logging.info("Bundling {}".format(source))
check_output([ "cp", "-L", source, target ]) check_call([ "cp", "-L", source, target ])
node.path = os.path.dirname(target) node.path = os.path.dirname(target)
#fix permissions #fix permissions
check_output([ "chmod", "a+w", target ]) check_call([ "chmod", "a+w", target ])
#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"
@ -290,47 +293,53 @@ def get_rpaths(library):
return rpaths return rpaths
def add_rpaths(graph, node, bundle_path): def add_rpaths(graph, node, bundle_path):
if node.children: lib = os.path.join(node.path, node.name)
lib = os.path.join(node.path, node.name)
if in_bundle(lib, bundle_path):
install_names = list_install_names(lib)
rpaths = []
logging.debug(lib) if in_bundle(lib, bundle_path):
for install_name in install_names: logging.debug(lib)
name = os.path.basename(install_name)
#change install names to use rpaths
logging.debug(" ~ " + name + " => @rpath/" + name)
check_call([ "install_name_tool", "-change",
install_name, "@rpath/" + name, lib ])
dep_node = node.children[node.children.index(name)] # Remove existing rpaths that could take precedence
rel_path = os.path.relpath(graph.get_node(dep_node).path, for rpath in get_rpaths(lib):
logging.debug(" - rpath: " + rpath)
check_call(["install_name_tool", "-delete_rpath", rpath, lib])
if node.children:
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
logging.debug(" ~ rpath: " + name + " => @rpath/" + name)
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) node.path)
rpath = "" rpath = ""
if rel_path == ".": if rel_path == ".":
rpath = "@loader_path/" rpath = "@loader_path/"
else: else:
rpath = "@loader_path/" + rel_path + "/" rpath = "@loader_path/" + rel_path + "/"
if rpath not in rpaths: if rpath not in rpaths:
rpaths.append(rpath) rpaths.append(rpath)
for rpath in get_rpaths(lib): for rpath in rpaths:
# Remove existing rpaths because the libraries copied into the # Ensure that lib has rpath set
# bundle will point to a location outside the bundle if not rpath in get_rpaths(lib):
logging.debug(" - rpath: " + rpath) logging.debug(" + rpath: " + rpath)
check_output(["install_name_tool", "-delete_rpath", rpath, lib]) check_call([ "install_name_tool", "-add_rpath", rpath, lib ])
for rpath in rpaths: def change_libid(graph, node, bundle_path):
# Ensure that lib has rpath set lib = os.path.join(node.path, node.name)
if not rpath in get_rpaths(lib):
logging.debug(" + rpath: " + rpath + " to library " + lib)
check_output([ "install_name_tool",
"-add_rpath", rpath, lib ])
#Change the loader ID_DYLIB to a bundle-local name (i.e. non-absolute) logging.debug(lib)
logging.debug(" ~ id: " + node.name)
check_output([ "install_name_tool", "-id", node.name, lib ]) if in_bundle(lib, bundle_path):
logging.debug(" ~ id: " + node.name)
check_call([ "install_name_tool", "-id", node.name, lib ])
def print_child(graph, node, path): def print_child(graph, node, path):
logging.debug(" >" + str(node)) logging.debug(" >" + str(node))
@ -354,7 +363,7 @@ def main():
#change to level to logging.DEBUG for diagnostic messages #change to level to logging.DEBUG for diagnostic messages
logging.basicConfig(stream=sys.stdout, level=logging.INFO, logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s" ) format="-- %(message)s" )
logging.info("Analyzing bundle dependencies...") 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)
@ -368,6 +377,9 @@ def main():
logging.info("Updating dynamic loader paths...") logging.info("Updating dynamic loader paths...")
graph.visit(add_rpaths, [bundle_path]) graph.visit(add_rpaths, [bundle_path])
logging.info("Setting bundled library IDs...")
graph.visit(change_libid, [bundle_path])
logging.info("Done.") logging.info("Done.")
if __name__ == "__main__": if __name__ == "__main__":