From ec24d4bf4f833e46630026f60cde947e1db1e239 Mon Sep 17 00:00:00 2001 From: Ken McGaugh Date: Sun, 2 Aug 2026 10:34:55 +1200 Subject: [PATCH 1/2] fix(macos): fix macOS build issues causing crashes under Tahoe and preventing codesigning. Under macOS Tahoe, xstudio is crashing due to OpenGL compatibility issues as the build was finding libraries under /opt/X11. Forcing the build to find the system OpenGL framework fixes the crash. However vcpkg's glew port was still linking to the OpenGL libraries under /opt/X11 which prevents codesigning, so this installs a patch forcing it to also find the system framework. The xSTUDIO.app could not be codesigned due the bundle structure. This commit fixes this by putting all python and qml files under the app's Resources folder instead of Frameworks. The rpath modifications that was happing durning the post-build stage was also causing issues under Tahoe, forcing the developer to run an ad-hoc codesigning over the app before it could be launched. This commit removes any rpath modifications during build allowing the app to be launchable immediately. The rpath modifications were also causing issues with macdeployqt, requiring further unnecessary cleanup of the resulting install names and rpaths. Many thanks to David Cameron for figuring all this out. Signed-off-by: Ken McGaugh --- CMakeLists.txt | 9 ++- cmake/macros.cmake | 2 +- .../glew/fix-LNK2019.patch | 14 +++++ cmake/vcpkg_overlay_ports/glew/opengl.patch | 17 +++++ cmake/vcpkg_overlay_ports/glew/portfile.cmake | 62 +++++++++++++++++++ .../vcpkg_overlay_ports/glew/trim-build.diff | 23 +++++++ cmake/vcpkg_overlay_ports/glew/usage | 14 +++++ .../glew/vcpkg-cmake-wrapper.cmake | 25 ++++++++ cmake/vcpkg_overlay_ports/glew/vcpkg.json | 18 ++++++ extern/quickfuture/CMakeLists.txt | 4 +- extern/quickpromise/CMakeLists.txt | 12 ++-- python/CMakeLists.txt | 4 +- scripts/qt_install/CMakeLists.txt | 17 +++-- scripts/qt_install/qt_install.cmake.in | 3 - src/embedded_python/src/embedded_python.cpp | 4 +- src/launch/xstudio/src/CMakeLists.txt | 28 ++++----- src/launch/xstudio/src/Info.plist | 12 ++-- .../xstudio/src/fixup_macos_bundle.cmake | 15 ----- src/launch/xstudio/src/macdeploy.cmake.in | 26 -------- vcpkg.json | 4 ++ 20 files changed, 231 insertions(+), 82 deletions(-) create mode 100644 cmake/vcpkg_overlay_ports/glew/fix-LNK2019.patch create mode 100644 cmake/vcpkg_overlay_ports/glew/opengl.patch create mode 100644 cmake/vcpkg_overlay_ports/glew/portfile.cmake create mode 100644 cmake/vcpkg_overlay_ports/glew/trim-build.diff create mode 100644 cmake/vcpkg_overlay_ports/glew/usage create mode 100644 cmake/vcpkg_overlay_ports/glew/vcpkg-cmake-wrapper.cmake create mode 100644 cmake/vcpkg_overlay_ports/glew/vcpkg.json delete mode 100644 scripts/qt_install/qt_install.cmake.in delete mode 100644 src/launch/xstudio/src/fixup_macos_bundle.cmake delete mode 100644 src/launch/xstudio/src/macdeploy.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 10fa0f481..5547fbb6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,14 @@ project(${XSTUDIO_GLOBAL_NAME} VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) # Pre-create the target so Qt's FindWrapOpenGL early-returns and never hits the # broken AGL block. Can be removed once the minimum supported Qt is >= 6.9. if(APPLE AND NOT TARGET WrapOpenGL::WrapOpenGL) - find_package(OpenGL REQUIRED) + # Force the system framework to be found, otherwise + # xstudio crashes under Tahoe and it prevents codesigning. + # Note that vcpkg's glew port needs to be patched similarly. + block(SCOPE_FOR VARIABLES) + set(CMAKE_FIND_FRAMEWORK ONLY) + find_package(OpenGL REQUIRED) + endblock() + add_library(WrapOpenGL::WrapOpenGL INTERFACE IMPORTED) target_link_libraries(WrapOpenGL::WrapOpenGL INTERFACE OpenGL::GL) endif() diff --git a/cmake/macros.cmake b/cmake/macros.cmake index 920bcfcb9..378101da0 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -225,7 +225,7 @@ macro(add_plugin_qml name _dir) cmake_path(GET DIR FILENAME dirname) add_custom_command(TARGET ${name}_COPY_QML POST_BUILD COMMAND ${CMAKE_COMMAND} -E - copy_directory ${DIR} ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/PlugIns/xstudio/qml/${dirname}) + copy_directory ${DIR} ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/qml/${dirname}) endif() endforeach() else() diff --git a/cmake/vcpkg_overlay_ports/glew/fix-LNK2019.patch b/cmake/vcpkg_overlay_ports/glew/fix-LNK2019.patch new file mode 100644 index 000000000..bfb58ade7 --- /dev/null +++ b/cmake/vcpkg_overlay_ports/glew/fix-LNK2019.patch @@ -0,0 +1,14 @@ +diff --git a/build/cmake/CMakeLists.txt b/build/cmake/CMakeLists.txt +index 5649972..c0252ec 100644 +--- a/build/cmake/CMakeLists.txt ++++ b/build/cmake/CMakeLists.txt +@@ -129,9 +129,6 @@ if (MSVC) + target_compile_options (glew PRIVATE -GS-) + target_compile_options (glew_s PRIVATE -GS-) + # remove stdlib dependency +- target_link_libraries (glew LINK_PRIVATE -nodefaultlib -noentry) +- target_link_libraries (glew LINK_PRIVATE libvcruntime.lib) +- target_link_libraries (glew LINK_PRIVATE msvcrt.lib ) + string(REGEX REPLACE "/RTC(su|[1su])" "" CMAKE_C_FLAGS_DEBUG ${CMAKE_C_FLAGS_DEBUG}) + elseif (WIN32 AND ((CMAKE_C_COMPILER_ID MATCHES "GNU") OR (CMAKE_C_COMPILER_ID MATCHES "Clang"))) + # remove stdlib dependency on windows with GCC and Clang (for similar reasons diff --git a/cmake/vcpkg_overlay_ports/glew/opengl.patch b/cmake/vcpkg_overlay_ports/glew/opengl.patch new file mode 100644 index 000000000..9e47e1d7f --- /dev/null +++ b/cmake/vcpkg_overlay_ports/glew/opengl.patch @@ -0,0 +1,17 @@ +--- a/build/cmake/CMakeLists.txt ++++ b/build/cmake/CMakeLists.txt +@@ -36,7 +36,13 @@ + set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + endif() + +-find_package (OpenGL REQUIRED) ++if (APPLE) ++ set(CMAKE_FIND_FRAMEWORK ONLY) ++ find_package (OpenGL REQUIRED) ++ unset(CMAKE_FIND_FRAMEWORK) ++else() ++ find_package (OpenGL REQUIRED) ++endif() + + # prefer GLVND + if (OPENGL_opengl_LIBRARY) diff --git a/cmake/vcpkg_overlay_ports/glew/portfile.cmake b/cmake/vcpkg_overlay_ports/glew/portfile.cmake new file mode 100644 index 000000000..40005dff1 --- /dev/null +++ b/cmake/vcpkg_overlay_ports/glew/portfile.cmake @@ -0,0 +1,62 @@ +if("x11" IN_LIST FEATURES) + message(WARNING "${PORT} requires the following libraries from the system package manager:\n libxmu-dev\n libxi-dev\n libgl-dev\n\nThese can be installed on Ubuntu systems via apt-get install libxmu-dev libxi-dev libgl-dev.") +endif() + +# Don't change to vcpkg_from_github! The sources in the git repository (archives) are missing some files that are distributed inside releases. +# More info: https://github.com/nigels-com/glew/issues/31 and https://github.com/nigels-com/glew/issues/13 +vcpkg_download_distfile(ARCHIVE + URLS "https://github.com/nigels-com/glew/releases/download/glew-${VERSION}/glew-${VERSION}.tgz" + FILENAME "glew-${VERSION}.tgz" + SHA512 cb4caecf32ec0f180c2691dc7769ffc99571c64f259a2663a2b80e788f1c2fd5362c59e0caaeefed6fb78a4070366d244666a657358049b09071b59fae2377e0 +) + +vcpkg_extract_source_archive( + SOURCE_PATH + ARCHIVE "${ARCHIVE}" + SOURCE_BASE glew + PATCHES + fix-LNK2019.patch + trim-build.diff + opengl.patch +) + +set(options "") +if(VCPKG_TARGET_IS_ANDROID) + list(APPEND options "-DGLEW_X11=OFF") +endif() + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}/build/cmake" + OPTIONS + ${options} + -DBUILD_UTILS=OFF +) + +vcpkg_cmake_install() +vcpkg_copy_pdbs() +vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/glew) +vcpkg_fixup_pkgconfig() + +# Burn-in CMake build config +vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/include/GL/glew.h" "ifndef GLEW_NO_GLU" "if 0") + +if(NOT VCPKG_BUILD_TYPE) + set(libname GLEW) + if(VCPKG_TARGET_IS_WINDOWS) + set(libname glew32) + endif() + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/glew.pc" " -l${libname}" " -l${libname}d") +endif() + +if(VCPKG_LIBRARY_LINKAGE STREQUAL "static") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/include/GL/glew.h" "#ifdef GLEW_STATIC" "#if 1") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/include/GL/wglew.h" "#ifdef GLEW_STATIC" "#if 1") +endif() + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") + +file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/vcpkg-cmake-wrapper.cmake" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") +file(COPY "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") + +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") diff --git a/cmake/vcpkg_overlay_ports/glew/trim-build.diff b/cmake/vcpkg_overlay_ports/glew/trim-build.diff new file mode 100644 index 000000000..6a1b358b4 --- /dev/null +++ b/cmake/vcpkg_overlay_ports/glew/trim-build.diff @@ -0,0 +1,23 @@ +diff --git a/build/cmake/CMakeLists.txt b/build/cmake/CMakeLists.txt +index c5c1fb8..b1f41bf 100644 +--- a/build/cmake/CMakeLists.txt ++++ b/build/cmake/CMakeLists.txt +@@ -162,8 +162,10 @@ endforeach() + set(targets_to_install "") + if(BUILD_SHARED_LIBS) + list(APPEND targets_to_install glew) ++ set_target_properties(glew_s PROPERTIES EXCLUDE_FROM_ALL 1) + else () + list(APPEND targets_to_install glew_s) ++ set_target_properties(glew PROPERTIES EXCLUDE_FROM_ALL 1) + endif() + + install ( TARGETS ${targets_to_install} +@@ -235,6 +237,7 @@ if(WIN32 AND MSVC AND (NOT MSVC_VERSION LESS 1600)) + DESTINATION ${CMAKE_INSTALL_LIBDIR} + CONFIGURATIONS Debug RelWithDebInfo + COMPONENT Runtime ++ OPTIONAL # subject to BUILD_SHARED_LIBS + ) + endif() + diff --git a/cmake/vcpkg_overlay_ports/glew/usage b/cmake/vcpkg_overlay_ports/glew/usage new file mode 100644 index 000000000..8eebb4b3b --- /dev/null +++ b/cmake/vcpkg_overlay_ports/glew/usage @@ -0,0 +1,14 @@ +glew provides CMake targets: + + # Legacy CMake find module + find_package(GLEW REQUIRED) + target_link_libraries(main PRIVATE GLEW::GLEW) + + # Modern CMake config package + find_package(GLEW CONFIG REQUIRED) + target_link_libraries(main PRIVATE GLEW::GLEW) + +glew provides pkg-config modules: + + # The OpenGL Extension Wrangler library + glew diff --git a/cmake/vcpkg_overlay_ports/glew/vcpkg-cmake-wrapper.cmake b/cmake/vcpkg_overlay_ports/glew/vcpkg-cmake-wrapper.cmake new file mode 100644 index 000000000..909a63540 --- /dev/null +++ b/cmake/vcpkg_overlay_ports/glew/vcpkg-cmake-wrapper.cmake @@ -0,0 +1,25 @@ +include(SelectLibraryConfigurations) +_find_package(GLEW CONFIG) +if(GLEW_FOUND AND TARGET GLEW::GLEW AND NOT DEFINED GLEW_INCLUDE_DIRS) + get_target_property(GLEW_INCLUDE_DIRS GLEW::GLEW INTERFACE_INCLUDE_DIRECTORIES) + set(GLEW_INCLUDE_DIR ${GLEW_INCLUDE_DIRS}) + get_target_property(_GLEW_DEFS GLEW::GLEW INTERFACE_COMPILE_DEFINITIONS) + if("${_GLEW_DEFS}" MATCHES "GLEW_STATIC") + get_target_property(GLEW_LIBRARY_DEBUG GLEW::GLEW IMPORTED_LOCATION_DEBUG) + get_target_property(GLEW_LIBRARY_RELEASE GLEW::GLEW IMPORTED_LOCATION_RELEASE) + else() + get_target_property(GLEW_LIBRARY_DEBUG GLEW::GLEW IMPORTED_IMPLIB_DEBUG) + get_target_property(GLEW_LIBRARY_RELEASE GLEW::GLEW IMPORTED_IMPLIB_RELEASE) + endif() + get_target_property(_GLEW_LINK_INTERFACE GLEW::GLEW IMPORTED_LINK_INTERFACE_LIBRARIES_RELEASE) # same for debug and release + list(APPEND GLEW_LIBRARIES ${_GLEW_LINK_INTERFACE}) + list(APPEND GLEW_LIBRARY ${_GLEW_LINK_INTERFACE}) + select_library_configurations(GLEW) + if("${_GLEW_DEFS}" MATCHES "GLEW_STATIC") + set(GLEW_STATIC_LIBRARIES ${GLEW_LIBRARIES}) + else() + set(GLEW_SHARED_LIBRARIES ${GLEW_LIBRARIES}) + endif() + unset(_GLEW_DEFS) + unset(_GLEW_LINK_INTERFACE) +endif() \ No newline at end of file diff --git a/cmake/vcpkg_overlay_ports/glew/vcpkg.json b/cmake/vcpkg_overlay_ports/glew/vcpkg.json new file mode 100644 index 000000000..a448a8179 --- /dev/null +++ b/cmake/vcpkg_overlay_ports/glew/vcpkg.json @@ -0,0 +1,18 @@ +{ + "name": "glew", + "version": "2.3.1", + "description": "The OpenGL Extension Wrangler Library (GLEW) is a cross-platform open-source C/C++ extension loading library.", + "homepage": "https://github.com/nigels-com/glew", + "supports": "!android", + "dependencies": [ + "opengl", + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +} diff --git a/extern/quickfuture/CMakeLists.txt b/extern/quickfuture/CMakeLists.txt index 1e3176484..90b36c099 100644 --- a/extern/quickfuture/CMakeLists.txt +++ b/extern/quickfuture/CMakeLists.txt @@ -22,7 +22,7 @@ set(QML_FILES if(APPLE) set_target_properties(quickfuture PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/PlugIns/xstudio/qml/QuickFuture" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/qml/QuickFuture" ) else() set_target_properties(quickfuture @@ -49,7 +49,7 @@ set(QML_FUTURE_FILES ) if (APPLE) - set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/PlugIns/xstudio/qml/QuickFuture) + set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/qml/QuickFuture) else() set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickFuture) endif() diff --git a/extern/quickpromise/CMakeLists.txt b/extern/quickpromise/CMakeLists.txt index 975068f77..4af7b8574 100644 --- a/extern/quickpromise/CMakeLists.txt +++ b/extern/quickpromise/CMakeLists.txt @@ -38,11 +38,15 @@ else() add_library(quickpromise SHARED) endif() -# On Windows the DLL must be next to the QML files for Qt to load the plugin. -# (Install puts it at bin/ separately, see below.) -if(WIN32) +if(APPLE) + set_target_properties(quickpromise + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickPromise" + ) +else() set_target_properties(quickpromise PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickPromise" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickPromise") endif() @@ -68,7 +72,7 @@ endif() add_custom_target(COPY_PROMISE_QML) if (APPLE) - set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/PlugIns/xstudio/qml/QuickPromise) + set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/qml/QuickPromise) else() set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickPromise) endif() diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index f7cdfa3da..188dbf71a 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -26,8 +26,8 @@ if(APPLE) add_custom_target(python_module ALL DEPENDS __pybind_xstudio ${VCPKG_LOCATION}/xstudio/api) - # Here we copy the entire python installation into Frameworks - install(DIRECTORY ${VCPKG_LOCATION}/lib/${PYTHONVP} DESTINATION ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Frameworks/lib/) + # Here we copy the entire python installation into Resources + install(DIRECTORY ${VCPKG_LOCATION}/lib/${PYTHONVP} DESTINATION ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/python/lib) elseif(WIN32) diff --git a/scripts/qt_install/CMakeLists.txt b/scripts/qt_install/CMakeLists.txt index 7c59a03c3..bd51bc968 100644 --- a/scripts/qt_install/CMakeLists.txt +++ b/scripts/qt_install/CMakeLists.txt @@ -1,8 +1,17 @@ -# After everything else is installed, windeployqt will scan the contents and package up Qt dependencies. +# After everything else is installed, windeployqt/macdeployqt will scan the contents and package up Qt dependencies. get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY) -find_program(windeployqt_exe windeployqt HINTS "${_qt_bin_dir}") -configure_file(qt_install.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/qt_install.cmake @ONLY) -install(SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/qt_install.cmake) +if(APPLE) + find_program(macdeployqt_exe macdeployqt HINTS "${_qt_bin_dir}") + set(_cmd "${macdeployqt_exe} ${CMAKE_BINARY_DIR}/xSTUDIO.app -qmldir=${CMAKE_SOURCE_DIR}/ui") +elseif(WIN32) + find_program(windeployqt_exe windeployqt HINTS "${_qt_bin_dir}") + set(_cmd "${windeployqt_exe} ${CMAKE_INSTALL_PREFIX}/bin/xstudio.exe --qmldir ${CMAKE_SOURCE_DIR}/ui)") +endif() + +if(NOT "${_cmd}" STREQUAL "") + install(CODE "message(\"Running: ${_cmd}\")") + install(CODE "execute_process(COMMAND ${_cmd})") +endif() diff --git a/scripts/qt_install/qt_install.cmake.in b/scripts/qt_install/qt_install.cmake.in deleted file mode 100644 index 53aa5e9f8..000000000 --- a/scripts/qt_install/qt_install.cmake.in +++ /dev/null @@ -1,3 +0,0 @@ -message("Running windeployqt... with args: ${CMAKE_INSTALL_PREFIX}/bin/xstudio.exe --qmldir @CMAKE_SOURCE_DIR@/ui") -execute_process(COMMAND "@windeployqt_exe@" "${CMAKE_INSTALL_PREFIX}/bin/xstudio.exe" --qmldir "@CMAKE_SOURCE_DIR@/ui" - WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}") diff --git a/src/embedded_python/src/embedded_python.cpp b/src/embedded_python/src/embedded_python.cpp index 32f84db54..8768f3507 100644 --- a/src/embedded_python/src/embedded_python.cpp +++ b/src/embedded_python/src/embedded_python.cpp @@ -56,8 +56,8 @@ void EmbeddedPython::setup() { std::string python_home; if (p.string().find("Contents/Frameworks") != std::string::npos) { // String match will happen On MacOS install, here python - // installation is in Frameworks colder in the app bundle - python_home = p.parent_path(); + // installation is in Resources folder in the app bundle + python_home = p.parent_path().parent_path() / "Resources/python"; } else { // Otherwise, we jump up twice to get above the 'lib' folder // where python310.so is installed, as python home should diff --git a/src/launch/xstudio/src/CMakeLists.txt b/src/launch/xstudio/src/CMakeLists.txt index aa460f563..b66245a36 100644 --- a/src/launch/xstudio/src/CMakeLists.txt +++ b/src/launch/xstudio/src/CMakeLists.txt @@ -130,10 +130,6 @@ elseif(APPLE) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/xstudio_app.ico "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/") @@ -144,17 +140,19 @@ elseif(APPLE) COMMAND ${CMAKE_COMMAND} -E copy ${FFMPEG_APP} "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/MacOS/ffmpeg") - get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) - get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY) - find_program(macdeployqt_exe macdeployqt HINTS "${_qt_bin_dir}") - configure_file(macdeploy.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/macdeploy.cmake @ONLY) - install(SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/macdeploy.cmake) - - # Add custom command to fix library paths after build - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -DAPP_BUNDLE_DIR=${CMAKE_BINARY_DIR}/xSTUDIO.app - -P ${CMAKE_CURRENT_SOURCE_DIR}/fixup_macos_bundle.cmake - ) + # change its rpath so it can find its libraries in their build location so it and can be run in-place + # (this gets cleaned up during the install by macdeployqt) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND install_name_tool -rpath "@loader_path/../../lib" "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib" + "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/MacOS/ffmpeg") + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND install_name_tool -add_rpath "@executable_path/../Frameworks" + "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/MacOS/ffmpeg") + + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Info.plist" "${CMAKE_CURRENT_BINARY_DIR}/Info.plist") + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_BINARY_DIR}/Info.plist" "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Info.plist") else() diff --git a/src/launch/xstudio/src/Info.plist b/src/launch/xstudio/src/Info.plist index 57dc1e23c..a9a61f038 100644 --- a/src/launch/xstudio/src/Info.plist +++ b/src/launch/xstudio/src/Info.plist @@ -9,7 +9,7 @@ CFBundleIconFile xstudio_app.ico CFBundleIdentifier - com.googlecode.skia.${PRODUCT_NAME:rfc1034identifier} + com.dneg.xSTUDIO CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -17,16 +17,14 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? + ${XSTUDIO_GLOBAL_VERSION} CFBundleVersion 1 LSMinimumSystemVersion - ${MACOSX_DEPLOYMENT_TARGET} + ${CMAKE_OSX_DEPLOYMENT_TARGET} NSMainNibFile - HelloWorld + MainMenu NSPrincipalClass NSApplication - \ No newline at end of file + diff --git a/src/launch/xstudio/src/fixup_macos_bundle.cmake b/src/launch/xstudio/src/fixup_macos_bundle.cmake deleted file mode 100644 index 4766332a6..000000000 --- a/src/launch/xstudio/src/fixup_macos_bundle.cmake +++ /dev/null @@ -1,15 +0,0 @@ -# Fix library paths in the app bundle to ensure they're relative -file(GLOB_RECURSE LIBRARIES "${APP_BUNDLE_DIR}/Contents/Frameworks/*.dylib") -foreach(LIB ${LIBRARIES}) - get_filename_component(LIB_NAME ${LIB} NAME) - execute_process(COMMAND install_name_tool -id "@rpath/${LIB_NAME}" ${LIB}) -endforeach() - -# Fix executable references to libraries -execute_process( - COMMAND - install_name_tool - -add_rpath - "@executable_path/../Frameworks" - ${APP_BUNDLE_DIR}/Contents/MacOS/xstudio.bin -) diff --git a/src/launch/xstudio/src/macdeploy.cmake.in b/src/launch/xstudio/src/macdeploy.cmake.in deleted file mode 100644 index 4a7b59b7e..000000000 --- a/src/launch/xstudio/src/macdeploy.cmake.in +++ /dev/null @@ -1,26 +0,0 @@ - -# This command manipulates the rpath in ffmpeg so it can find the ffmpeg libs in the Frameworks -# folder in the app bundle -execute_process(COMMAND install_name_tool ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/MacOS/ffmpeg -rpath @loader_path/../../lib @loader_path/../Frameworks WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}") - -# macdeployqt finds xSTUDIO's library dependencies and adds them to the app bundle, as well as -# making necessary rpath manipulations -message("Running @macdeployqt_exe@ with args: ${CMAKE_BINARY_DIR}/xSTUDIO.app -qmldir=@CMAKE_SOURCE_DIR@/ui") -execute_process(COMMAND "@macdeployqt_exe@" ${CMAKE_BINARY_DIR}/xSTUDIO.app -qmldir=@CMAKE_SOURCE_DIR@/ui - WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}") - -# Fix library paths to ensure they're relative to the bundle -execute_process( - COMMAND - install_name_tool -id - "@rpath/libglobal.dylib" - ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Frameworks/libglobal.dylib -) -execute_process( - COMMAND - install_name_tool - -change - "@rpath/libglobal.dylib" - "@executable_path/../Frameworks/libglobal.dylib" - ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/MacOS/xstudio.bin -) diff --git a/vcpkg.json b/vcpkg.json index 77b7eec24..430dc6236 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -74,6 +74,10 @@ "name": "freetype", "version": "2.10.1-6#0" }, + { + "name": "glew", + "version": "2.3.1" + }, { "name": "boost-modular-build-helper", "version": "1.84.0#3" From 4d710c90c341081e55001ac18fb5a5f5650e8959 Mon Sep 17 00:00:00 2001 From: Ken McGaugh Date: Sun, 2 Aug 2026 11:23:53 +1200 Subject: [PATCH 2/2] feat(macos): add macos_finalize_bundle This adds a post-install step to macOS builds that runs a macos_finalize_bundle script which strips all absolute rpath's from and codesigns any binaries. By default it does an ad-hoc codesign, but by setting the CODESIGN_IDENTITY and KEYCHAIN_PASSWORD environment variables you can perform a proper codesign. Signed-off-by: Ken McGaugh --- CMakeLists.txt | 5 + scripts/macos_finalize_bundle/CMakeLists.txt | 10 + .../macos_finalize_bundle.py | 206 ++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 scripts/macos_finalize_bundle/CMakeLists.txt create mode 100755 scripts/macos_finalize_bundle/macos_finalize_bundle.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 5547fbb6f..22f97965b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -319,6 +319,11 @@ if (USE_VCPKG) add_subdirectory("scripts/qt_install") endif() +if (APPLE) + # Finalize the bundle by removing any absolute rpaths from and codesigning any binaries + add_subdirectory("scripts/macos_finalize_bundle") +endif() + if (WIN32) include(InstallRequiredSystemLibraries) diff --git a/scripts/macos_finalize_bundle/CMakeLists.txt b/scripts/macos_finalize_bundle/CMakeLists.txt new file mode 100644 index 000000000..e4fc64025 --- /dev/null +++ b/scripts/macos_finalize_bundle/CMakeLists.txt @@ -0,0 +1,10 @@ +# After macdeployqt has done its thing to the bundle, it still needs +# some cleanup by removing absolute rpath's from and codesigning +# all the binaries. + +find_package(Python COMPONENTS Interpreter REQUIRED) + +set(_cmd "${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/macos_finalize_bundle.py ${CMAKE_BINARY_DIR}/xSTUDIO.app") + +install(CODE "message(\"Running: ${_cmd}\")") +install(CODE "execute_process(COMMAND ${_cmd})") diff --git a/scripts/macos_finalize_bundle/macos_finalize_bundle.py b/scripts/macos_finalize_bundle/macos_finalize_bundle.py new file mode 100755 index 000000000..ae4872fc7 --- /dev/null +++ b/scripts/macos_finalize_bundle/macos_finalize_bundle.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +import abc +import argparse +import os +import plistlib +import re +import subprocess +import sys +import shutil +import logging + + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) + +CODESIGN_IDENTITY = os.environ.get("CODESIGN_IDENTITY", "-") +KEYCHAIN_PASSWORD = os.environ.get("KEYCHAIN_PASSWORD") + +def fatal_error(msg): + """Print error to stderr and exit with code 1""" + logging.critical(msg) + sys.exit(1) + + +def finalize_framework(path): + name = os.path.basename(path) + lib_name, _ = os.path.splitext(name) + lib_path = os.path.join(path, "Versions", "Current", lib_name) + remove_absolute_rpaths(lib_path) + sign_path(lib_path) + + +def unlock_keychain(): + + if not CODESIGN_IDENTITY: + logging.warning("No identity given. Skipping unlock_keychain") + return + if CODESIGN_IDENTITY == "-": + logging.info("ad-hoc identity given. Skipping unlock_keychain") + + if not KEYCHAIN_PASSWORD: + return + + try: + subprocess.check_call( + ["security", "unlock-keychain", "-p", + KEYCHAIN_PASSWORD, "login.keychain"] + ) + except subprocess.CalledProcessError as error: + fatal_error(f"Error unlocking keychain: {error}") + + +def finalize_app(path): + exe_path = os.path.join(path, "Contents", "MacOS") + app_name = os.path.basename(path) + info_plist_path = os.path.join(path, "Contents", "Info.plist") + info = plistlib.load(open(info_plist_path, "rb")) + app_name = info.get("CFBundleExecutable") + # sign any extra exes but skip the bundle exe + for name in os.listdir(exe_path): + if name == app_name: + continue + exe = os.path.join(exe_path, name) + remove_absolute_rpaths(exe) + sign_path(exe) + remove_absolute_rpaths(os.path.join(exe_path, app_name)) + sign_path(path) + + +def sign_path(path): + if not CODESIGN_IDENTITY: + logging.warning( + f"No identity given. Skipping code sign step for {path}") + return + code_sign = "/usr/bin/codesign" + logging.info(f"Signing {path} with identity {CODESIGN_IDENTITY}") + try: + subprocess.check_output( + [ + code_sign, + "--force", + "--sign", + CODESIGN_IDENTITY, + path, + ] + ) + except subprocess.CalledProcessError as error: + fatal_error( + f"Error signing {path}. stderr: {error.stderr}, stdout: {error.stdout}" + ) + + +def finalize_dir(full_path, name): + if name.endswith(".framework"): + finalize_framework(full_path) + if name.endswith(".app"): + finalize_app(full_path) + + +def finalize_bundle(output_path): + unlock_keychain() + for root, dirs, files in os.walk(output_path, topdown=False): + for name in files: + _, ext = os.path.splitext(name) + if ext in (".dylib", ".so"): + lib = os.path.join(root, name) + remove_absolute_rpaths(lib) + sign_path(lib) + for name in dirs: + full_path = os.path.join(root, name) + finalize_dir(full_path, name) + finalize_dir(output_path, os.path.basename(output_path)) + + +def get_rpaths(binary_path): + """ + Run otool -l on a binary and return list of rpaths. + + Args: + binary_path: Path to the binary/framework/dylib + + Returns: + List of rpath strings (e.g., ['@executable_path/../Frameworks']) + """ + try: + # Run otool -l + result = subprocess.run( + ["otool", "-l", binary_path], capture_output=True, text=True, check=True + ) + + rpaths = [] + lines = result.stdout.split("\n") + + # Look for LC_RPATH sections + for i, line in enumerate(lines): + if "cmd LC_RPATH" in line: + # The path is typically 2 lines after LC_RPATH + if i + 2 < len(lines): + path_line = lines[i + 2].strip() + # Extract path using regex + match = re.search(r"path\s+(.+?)\s+\(offset", path_line) + if match: + rpaths.append(match.group(1)) + + return rpaths + + except subprocess.CalledProcessError as e: + fatal_error(f"Error running otool: {e}") + except FileNotFoundError: + fatal_error("otool not found") + + +def delete_rpath(binary_path, rpath): + """ + Delete an rpath from a binary using install_name_tool. + + Args: + binary_path: Path to the binary/framework/dylib + rpath: The rpath to delete (e.g., '@executable_path/../Frameworks') + + Raises: + RuntimeError: If install_name_tool fails + """ + try: + subprocess.run( + ["install_name_tool", "-delete_rpath", rpath, binary_path], + capture_output=True, + text=True, + check=True, + ) + except subprocess.CalledProcessError as e: + fatal_error(f"install_name_tool failed: {e.stderr}") + except FileNotFoundError: + fatal_error("install_name_tool not found") + + +def remove_absolute_rpaths(binary_path): + """ + Remove all absolute rpaths from a binary, keeping only relative ones (@-prefixed). + + Args: + binary_path: Path to the binary/framework/dylib + """ + rpaths = get_rpaths(binary_path) + + for rpath in rpaths: + if not rpath.startswith("@"): + logging.info( + f"Deleting absolute rpath '{rpath}' from '{binary_path}'") + delete_rpath(binary_path, rpath) + + +def main(): + parser = argparse.ArgumentParser( + "finalize_macos_bundle", + description="rpath fixer and codesign tool for MacOS build" + ) + parser.add_argument("bundle") + + args = parser.parse_args() + finalize_bundle(args.bundle) + +if __name__ == "__main__": + main()