From c1c472958ea0fefecc280f68e512b633dd21d521 Mon Sep 17 00:00:00 2001 From: Jeffrey Pfau Date: Sat, 6 Jun 2015 00:56:33 -0700 Subject: [PATCH 1/3] Tools: Use non-CMake tool for fixing up OS X bundles --- Source/Core/DolphinQt/CMakeLists.txt | 2 +- Source/Core/DolphinWX/CMakeLists.txt | 2 +- Tools/deploy-mac.py | 172 +++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100755 Tools/deploy-mac.py diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 8d12e4d5f4..fdd8325fa7 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -66,7 +66,7 @@ if(APPLE) message(\"(Note: This is only necessary to produce a redistributable binary.\") message(\"To skip, pass -DSKIP_POSTPROCESS_BUNDLE=1 to cmake.)\") set(BU_CHMOD_BUNDLE_ITEMS ON) - fixup_bundle(\"${BUNDLE_PATH}\" \"\" \"\") + execute_process(COMMAND ${CMAKE_SOURCE_DIR}/Tools/deploy-mac.py -p platforms/libqcocoa.dylib \"${BUNDLE_PATH}\") file(INSTALL ${CMAKE_SOURCE_DIR}/Data/Sys DESTINATION ${BUNDLE_PATH}/Contents/Resources ) diff --git a/Source/Core/DolphinWX/CMakeLists.txt b/Source/Core/DolphinWX/CMakeLists.txt index 275a68f426..93c5c71829 100644 --- a/Source/Core/DolphinWX/CMakeLists.txt +++ b/Source/Core/DolphinWX/CMakeLists.txt @@ -158,7 +158,7 @@ elseif(wxWidgets_FOUND) message(\"(Note: This is only necessary to produce a redistributable binary.\") message(\"To skip, pass -DSKIP_POSTPROCESS_BUNDLE=1 to cmake.)\") set(BU_CHMOD_BUNDLE_ITEMS ON) - fixup_bundle(\"${BUNDLE_PATH}\" \"\" \"\") + execute_process(COMMAND ${CMAKE_SOURCE_DIR}/Tools/deploy-mac.py \"${BUNDLE_PATH}\") file(INSTALL \"${CMAKE_SOURCE_DIR}/Data/Sys\" DESTINATION \"${BUNDLE_PATH}/Contents/Resources\" ) diff --git a/Tools/deploy-mac.py b/Tools/deploy-mac.py new file mode 100755 index 0000000000..d566fe7c85 --- /dev/null +++ b/Tools/deploy-mac.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +from __future__ import print_function +import argparse +import errno +import os +import re +import shutil +import subprocess + +qtPath = None +verbose = False + +def splitPath(path): + folders = [] + while True: + path, folder = os.path.split(path) + if folder != '': + folders.append(folder) + else: + if path != '': + folders.append(path) + break + folders.reverse() + return folders + +def joinPath(path): + return reduce(os.path.join, path, '') + +def findFramework(path): + child = [] + while path and not path[-1].endswith('.framework'): + child.append(path.pop()) + child.reverse() + return path, child + +def findQtPath(path): + parent, child = findFramework(splitPath(path)) + return joinPath(parent[:-2]) + +def makedirs(path): + split = splitPath(path) + accum = [] + split.reverse() + while split: + accum.append(split.pop()) + newPath = joinPath(accum) + if newPath == '/': + continue + try: + os.mkdir(newPath) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + +def parseOtoolLine(line, execPath, root): + if not line.startswith('\t'): + return None, None, None, None + line = line[1:] + match = re.match('([@/].*) \(compatibility version.*\)', line) + path = match.group(1) + split = splitPath(path) + newExecPath = ['@executable_path', '..', 'Frameworks'] + newPath = execPath[:-1] + newPath.append('Frameworks') + if split[:3] == ['/', 'usr', 'lib'] or split[:2] == ['/', 'System']: + return None, None, None, None + if split[0] == '@executable_path': + split[:1] = execPath + if split[0] == '/' and not os.access(joinPath(split), os.F_OK): + split[:1] = root + try: + oldPath = joinPath(split) + while True: + linkPath = os.readlink(os.path.abspath(oldPath)) + oldPath = os.path.join(os.path.dirname(oldPath), linkPath) + except OSError as e: + if e.errno != errno.EINVAL: + raise + split = splitPath(oldPath) + isFramework = False + if not split[-1].endswith('.dylib'): + isFramework = True + split, framework = findFramework(split) + newPath.append(split[-1]) + newExecPath.append(split[-1]) + if isFramework: + newPath.extend(framework) + newExecPath.extend(framework) + split.extend(framework) + newPath = joinPath(newPath) + newExecPath = joinPath(newExecPath) + return joinPath(split), newPath, path, newExecPath + +def updateMachO(bin, execPath, root): + global qtPath + otoolOutput = subprocess.check_output([otool, '-L', bin]) + toUpdate = [] + for line in otoolOutput.split('\n'): + oldPath, newPath, oldExecPath, newExecPath = parseOtoolLine(line, execPath, root) + if not newPath: + continue + if os.access(newPath, os.F_OK): + if verbose: + print('Skipping copying {}, already done.'.format(oldPath)) + elif os.path.abspath(oldPath) != os.path.abspath(newPath): + if verbose: + print('Copying {} to {}...'.format(oldPath, newPath)) + parent, child = os.path.split(newPath) + makedirs(parent) + shutil.copy2(oldPath, newPath) + os.chmod(newPath, 0o644) + toUpdate.append((newPath, oldExecPath, newExecPath)) + if not qtPath and 'Qt' in oldPath: + qtPath = findQtPath(oldPath) + if verbose: + print('Found Qt path at {}.'.format(qtPath)) + for path, oldExecPath, newExecPath in toUpdate: + if path != bin: + updateMachO(path, execPath, root) + if verbose: + print('Updating Mach-O load from {} to {}...'.format(oldExecPath, newExecPath)) + subprocess.check_call([installNameTool, '-change', oldExecPath, newExecPath, bin]) + else: + if verbose: + print('Updating Mach-O id from {} to {}...'.format(oldExecPath, newExecPath)) + subprocess.check_call([installNameTool, '-id', newExecPath, bin]) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-R', '--root', metavar='ROOT', default='/', help='root directory to search') + parser.add_argument('-I', '--install-name-tool', metavar='INSTALL_NAME_TOOL', default='install_name_tool', help='path to install_name_tool') + parser.add_argument('-O', '--otool', metavar='OTOOL', default='otool', help='path to otool') + parser.add_argument('-p', '--qt-plugins', metavar='PLUGINS', default='', help='Qt plugins to include (comma-separated)') + parser.add_argument('-v', '--verbose', action='store_true', default=False, help='output more information') + parser.add_argument('bundle', help='application bundle to deploy') + args = parser.parse_args() + + otool = args.otool + installNameTool = args.install_name_tool + verbose = args.verbose + + try: + shutil.rmtree(os.path.join(args.bundle, 'Contents/Frameworks/')) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + for executable in os.listdir(os.path.join(args.bundle, 'Contents/MacOS')): + if executable.endswith('.dSYM'): + continue + fullPath = os.path.join(args.bundle, 'Contents/MacOS/', executable) + updateMachO(fullPath, splitPath(os.path.join(args.bundle, 'Contents/MacOS')), splitPath(args.root)) + if args.qt_plugins: + try: + shutil.rmtree(os.path.join(args.bundle, 'Contents/PlugIns/')) + except OSError as e: + if e.errno != errno.ENOENT: + raise + makedirs(os.path.join(args.bundle, 'Contents/PlugIns')) + makedirs(os.path.join(args.bundle, 'Contents/Resources')) + with open(os.path.join(args.bundle, 'Contents/Resources/qt.conf'), 'w') as conf: + conf.write('[Paths]\nPlugins = PlugIns\n') + plugins = args.qt_plugins.split(',') + for plugin in plugins: + plugin = plugin.strip() + kind, plug = os.path.split(plugin) + newDir = os.path.join(args.bundle, 'Contents/PlugIns/', kind) + makedirs(newDir) + newPath = os.path.join(newDir, plug) + shutil.copy2(os.path.join(qtPath, 'plugins', plugin), newPath) + updateMachO(newPath, splitPath(os.path.join(args.bundle, 'Contents/MacOS')), splitPath(args.root)) From 14dbe7e6dbf1eb8f2a04a1148c14fafd75f21872 Mon Sep 17 00:00:00 2001 From: Jeffrey Pfau Date: Sat, 6 Jun 2015 12:45:40 -0700 Subject: [PATCH 2/3] Tools: Clean up deploy-mac script symlink resolution --- Tools/deploy-mac.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Tools/deploy-mac.py b/Tools/deploy-mac.py index d566fe7c85..9009c79d1c 100755 --- a/Tools/deploy-mac.py +++ b/Tools/deploy-mac.py @@ -69,15 +69,8 @@ def parseOtoolLine(line, execPath, root): split[:1] = execPath if split[0] == '/' and not os.access(joinPath(split), os.F_OK): split[:1] = root - try: - oldPath = joinPath(split) - while True: - linkPath = os.readlink(os.path.abspath(oldPath)) - oldPath = os.path.join(os.path.dirname(oldPath), linkPath) - except OSError as e: - if e.errno != errno.EINVAL: - raise - split = splitPath(oldPath) + oldPath = os.path.realpath(joinPath(split)) + split = splitPath(oldPath) isFramework = False if not split[-1].endswith('.dylib'): isFramework = True From a96759d9e990c67e58269202158169d8f2f5ea4f Mon Sep 17 00:00:00 2001 From: Jeffrey Pfau Date: Sat, 6 Jun 2015 12:51:47 -0700 Subject: [PATCH 3/3] Tools: Collapse install_name_tool calls in deploy-mac --- Tools/deploy-mac.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tools/deploy-mac.py b/Tools/deploy-mac.py index 9009c79d1c..8305a43046 100755 --- a/Tools/deploy-mac.py +++ b/Tools/deploy-mac.py @@ -108,16 +108,19 @@ def updateMachO(bin, execPath, root): qtPath = findQtPath(oldPath) if verbose: print('Found Qt path at {}.'.format(qtPath)) + args = [installNameTool] for path, oldExecPath, newExecPath in toUpdate: if path != bin: updateMachO(path, execPath, root) if verbose: print('Updating Mach-O load from {} to {}...'.format(oldExecPath, newExecPath)) - subprocess.check_call([installNameTool, '-change', oldExecPath, newExecPath, bin]) + args.extend(['-change', oldExecPath, newExecPath]) else: if verbose: print('Updating Mach-O id from {} to {}...'.format(oldExecPath, newExecPath)) - subprocess.check_call([installNameTool, '-id', newExecPath, bin]) + args.extend(['-id', newExecPath]) + args.append(bin) + subprocess.check_call(args) if __name__ == '__main__': parser = argparse.ArgumentParser()