CMake: Make bundle building on MacOS optional

Also makes Xcode build without needing to copy/symlink resources.
pull/3794/head
Stenzek 2 weeks ago
parent 17a721c8f7
commit 1f065b5eae
No known key found for this signature in database

@ -59,7 +59,7 @@ jobs:
mkdir build mkdir build
cd build cd build
export MACOSX_DEPLOYMENT_TARGET=13.3 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 cmake --build . --parallel
mv bin/DuckStation.app . mv bin/DuckStation.app .
codesign -s - --deep -f -v DuckStation.app codesign -s - --deep -f -v DuckStation.app

@ -46,7 +46,7 @@ function(copy_base_translations target)
endif() endif()
target_sources(${target} PRIVATE ${path}) target_sources(${target} PRIVATE ${path})
if(APPLE) if(BUILD_MACOS_BUNDLE)
set_source_files_properties(${path} PROPERTIES MACOSX_PACKAGE_LOCATION Resources/translations) set_source_files_properties(${path} PROPERTIES MACOSX_PACKAGE_LOCATION Resources/translations)
else() else()
add_custom_command(TARGET ${target} POST_BUILD add_custom_command(TARGET ${target} POST_BUILD

@ -16,7 +16,13 @@ if(LINUX OR BSD)
option(ENABLE_WAYLAND "Support Wayland window system" ON) option(ENABLE_WAYLAND "Support Wayland window system" ON)
endif() endif()
if(APPLE) if(APPLE)
option(BUILD_MACOS_BUNDLE "Build MacOS application bundles" OFF)
option(SKIP_POSTPROCESS_BUNDLE "Disable bundle post-processing, including Qt additions" 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() endif()
# Set _DEBUG macro for Debug builds. # Set _DEBUG macro for Debug builds.

@ -314,17 +314,28 @@ if(APPLE)
) )
endforeach() 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( add_custom_command(
OUTPUT ${metallib_file} OUTPUT ${metallib_path}
COMMAND xcrun metallib -o ${metallib_file} ${air_files} COMMAND xcrun metallib -o ${metallib_path} ${air_files}
DEPENDS ${air_files} DEPENDS ${air_files}
COMMENT "Linking Metal library ${library_name}.metallib" COMMENT "Linking Metal library ${metallib_filename}"
) )
target_sources(${target} PRIVATE ${metallib_file}) target_sources(${target} PRIVATE ${metallib_path})
set_source_files_properties(${metallib_file} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) if(BUILD_MACOS_BUNDLE)
set_source_files_properties(${metallib_path} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
else()
set(RESOURCES_DIRECTORY "$<TARGET_FILE_DIR:${target}>/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() endfunction()
endif() endif()
@ -344,7 +355,7 @@ function(add_resources TARGET DEST_SUBDIR SOURCE_DIR)
# Get the subdirectory portion (if any) # Get the subdirectory portion (if any)
get_filename_component(REL_SUBDIR "${REL_PATH}" DIRECTORY) get_filename_component(REL_SUBDIR "${REL_PATH}" DIRECTORY)
if(APPLE) if(BUILD_MACOS_BUNDLE)
# On macOS, add as source with MACOSX_PACKAGE_LOCATION # On macOS, add as source with MACOSX_PACKAGE_LOCATION
target_sources(${TARGET} PRIVATE "${SOURCE_FILE}") target_sources(${TARGET} PRIVATE "${SOURCE_FILE}")
if(REL_SUBDIR) if(REL_SUBDIR)
@ -391,7 +402,7 @@ function(add_runtime_libraries TARGET)
get_filename_component(dyn_lib_dir "${dyn_lib_path}" DIRECTORY) 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. # For normal macOS bundle generators, put the dylib into Contents/Frameworks.
message(STATUS "Bundling imported library ${dyn_lib_soname}") message(STATUS "Bundling imported library ${dyn_lib_soname}")
target_sources(${TARGET} PRIVATE "${dyn_lib_path}") target_sources(${TARGET} PRIVATE "${dyn_lib_path}")

@ -47,6 +47,8 @@
#ifdef _WIN32 #ifdef _WIN32
#include "common/windows_headers.h" #include "common/windows_headers.h"
#include <ShlObj.h> #include <ShlObj.h>
#elifdef __APPLE__
#include "common/cocoa_tools.h"
#endif #endif
LOG_CHANNEL(Core); LOG_CHANNEL(Core);
@ -56,7 +58,7 @@ namespace Core {
/// Use two async worker threads, should be enough for most tasks. /// Use two async worker threads, should be enough for most tasks.
static constexpr u32 NUM_ASYNC_WORKER_THREADS = 2; 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 bool SetDataRoot(Error* error);
static void SetDefaultSettings(SettingsInterface& si, bool host, bool system, bool controller, bool ignore_user_prefs); 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 } // 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; return false;
if (!SetDataRoot(error)) if (!SetDataRoot(error))
@ -101,7 +103,7 @@ bool Core::SetCriticalFolders(const char* resources_subdir, Error* error)
return true; return true;
} }
bool Core::SetAppRootAndResources(const char* resources_subdir, Error* error) bool Core::SetAppRootAndResources(Error* error)
{ {
const std::string program_path = FileSystem::GetProgramPath(error); const std::string program_path = FileSystem::GetProgramPath(error);
if (program_path.empty()) 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)); 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__ #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<std::string> 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 #endif
if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str())) if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str()))

@ -10,7 +10,7 @@ class Error;
namespace Core { namespace Core {
/// Based on the current configuration, determines what the data directory is. /// 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. /// Returns the path to the configuration file.
/// We split this out so it can be retrieved by the host for error message purposes, /// We split this out so it can be retrieved by the host for error message purposes,

@ -248,43 +248,46 @@ if(APPLE)
target_link_libraries(duckstation-qt PRIVATE ${COREGRAPHICS_LIBRARY}) target_link_libraries(duckstation-qt PRIVATE ${COREGRAPHICS_LIBRARY})
# Don't generate a bundle for XCode, it makes code signing fail... # Don't generate a bundle for XCode, it makes code signing fail...
get_scm_version() if(NOT CMAKE_GENERATOR MATCHES "Xcode" AND BUILD_MACOS_BUNDLE)
set(BUNDLE_PATH ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/DuckStation.app) get_scm_version()
set_target_properties(duckstation-qt PROPERTIES set(BUNDLE_PATH ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/DuckStation.app)
MACOSX_BUNDLE true set_target_properties(duckstation-qt PROPERTIES
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in MACOSX_BUNDLE true
OUTPUT_NAME DuckStation 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 set_target_properties(duckstation-qt PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "" XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY ""
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED NO XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED NO
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED 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() 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() endif()
# Compile qrc to a binary file. # Compile qrc to a binary file.
if(NOT APPLE) if(NOT BUILD_MACOS_BUNDLE)
set(RCC_FILE "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/resources/duckstation-qt.rcc") set(RCC_FILENAME "duckstation-qt.rcc")
qt_add_binary_resources(duckstation-qt-rcc resources/duckstation-qt.qrc DESTINATION ${RCC_FILE} OPTIONS -no-compress) set(RCC_BUILD_PATH "${CMAKE_CURRENT_BINARY_DIR}/${RCC_FILENAME}")
set(RCC_OUTPUT_DIR "$<TARGET_FILE_DIR:duckstation-qt>/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) 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_command(TARGET duckstation-qt POST_BUILD COMMAND "${CMAKE_COMMAND}" -E make_directory "${RCC_OUTPUT_DIR}")
add_custom_target(duckstation-qt-rcc-mkdir COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/resources") add_custom_command(TARGET duckstation-qt POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${RCC_BUILD_PATH}" "${RCC_OUTPUT_DIR}/${RCC_FILENAME}")
add_dependencies(duckstation-qt-rcc duckstation-qt-rcc-mkdir)
else() else()
set(RCC_FILE "${CMAKE_CURRENT_BINARY_DIR}/duckstation-qt.rcc") 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) qt_add_binary_resources(duckstation-qt-rcc resources/duckstation-qt.qrc DESTINATION ${RCC_FILE} OPTIONS -no-compress)
@ -296,7 +299,7 @@ endif()
# Translation setup. # Translation setup.
qt_add_lrelease(duckstation-qt TS_FILES ${TS_FILES} QM_FILES_OUTPUT_VARIABLE QM_FILES) 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 "$<TARGET_FILE_DIR:duckstation-qt>/translations") set(QM_OUTPUT_DIR "$<TARGET_FILE_DIR:duckstation-qt>/translations")
add_custom_command(TARGET duckstation-qt POST_BUILD COMMAND "${CMAKE_COMMAND}" -E make_directory "${QM_OUTPUT_DIR}") add_custom_command(TARGET duckstation-qt POST_BUILD COMMAND "${CMAKE_COMMAND}" -E make_directory "${QM_OUTPUT_DIR}")
foreach (QM_FILE IN LISTS QM_FILES) foreach (QM_FILE IN LISTS QM_FILES)

@ -80,6 +80,7 @@
#include "common/windows_headers.h" #include "common/windows_headers.h"
#include <objbase.h> // CoInitializeEx #include <objbase.h> // CoInitializeEx
#elif defined(__APPLE__) #elif defined(__APPLE__)
#include "common/cocoa_tools.h"
#include <unistd.h> #include <unistd.h>
#endif #endif
@ -847,16 +848,7 @@ void QtHost::DownloadFile(QWidget* parent, std::string url, std::string path,
bool QtHost::InitializeFoldersAndConfig(Error* error) bool QtHost::InitializeFoldersAndConfig(Error* error)
{ {
// Path to the resources directory relative to the application binary. if (!Core::SetCriticalFolders(error))
// 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))
return false; return false;
Error config_error; Error config_error;
@ -2503,7 +2495,11 @@ void QtHost::UpdateApplicationLanguage(QWidget* dialog_parent)
#ifndef __APPLE__ #ifndef __APPLE__
const QString base_dir = QStringLiteral("%1/translations").arg(qApp->applicationDirPath()); const QString base_dir = QStringLiteral("%1/translations").arg(qApp->applicationDirPath());
#else #else
const QString base_dir = QStringLiteral("%1/../Resources/translations").arg(qApp->applicationDirPath()); QString base_dir;
if (const std::optional<std::string> 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 #endif
// Qt base uses underscores instead of hyphens. // Qt base uses underscores instead of hyphens.

@ -82,7 +82,7 @@ static std::string s_dump_base_directory;
bool RegTestHost::InitializeFoldersAndConfig(Error* error) bool RegTestHost::InitializeFoldersAndConfig(Error* error)
{ {
if (!Core::SetCriticalFolders("resources", error)) if (!Core::SetCriticalFolders(error))
return false; return false;
if (!Core::InitializeBaseSettingsLayer({}, error)) if (!Core::InitializeBaseSettingsLayer({}, error))

@ -24,7 +24,7 @@ if(APPLE)
find_library(COCOA_LIBRARY Cocoa REQUIRED) find_library(COCOA_LIBRARY Cocoa REQUIRED)
target_link_libraries(updater PRIVATE ${COCOA_LIBRARY}) 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 OUTPUT_NAME "Updater")
set_target_properties(updater PROPERTIES set_target_properties(updater PROPERTIES
MACOSX_BUNDLE true MACOSX_BUNDLE true

@ -268,27 +268,29 @@ function(add_util_resources target)
get_property(UTIL_METAL_SOURCES GLOBAL PROPERTY UTIL_METAL_SOURCES) get_property(UTIL_METAL_SOURCES GLOBAL PROPERTY UTIL_METAL_SOURCES)
add_metal_sources(${target} ${UTIL_METAL_SOURCES} metal_shaders macos-metal2.3) add_metal_sources(${target} ${UTIL_METAL_SOURCES} metal_shaders macos-metal2.3)
# Copy MoltenVK into the bundle if(BUILD_MACOS_BUNDLE)
unset(MOLTENVK_PATH CACHE) # Copy MoltenVK into the bundle
find_file(MOLTENVK_PATH NAMES unset(MOLTENVK_PATH CACHE)
libMoltenVK.dylib find_file(MOLTENVK_PATH NAMES
lib/libMoltenVK.dylib libMoltenVK.dylib
) lib/libMoltenVK.dylib
if (MOLTENVK_PATH) )
target_sources(${target} PRIVATE "${MOLTENVK_PATH}") if (MOLTENVK_PATH)
set_source_files_properties("${MOLTENVK_PATH}" PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks) target_sources(${target} PRIVATE "${MOLTENVK_PATH}")
message(STATUS "Using MoltenVK from ${MOLTENVK_PATH}") set_source_files_properties("${MOLTENVK_PATH}" PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks)
else() message(STATUS "Using MoltenVK from ${MOLTENVK_PATH}")
message(WARNING "MoltenVK not found in path, it will depend on the target system having it.") else()
endif() message(WARNING "MoltenVK not found in path, it will depend on the target system having it.")
endif()
# Copy ffmpeg into the bundle. # Copy ffmpeg into the bundle.
foreach(component avcodec avformat avutil swresample swscale) foreach(component avcodec avformat avutil swresample swscale)
string(REGEX REPLACE "\([0-9]+\)\.[0-9]+\.[0-9]+" "\\1" major "${FFMPEG_${component}_VERSION}") 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}") string(REPLACE "lib${component}.dylib" "lib${component}.${major}.dylib" version_lib "${FFMPEG_${component}_LIBRARIES}")
target_sources(${target} PRIVATE ${version_lib}) target_sources(${target} PRIVATE ${version_lib})
set_source_files_properties(${target} PRIVATE ${version_lib} PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks) set_source_files_properties(${target} PRIVATE ${version_lib} PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks)
endforeach() endforeach()
endif()
endif() endif()
# Copy dynamically-loaded libraries into the bundle. # Copy dynamically-loaded libraries into the bundle.

Loading…
Cancel
Save