From 1f065b5eae4b5817a447f055118fbe6ca8ecacb4 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 6 Sep 2026 16:39:35 +1000 Subject: [PATCH] CMake: Make bundle building on MacOS optional Also makes Xcode build without needing to copy/symlink resources. --- .github/workflows/macos-build.yml | 2 +- CMakeModules/CopyBaseTranslations.cmake | 2 +- CMakeModules/DuckStationBuildOptions.cmake | 6 +++ CMakeModules/DuckStationUtils.cmake | 27 +++++++---- src/core/core.cpp | 21 ++++++--- src/core/core_private.h | 2 +- src/duckstation-qt/CMakeLists.txt | 55 ++++++++++++---------- src/duckstation-qt/qthost.cpp | 18 +++---- src/duckstation-regtest/regtest_host.cpp | 2 +- src/updater/CMakeLists.txt | 2 +- src/util/CMakeLists.txt | 42 +++++++++-------- 11 files changed, 102 insertions(+), 77 deletions(-) diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index 1e821fced..446e65c08 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -59,7 +59,7 @@ jobs: mkdir build cd build export MACOSX_DEPLOYMENT_TARGET=13.3 - cmake -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DCMAKE_BUILD_TYPE=Release -DENABLE_OPENGL=OFF -DCMAKE_PREFIX_PATH="$HOME/deps" -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -G Ninja .. + cmake -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DCMAKE_BUILD_TYPE=Release -DBUILD_MACOS_BUNDLE=ON -DENABLE_OPENGL=OFF -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -G Ninja .. cmake --build . --parallel mv bin/DuckStation.app . codesign -s - --deep -f -v DuckStation.app diff --git a/CMakeModules/CopyBaseTranslations.cmake b/CMakeModules/CopyBaseTranslations.cmake index bb00d2090..04bcdab70 100644 --- a/CMakeModules/CopyBaseTranslations.cmake +++ b/CMakeModules/CopyBaseTranslations.cmake @@ -46,7 +46,7 @@ function(copy_base_translations target) endif() target_sources(${target} PRIVATE ${path}) - if(APPLE) + if(BUILD_MACOS_BUNDLE) set_source_files_properties(${path} PROPERTIES MACOSX_PACKAGE_LOCATION Resources/translations) else() add_custom_command(TARGET ${target} POST_BUILD diff --git a/CMakeModules/DuckStationBuildOptions.cmake b/CMakeModules/DuckStationBuildOptions.cmake index 3504d0338..905ffec1c 100644 --- a/CMakeModules/DuckStationBuildOptions.cmake +++ b/CMakeModules/DuckStationBuildOptions.cmake @@ -16,7 +16,13 @@ if(LINUX OR BSD) option(ENABLE_WAYLAND "Support Wayland window system" ON) endif() if(APPLE) + option(BUILD_MACOS_BUNDLE "Build MacOS application bundles" OFF) option(SKIP_POSTPROCESS_BUNDLE "Disable bundle post-processing, including Qt additions" OFF) + + # Cannot have a Xcode and bundle build for now due to code signing. + if(CMAKE_GENERATOR MATCHES "Xcode" AND BUILD_MACOS_BUNDLE) + message(FATAL_ERROR "Cannot build bundles when using the Xcode generator") + endif() endif() # Set _DEBUG macro for Debug builds. diff --git a/CMakeModules/DuckStationUtils.cmake b/CMakeModules/DuckStationUtils.cmake index e2f103da4..2bd7d72ed 100644 --- a/CMakeModules/DuckStationUtils.cmake +++ b/CMakeModules/DuckStationUtils.cmake @@ -314,17 +314,28 @@ if(APPLE) ) endforeach() - set(metallib_file ${CMAKE_CURRENT_BINARY_DIR}/${library_name}.metallib) + set(metallib_filename "${library_name}.metallib") + set(metallib_path "${CMAKE_CURRENT_BINARY_DIR}/${metallib_filename}") add_custom_command( - OUTPUT ${metallib_file} - COMMAND xcrun metallib -o ${metallib_file} ${air_files} + OUTPUT ${metallib_path} + COMMAND xcrun metallib -o ${metallib_path} ${air_files} DEPENDS ${air_files} - COMMENT "Linking Metal library ${library_name}.metallib" + COMMENT "Linking Metal library ${metallib_filename}" ) - target_sources(${target} PRIVATE ${metallib_file}) - set_source_files_properties(${metallib_file} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + target_sources(${target} PRIVATE ${metallib_path}) + if(BUILD_MACOS_BUNDLE) + set_source_files_properties(${metallib_path} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + else() + set(RESOURCES_DIRECTORY "$/resources") + add_custom_command(TARGET duckstation-qt POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E make_directory "${RESOURCES_DIRECTORY}" + ) + add_custom_command(TARGET duckstation-qt POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${metallib_path}" "${RESOURCES_DIRECTORY}/${metallib_filename}" + ) + endif() endfunction() endif() @@ -344,7 +355,7 @@ function(add_resources TARGET DEST_SUBDIR SOURCE_DIR) # Get the subdirectory portion (if any) get_filename_component(REL_SUBDIR "${REL_PATH}" DIRECTORY) - if(APPLE) + if(BUILD_MACOS_BUNDLE) # On macOS, add as source with MACOSX_PACKAGE_LOCATION target_sources(${TARGET} PRIVATE "${SOURCE_FILE}") if(REL_SUBDIR) @@ -391,7 +402,7 @@ function(add_runtime_libraries TARGET) get_filename_component(dyn_lib_dir "${dyn_lib_path}" DIRECTORY) - if(APPLE AND NOT CMAKE_GENERATOR STREQUAL "Xcode" AND NOT SKIP_POSTPROCESS_BUNDLE) + if(BUILD_MACOS_BUNDLE) # For normal macOS bundle generators, put the dylib into Contents/Frameworks. message(STATUS "Bundling imported library ${dyn_lib_soname}") target_sources(${TARGET} PRIVATE "${dyn_lib_path}") diff --git a/src/core/core.cpp b/src/core/core.cpp index c29065111..ed4248e9f 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -47,6 +47,8 @@ #ifdef _WIN32 #include "common/windows_headers.h" #include +#elifdef __APPLE__ +#include "common/cocoa_tools.h" #endif LOG_CHANNEL(Core); @@ -56,7 +58,7 @@ namespace Core { /// Use two async worker threads, should be enough for most tasks. static constexpr u32 NUM_ASYNC_WORKER_THREADS = 2; -static bool SetAppRootAndResources(const char* resources_subdir, Error* error); +static bool SetAppRootAndResources(Error* error); static bool SetDataRoot(Error* error); static void SetDefaultSettings(SettingsInterface& si, bool host, bool system, bool controller, bool ignore_user_prefs); @@ -82,9 +84,9 @@ ALIGN_TO_CACHE_LINE static CoreLocals s_locals; } // namespace Core -bool Core::SetCriticalFolders(const char* resources_subdir, Error* error) +bool Core::SetCriticalFolders(Error* error) { - if (!SetAppRootAndResources(resources_subdir, error)) + if (!SetAppRootAndResources(error)) return false; if (!SetDataRoot(error)) @@ -101,7 +103,7 @@ bool Core::SetCriticalFolders(const char* resources_subdir, Error* error) return true; } -bool Core::SetAppRootAndResources(const char* resources_subdir, Error* error) +bool Core::SetAppRootAndResources(Error* error) { const std::string program_path = FileSystem::GetProgramPath(error); if (program_path.empty()) @@ -111,10 +113,15 @@ bool Core::SetAppRootAndResources(const char* resources_subdir, Error* error) EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path)); - // MacOS resources are inside the app bundle, so canonicalize them. - EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, resources_subdir); #ifdef __APPLE__ - EmuFolders::Resources = Path::Canonicalize(EmuFolders::Resources); + // MacOS resources are inside the app bundle. We might not be running in a bundle. + const std::optional bundle_path = CocoaTools::GetBundlePath(); + if (bundle_path.has_value()) + EmuFolders::Resources = Path::Combine(bundle_path.value(), "Contents/Resources"); + else + EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources"); +#else + EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources"); #endif if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str())) diff --git a/src/core/core_private.h b/src/core/core_private.h index 734f9d883..ecd492b91 100644 --- a/src/core/core_private.h +++ b/src/core/core_private.h @@ -10,7 +10,7 @@ class Error; namespace Core { /// Based on the current configuration, determines what the data directory is. -bool SetCriticalFolders(const char* resources_subdir, Error* error); +bool SetCriticalFolders(Error* error); /// Returns the path to the configuration file. /// We split this out so it can be retrieved by the host for error message purposes, diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index fd8ee6b21..118c35ae9 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -248,43 +248,46 @@ if(APPLE) target_link_libraries(duckstation-qt PRIVATE ${COREGRAPHICS_LIBRARY}) # Don't generate a bundle for XCode, it makes code signing fail... - get_scm_version() - set(BUNDLE_PATH ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/DuckStation.app) - set_target_properties(duckstation-qt PROPERTIES - MACOSX_BUNDLE true - MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in - OUTPUT_NAME DuckStation - ) + if(NOT CMAKE_GENERATOR MATCHES "Xcode" AND BUILD_MACOS_BUNDLE) + get_scm_version() + set(BUNDLE_PATH ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/DuckStation.app) + set_target_properties(duckstation-qt PROPERTIES + MACOSX_BUNDLE true + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in + OUTPUT_NAME DuckStation + ) + + # Copy icon into the bundle + target_sources(duckstation-qt PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/DuckStation.icns") + set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/DuckStation.icns" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - if(CMAKE_GENERATOR MATCHES "Xcode") + if(NOT SKIP_POSTPROCESS_BUNDLE) + # Inject Qt Libraries into bundle. + find_program(MACDEPLOYQT_EXE macdeployqt HINTS "${QT_BINARY_DIRECTORY}") + add_custom_target(duckstation-postprocess-bundle ALL + COMMAND "${MACDEPLOYQT_EXE}" "${BUNDLE_PATH}" -no-strip + ) + add_dependencies(duckstation-postprocess-bundle duckstation-qt) + endif() + elseif(CMAKE_GENERATOR MATCHES "Xcode") set_target_properties(duckstation-qt PROPERTIES XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "" XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED NO XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED NO ) - elseif(NOT SKIP_POSTPROCESS_BUNDLE) - # Inject Qt Libraries into bundle. - find_program(MACDEPLOYQT_EXE macdeployqt HINTS "${QT_BINARY_DIRECTORY}") - add_custom_target(duckstation-postprocess-bundle ALL - COMMAND "${MACDEPLOYQT_EXE}" "${BUNDLE_PATH}" -no-strip - ) - add_dependencies(duckstation-postprocess-bundle duckstation-qt) endif() - - # Copy icon into the bundle - target_sources(duckstation-qt PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/DuckStation.icns") - set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/DuckStation.icns" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) endif() # Compile qrc to a binary file. -if(NOT APPLE) - set(RCC_FILE "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/resources/duckstation-qt.rcc") - qt_add_binary_resources(duckstation-qt-rcc resources/duckstation-qt.qrc DESTINATION ${RCC_FILE} OPTIONS -no-compress) +if(NOT BUILD_MACOS_BUNDLE) + set(RCC_FILENAME "duckstation-qt.rcc") + set(RCC_BUILD_PATH "${CMAKE_CURRENT_BINARY_DIR}/${RCC_FILENAME}") + set(RCC_OUTPUT_DIR "$/resources") + qt_add_binary_resources(duckstation-qt-rcc resources/duckstation-qt.qrc DESTINATION ${RCC_BUILD_PATH} OPTIONS -no-compress) add_dependencies(duckstation-qt duckstation-qt-rcc) - # Need to ensure the resources directory exists, it might not. Happens when low CPU count and parallel builds. - add_custom_target(duckstation-qt-rcc-mkdir COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/resources") - add_dependencies(duckstation-qt-rcc duckstation-qt-rcc-mkdir) + add_custom_command(TARGET duckstation-qt POST_BUILD COMMAND "${CMAKE_COMMAND}" -E make_directory "${RCC_OUTPUT_DIR}") + add_custom_command(TARGET duckstation-qt POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${RCC_BUILD_PATH}" "${RCC_OUTPUT_DIR}/${RCC_FILENAME}") else() set(RCC_FILE "${CMAKE_CURRENT_BINARY_DIR}/duckstation-qt.rcc") qt_add_binary_resources(duckstation-qt-rcc resources/duckstation-qt.qrc DESTINATION ${RCC_FILE} OPTIONS -no-compress) @@ -296,7 +299,7 @@ endif() # Translation setup. qt_add_lrelease(duckstation-qt TS_FILES ${TS_FILES} QM_FILES_OUTPUT_VARIABLE QM_FILES) -if(NOT APPLE) +if(NOT BUILD_MACOS_BUNDLE) set(QM_OUTPUT_DIR "$/translations") add_custom_command(TARGET duckstation-qt POST_BUILD COMMAND "${CMAKE_COMMAND}" -E make_directory "${QM_OUTPUT_DIR}") foreach (QM_FILE IN LISTS QM_FILES) diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index d2e070b11..11124dee3 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -80,6 +80,7 @@ #include "common/windows_headers.h" #include // CoInitializeEx #elif defined(__APPLE__) +#include "common/cocoa_tools.h" #include #endif @@ -847,16 +848,7 @@ void QtHost::DownloadFile(QWidget* parent, std::string url, std::string path, bool QtHost::InitializeFoldersAndConfig(Error* error) { - // Path to the resources directory relative to the application binary. -// On Windows/Linux, these are in the binary directory. -// On macOS, this is in the bundle resources directory. -#ifndef __APPLE__ - static constexpr const char* RESOURCES_RELATIVE_PATH = "resources"; -#else - static constexpr const char* RESOURCES_RELATIVE_PATH = "../Resources"; -#endif - - if (!Core::SetCriticalFolders(RESOURCES_RELATIVE_PATH, error)) + if (!Core::SetCriticalFolders(error)) return false; Error config_error; @@ -2503,7 +2495,11 @@ void QtHost::UpdateApplicationLanguage(QWidget* dialog_parent) #ifndef __APPLE__ const QString base_dir = QStringLiteral("%1/translations").arg(qApp->applicationDirPath()); #else - const QString base_dir = QStringLiteral("%1/../Resources/translations").arg(qApp->applicationDirPath()); + QString base_dir; + if (const std::optional bundle_path = CocoaTools::GetBundlePath(); bundle_path.has_value()) + base_dir = QString::fromStdString(Path::Combine(bundle_path.value(), "Contents/Resources/translations")); + else + base_dir = QStringLiteral("%1/translations").arg(qApp->applicationDirPath()); #endif // Qt base uses underscores instead of hyphens. diff --git a/src/duckstation-regtest/regtest_host.cpp b/src/duckstation-regtest/regtest_host.cpp index 61bc6b5f0..8091ae81d 100644 --- a/src/duckstation-regtest/regtest_host.cpp +++ b/src/duckstation-regtest/regtest_host.cpp @@ -82,7 +82,7 @@ static std::string s_dump_base_directory; bool RegTestHost::InitializeFoldersAndConfig(Error* error) { - if (!Core::SetCriticalFolders("resources", error)) + if (!Core::SetCriticalFolders(error)) return false; if (!Core::InitializeBaseSettingsLayer({}, error)) diff --git a/src/updater/CMakeLists.txt b/src/updater/CMakeLists.txt index af2d4ead7..b8167af0e 100644 --- a/src/updater/CMakeLists.txt +++ b/src/updater/CMakeLists.txt @@ -24,7 +24,7 @@ if(APPLE) find_library(COCOA_LIBRARY Cocoa REQUIRED) target_link_libraries(updater PRIVATE ${COCOA_LIBRARY}) - if(NOT CMAKE_GENERATOR MATCHES "Xcode" AND NOT SKIP_POSTPROCESS_BUNDLE) + if(BUILD_MACOS_BUNDLE) set_target_properties(updater PROPERTIES OUTPUT_NAME "Updater") set_target_properties(updater PROPERTIES MACOSX_BUNDLE true diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index de6f83abb..4173ad709 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -268,27 +268,29 @@ function(add_util_resources target) get_property(UTIL_METAL_SOURCES GLOBAL PROPERTY UTIL_METAL_SOURCES) add_metal_sources(${target} ${UTIL_METAL_SOURCES} metal_shaders macos-metal2.3) - # Copy MoltenVK into the bundle - unset(MOLTENVK_PATH CACHE) - find_file(MOLTENVK_PATH NAMES - libMoltenVK.dylib - lib/libMoltenVK.dylib - ) - if (MOLTENVK_PATH) - target_sources(${target} PRIVATE "${MOLTENVK_PATH}") - set_source_files_properties("${MOLTENVK_PATH}" PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks) - message(STATUS "Using MoltenVK from ${MOLTENVK_PATH}") - else() - message(WARNING "MoltenVK not found in path, it will depend on the target system having it.") - endif() + if(BUILD_MACOS_BUNDLE) + # Copy MoltenVK into the bundle + unset(MOLTENVK_PATH CACHE) + find_file(MOLTENVK_PATH NAMES + libMoltenVK.dylib + lib/libMoltenVK.dylib + ) + if (MOLTENVK_PATH) + target_sources(${target} PRIVATE "${MOLTENVK_PATH}") + set_source_files_properties("${MOLTENVK_PATH}" PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks) + message(STATUS "Using MoltenVK from ${MOLTENVK_PATH}") + else() + message(WARNING "MoltenVK not found in path, it will depend on the target system having it.") + endif() - # Copy ffmpeg into the bundle. - foreach(component avcodec avformat avutil swresample swscale) - string(REGEX REPLACE "\([0-9]+\)\.[0-9]+\.[0-9]+" "\\1" major "${FFMPEG_${component}_VERSION}") - string(REPLACE "lib${component}.dylib" "lib${component}.${major}.dylib" version_lib "${FFMPEG_${component}_LIBRARIES}") - target_sources(${target} PRIVATE ${version_lib}) - set_source_files_properties(${target} PRIVATE ${version_lib} PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks) - endforeach() + # Copy ffmpeg into the bundle. + foreach(component avcodec avformat avutil swresample swscale) + string(REGEX REPLACE "\([0-9]+\)\.[0-9]+\.[0-9]+" "\\1" major "${FFMPEG_${component}_VERSION}") + string(REPLACE "lib${component}.dylib" "lib${component}.${major}.dylib" version_lib "${FFMPEG_${component}_LIBRARIES}") + target_sources(${target} PRIVATE ${version_lib}) + set_source_files_properties(${target} PRIVATE ${version_lib} PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks) + endforeach() + endif() endif() # Copy dynamically-loaded libraries into the bundle.