From 38816bf51da004e85ab75e9aa7e884df653bf508 Mon Sep 17 00:00:00 2001 From: Michael Maltese Date: Fri, 3 Feb 2017 16:10:59 -0800 Subject: [PATCH] CMake: use BundleUtilities to fix up Dolphin.app --- CMakeLists.txt | 3 - CMakeTests/DolphinPostprocessBundle.cmake | 46 ++++++ Source/Core/DolphinQt2/CMakeLists.txt | 19 ++- Source/Core/DolphinQt2/qt.conf | 0 Source/Core/DolphinWX/CMakeLists.txt | 7 +- Tools/deploy-mac.py | 162 ---------------------- 6 files changed, 61 insertions(+), 176 deletions(-) create mode 100644 CMakeTests/DolphinPostprocessBundle.cmake create mode 100644 Source/Core/DolphinQt2/qt.conf delete mode 100755 Tools/deploy-mac.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a340db313..c3a2fcae8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -255,9 +255,6 @@ else() endif() if(CMAKE_SYSTEM_NAME MATCHES "Darwin") - # This doesn't play well with the packaging script that doesn't understand @rpath - set(CMAKE_MACOSX_RPATH OFF) - if(NOT OSX_USE_DEFAULT_SEARCH_PATH) # Hack up the path to prioritize the path to built-in OS libraries to # increase the chance of not depending on a bunch of copies of them diff --git a/CMakeTests/DolphinPostprocessBundle.cmake b/CMakeTests/DolphinPostprocessBundle.cmake new file mode 100644 index 0000000000..4d2eee175e --- /dev/null +++ b/CMakeTests/DolphinPostprocessBundle.cmake @@ -0,0 +1,46 @@ +# This module can be used in two different ways. +# +# When invoked as `cmake -P DolphinPostprocessBundle.cmake`, it fixes up an +# application folder to be standalone. It bundles all required libraries from +# the system and fixes up library IDs. Any additional shared libraries, like +# plugins, that are found under Contents/MacOS/ will be made standalone as well. +# +# When called with `include(DolphinPostprocessBundle)`, it defines a helper +# function `dolphin_postprocess_bundle` that sets up the command form of the +# module as a post-build step. + +if(CMAKE_GENERATOR) + # Being called as include(DolphinPostprocessBundle), so define a helper function. + set(_DOLPHIN_POSTPROCESS_BUNDLE_MODULE_LOCATION "${CMAKE_CURRENT_LIST_FILE}") + function(dolphin_postprocess_bundle target) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -DDOLPHIN_BUNDLE_PATH="$/../.." + -P "${_DOLPHIN_POSTPROCESS_BUNDLE_MODULE_LOCATION}" + ) + endfunction() + return() +endif() + +get_filename_component(DOLPHIN_BUNDLE_PATH "${DOLPHIN_BUNDLE_PATH}" REALPATH) +message(STATUS "Fixing up application bundle: ${DOLPHIN_BUNDLE_PATH}") + +# Make sure to fix up any additional shared libraries (like plugins) that are +# needed. +file(GLOB_RECURSE extra_libs "${DOLPHIN_BUNDLE_PATH}/Contents/MacOS/*.dylib") + +# BundleUtilities doesn't support DYLD_FALLBACK_LIBRARY_PATH behavior, which +# makes it sometimes break on libraries that do weird things with @rpath. Specify +# equivalent search directories until https://gitlab.kitware.com/cmake/cmake/issues/16625 +# is fixed and in our minimum CMake version. +set(extra_dirs "/usr/local/lib" "/lib" "/usr/lib") + +# BundleUtilities is overly verbose, so disable most of its messages +function(message) + if(NOT ARGV MATCHES "^STATUS;") + _message(${ARGV}) + endif() +endfunction() + +include(BundleUtilities) +set(BU_CHMOD_BUNDLE_ITEMS ON) +fixup_bundle("${DOLPHIN_BUNDLE_PATH}" "${extra_libs}" "${extra_dirs}") diff --git a/Source/Core/DolphinQt2/CMakeLists.txt b/Source/Core/DolphinQt2/CMakeLists.txt index feb0bb0cf4..877d1ff02d 100644 --- a/Source/Core/DolphinQt2/CMakeLists.txt +++ b/Source/Core/DolphinQt2/CMakeLists.txt @@ -49,13 +49,16 @@ if(APPLE) MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in ) - # Update library references to make the bundle portable - add_custom_command(TARGET ${DOLPHINQT2_BINARY} POST_BUILD - COMMAND echo "Fixing up application bundle: ${BUNDLE_PATH}" - COMMAND echo ${CMAKE_SOURCE_DIR}/Tools/deploy-mac.py $/../.. - ) + # Copy qt.conf into the bundle + target_sources(${DOLPHINQT2_BINARY} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/qt.conf") + set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/qt.conf" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - # Copy resources in the bundle + # Copy Qt plugins into the bundle + get_target_property(qtcocoa_location Qt5::QCocoaIntegrationPlugin LOCATION) + target_sources(${DOLPHINQT2_BINARY} PRIVATE "${qtcocoa_location}") + set_source_files_properties("${qtcocoa_location}" PROPERTIES MACOSX_PACKAGE_LOCATION MacOS/platforms) + + # Copy resources into the bundle file(GLOB_RECURSE resources RELATIVE "${CMAKE_SOURCE_DIR}/Data" "${CMAKE_SOURCE_DIR}/Data/Sys/*") foreach(res ${resources}) target_sources(${DOLPHINQT2_BINARY} PRIVATE "${CMAKE_SOURCE_DIR}/Data/${res}") @@ -64,6 +67,10 @@ if(APPLE) MACOSX_PACKAGE_LOCATION "Resources/${resdir}") source_group("Resources" FILES "${CMAKE_SOURCE_DIR}/Data/${res}") endforeach() + + # Update library references to make the bundle portable + include(DolphinPostprocessBundle) + dolphin_postprocess_bundle(${DOLPHINQT2_BINARY}) else() install(TARGETS ${DOLPHINQT2_BINARY} RUNTIME DESTINATION ${bindir}) endif() diff --git a/Source/Core/DolphinQt2/qt.conf b/Source/Core/DolphinQt2/qt.conf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Source/Core/DolphinWX/CMakeLists.txt b/Source/Core/DolphinWX/CMakeLists.txt index 1d4b958b04..020b7bbd59 100644 --- a/Source/Core/DolphinWX/CMakeLists.txt +++ b/Source/Core/DolphinWX/CMakeLists.txt @@ -176,11 +176,8 @@ if(wxWidgets_FOUND) endforeach() # Update library references to make the bundle portable - add_custom_command(TARGET ${DOLPHIN_EXE} POST_BUILD - COMMAND echo "Fixing up application bundle: ${BUNDLE_PATH}" - COMMAND ${CMAKE_SOURCE_DIR}/Tools/deploy-mac.py $/../.. - ) - + include(DolphinPostprocessBundle) + dolphin_postprocess_bundle(${DOLPHIN_EXE}) # Install bundle into systemwide /Applications directory. install(TARGETS ${DOLPHIN_EXE} DESTINATION /Applications) diff --git a/Tools/deploy-mac.py b/Tools/deploy-mac.py deleted file mode 100755 index 674b663525..0000000000 --- a/Tools/deploy-mac.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/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 - oldPath = os.path.realpath(joinPath(split)) - 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)) - newPath = None - 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)) - args = [installNameTool] - for path, oldExecPath, newExecPath in toUpdate: - if path != bin: - if path: - updateMachO(path, execPath, root) - if verbose: - print('Updating Mach-O load from {} to {}...'.format(oldExecPath, newExecPath)) - args.extend(['-change', oldExecPath, newExecPath]) - else: - if verbose: - print('Updating Mach-O id from {} to {}...'.format(oldExecPath, newExecPath)) - args.extend(['-id', newExecPath]) - args.append(bin) - - # Check we actually have to update some paths - if len(args) > 2: - subprocess.check_call(args) - -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 - - 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: - 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))