diff --git a/CMakeLists.txt b/CMakeLists.txt index 76a679d1ce28..ad734c0808b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,19 @@ OPTION(FREECAD_USE_EXTERNAL_PIVY "Use system installed python-pivy instead of th OPTION(FREECAD_USE_EXTERNAL_SMESH "Use system installed smesh instead of the bundled." OFF) OPTION(FREECAD_BUILD_DEBIAN "Prepare for a build of a Debian package" OFF) +if(APPLE) + OPTION(FREECAD_CREATE_MAC_APP "Create app bundle on install" OFF) + + if(FREECAD_CREATE_MAC_APP) + install( + DIRECTORY ${CMAKE_SOURCE_DIR}/src/MacAppBundle/FreeCAD.app + DESTINATION ${CMAKE_INSTALL_PREFIX} + ) + set(CMAKE_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX}/FreeCAD.app/Contents) + SET(CMAKE_INSTALL_LIBDIR ${CMAKE_INSTALL_PREFIX}/lib) + endif(FREECAD_CREATE_MAC_APP) +endif(APPLE) + OPTION(BUILD_CAM "Build the FreeCAD CAM module and the needed libs, be aware, unfinished code!" OFF) OPTION(BUILD_FEM "Build the FreeCAD FEM module, be aware, unfinished code!" ON) OPTION(BUILD_SANDBOX "Build the FreeCAD Sandbox module which is only for testing purposes" OFF) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aed73e059deb..8487a12342e5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,3 +25,6 @@ INSTALL(FILES Doc/Start_Page.html Doc/freecad.qhc Doc/freecad.qch DESTINATION ${CMAKE_INSTALL_DOCDIR} ) +if(FREECAD_CREATE_MAC_APP) + add_subdirectory(MacAppBundle) +endif(FREECAD_CREATE_MAC_APP) diff --git a/src/MacAppBundle/CMakeLists.txt b/src/MacAppBundle/CMakeLists.txt new file mode 100644 index 000000000000..adaa64774507 --- /dev/null +++ b/src/MacAppBundle/CMakeLists.txt @@ -0,0 +1,28 @@ +set(PYTHON_DIR_BASENAME python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}) + +if(PYTHON_LIBRARY MATCHES "(.*Python\\.framework).*") + #framework + set(PYTHON_DIR ${CMAKE_MATCH_1}/Versions/Current/lib/${PYTHON_DIR_BASENAME}) +else() + #unix + get_filename_component(PYTHON_DIR ${PYTHON_LIBRARY} PATH) + set(PYTHON_DIR ${PYTHON_DIR}/${PYTHON_DIR_BASENAME}) +endif() + +install(CODE "execute_process(COMMAND + ${CMAKE_COMMAND} -E copy_directory ${PYTHON_DIR} ${CMAKE_INSTALL_LIBDIR}/${PYTHON_DIR_BASENAME} +)") +install(DIRECTORY ${QT_PLUGINS_DIR}/ DESTINATION ${CMAKE_INSTALL_LIBDIR}/qtplugins) + +#files installed by homebrew do not have write permission for regular user +install(CODE "execute_process(COMMAND chmod -R a+w ${CMAKE_INSTALL_LIBDIR})") + +get_filename_component(APP_PATH ${CMAKE_INSTALL_PREFIX} PATH) + +install(CODE + "message(STATUS \"Making bundle relocatable...\") + execute_process( + COMMAND ${PYTHON_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/src/Tools/MakeMacBundleRelocatable.py ${APP_PATH} + )" +) diff --git a/src/MacAppBundle/FreeCAD.app/Contents/Info.plist b/src/MacAppBundle/FreeCAD.app/Contents/Info.plist new file mode 100644 index 000000000000..747d3002c6f4 --- /dev/null +++ b/src/MacAppBundle/FreeCAD.app/Contents/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + FreeCAD + CFBundleGetInfoString + + CFBundleIconFile + freecad.icns + CFBundleIdentifier + + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + + CFBundleName + FreeCAD + CFBundlePackageType + APPL + CFBundleShortVersionString + + CFBundleSignature + ???? + CFBundleVersion + 0.14-dev + CSResourcesFileMapped + + NSHumanReadableCopyright + + NSPrincipalClass + NSApplication + NSHighResolutionCapable + True + + diff --git a/src/MacAppBundle/FreeCAD.app/Contents/MacOS/FreeCAD b/src/MacAppBundle/FreeCAD.app/Contents/MacOS/FreeCAD new file mode 120000 index 000000000000..98ca77adaad3 --- /dev/null +++ b/src/MacAppBundle/FreeCAD.app/Contents/MacOS/FreeCAD @@ -0,0 +1 @@ +../bin/FreeCAD \ No newline at end of file diff --git a/src/MacAppBundle/FreeCAD.app/Contents/Resources/freecad.icns b/src/MacAppBundle/FreeCAD.app/Contents/Resources/freecad.icns new file mode 100644 index 000000000000..3a4a6a78174f Binary files /dev/null and b/src/MacAppBundle/FreeCAD.app/Contents/Resources/freecad.icns differ diff --git a/src/MacAppBundle/FreeCAD.app/Contents/Resources/qt.conf b/src/MacAppBundle/FreeCAD.app/Contents/Resources/qt.conf new file mode 100644 index 000000000000..444b6d72450d --- /dev/null +++ b/src/MacAppBundle/FreeCAD.app/Contents/Resources/qt.conf @@ -0,0 +1,2 @@ +[Paths] +Plugins=lib/qtplugins diff --git a/src/Tools/MakeMacBundleRelocatable.py b/src/Tools/MakeMacBundleRelocatable.py new file mode 100644 index 000000000000..de863c901ac7 --- /dev/null +++ b/src/Tools/MakeMacBundleRelocatable.py @@ -0,0 +1,286 @@ +import os +import sys +import subprocess +import pprint + +SYS_PATHS = ["/System/","/usr/lib/"] + +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 self.graph.has_key(name): + 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 = subprocess.check_output(["file",path]) + if output.count("Mach-O") != 0: + return True + + return False + +def is_system_lib(lib): + for p in SYS_PATHS: + if lib.startswith(p): + 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 = subprocess.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 absolte path install name + if not path: + path = install_path + + if not path: + raise LibraryNotFound("{0} not found in given paths".format(lib_name)) + + 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) + #print(path) + #print(pf) + #print(os.path.split(pf)[0] == os.path.split(path)[0]) + #print('----') + 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): + subprocess.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 + subprocess.check_call(["chmod", "a+w", os.path.join(bundle_path,"lib",node.name)]) + +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 + subprocess.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 path in rpaths: + subprocess.call(["install_name_tool", "-add_rpath", path, lib]) + +def print_node(graph, node): + print(os.path.join(node.path, node.name)) + +def main(): + 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", "/usr/local/lib", "/usr/local/Cellar/freetype/2.5.4/lib"] + + build_deps_graph(graph, bundle_path, dir_filter, search_paths) + + #graph.visit(print_node) + graph.visit(copy_into_bundle, [bundle_path]) + graph.visit(add_rpaths, [bundle_path]) + +if __name__ == "__main__": + main()