From 0360c9193cffa226948f28922dc3cd4f0e3f00a2 Mon Sep 17 00:00:00 2001 From: Dimitris Panokostas Date: Tue, 15 Sep 2026 22:15:25 +0200 Subject: [PATCH 1/2] build(macos): bundle libSDL3.dylib for sdl2-compat Homebrew's sdl2 formula ships sdl2-compat (2.32+), which dlopens libSDL3.dylib at runtime, trying @loader_path first. dylibbundler only walks LC_LOAD_DYLIB entries, so SDL3 never landed in the app bundle and launch died with "Failed loading SDL3 library.". After dylibbundler runs, detect sdl2-compat in the bundled libSDL2 (it embeds the dlopen name libSDL3.dylib; real SDL2 does not), copy libSDL3.dylib next to it from the Homebrew prefix, and re-sign the bundle so the seal stays valid. Real-SDL2 builds are untouched. --- cmake/macos/BundleSDL3Compat.cmake | 66 ++++++++++++++++++++++++++++++ cmake/macos/CMakeLists.txt | 9 ++++ 2 files changed, 75 insertions(+) create mode 100644 cmake/macos/BundleSDL3Compat.cmake diff --git a/cmake/macos/BundleSDL3Compat.cmake b/cmake/macos/BundleSDL3Compat.cmake new file mode 100644 index 000000000..484502b3d --- /dev/null +++ b/cmake/macos/BundleSDL3Compat.cmake @@ -0,0 +1,66 @@ +# Bundle libSDL3.dylib when libSDL2 is sdl2-compat. +# BUNDLE_DIR - the app bundle's Contents/Frameworks directory +# APP_BINARY - the main executable inside the app bundle +# SDL2_LIBRARY - the SDL2 library the executable was linked against +# +# Homebrew's sdl2 formula ships sdl2-compat (2.32+), which dlopens +# libSDL3.dylib at runtime (first candidate: @loader_path/libSDL3.dylib). +# dylibbundler only walks LC_LOAD_DYLIB entries, so the SDL3 dependency +# never lands in the bundle and the app aborts with +# "Failed loading SDL3 library." on launch. + +set(bundled_sdl2 "${BUNDLE_DIR}/libSDL2-2.0.0.dylib") +if(NOT EXISTS "${bundled_sdl2}") + # Static or monolithic SDL2 build: nothing to do. + return() +endif() + +# sdl2-compat embeds the literal name "libSDL3.dylib" in its dlopen +# search list; real SDL2 does not. +execute_process( + COMMAND grep -q libSDL3.dylib "${bundled_sdl2}" + RESULT_VARIABLE is_sdl2_compat + OUTPUT_QUIET ERROR_QUIET) +if(NOT is_sdl2_compat EQUAL 0) + # Real SDL2: no runtime SDL3 dependency. + return() +endif() + +get_filename_component(sdl2_lib_dir "${SDL2_LIBRARY}" DIRECTORY) +find_library(SDL3_LIBRARY + NAMES SDL3 libSDL3 + HINTS "${sdl2_lib_dir}" /opt/homebrew/lib /usr/local/lib) + +if(NOT SDL3_LIBRARY) + message(WARNING + "libSDL2 is sdl2-compat but libSDL3.dylib was not found; " + "the bundled app will fail to launch with " + "'Failed loading SDL3 library.' Install SDL3 (brew install sdl3) " + "and rebuild.") + return() +endif() + +execute_process( + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${SDL3_LIBRARY}" "${BUNDLE_DIR}/libSDL3.dylib" + RESULT_VARIABLE copy_result) +if(NOT copy_result EQUAL 0) + message(WARNING "failed to copy ${SDL3_LIBRARY} into ${BUNDLE_DIR}") + return() +endif() +execute_process( + COMMAND codesign --force --sign - "${BUNDLE_DIR}/libSDL3.dylib" + RESULT_VARIABLE codesign_result) +if(NOT codesign_result EQUAL 0) + message(WARNING "codesign failed for bundled libSDL3.dylib") +endif() + +# libSDL3.dylib was added after dylibbundler signed the bundle; re-sign the +# app so the bundle seal stays valid. +execute_process( + COMMAND codesign --force --deep --sign - "${APP_BINARY}" + RESULT_VARIABLE app_codesign_result) +if(NOT app_codesign_result EQUAL 0) + message(WARNING "codesign failed for ${APP_BINARY}") +endif() +message(STATUS "Bundled ${SDL3_LIBRARY} for sdl2-compat") diff --git a/cmake/macos/CMakeLists.txt b/cmake/macos/CMakeLists.txt index 983135854..124f19bf0 100644 --- a/cmake/macos/CMakeLists.txt +++ b/cmake/macos/CMakeLists.txt @@ -31,6 +31,15 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND dylibbundler -od -b -x $ -d $/../Frameworks/ -p @executable_path/../Frameworks/) +# Homebrew's sdl2 formula is sdl2-compat, which dlopens libSDL3.dylib at +# runtime; dylibbundler can't see dlopen dependencies, so bundle SDL3 here. +add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} + -DBUNDLE_DIR=$/../Frameworks + -DAPP_BINARY=$ + -DSDL2_LIBRARY=$ + -P ${CMAKE_CURRENT_LIST_DIR}/BundleSDL3Compat.cmake) + if (NOT "${CMAKE_GENERATOR}" MATCHES "Xcode") install(FILES $ DESTINATION $/../Resources/plugins/) From 2dd789e981c4aff1990924ab6831f5250d50969c Mon Sep 17 00:00:00 2001 From: Dimitris Panokostas Date: Tue, 15 Sep 2026 22:22:03 +0200 Subject: [PATCH 2/2] build(macos): re-sign the enclosing .app bundle, not the inner binary Per review: --deep on the executable does not regenerate the enclosing bundle's resource seal. Sign nested code (libSDL3.dylib) first, then the .app itself. --- cmake/macos/BundleSDL3Compat.cmake | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cmake/macos/BundleSDL3Compat.cmake b/cmake/macos/BundleSDL3Compat.cmake index 484502b3d..84bc9f316 100644 --- a/cmake/macos/BundleSDL3Compat.cmake +++ b/cmake/macos/BundleSDL3Compat.cmake @@ -55,12 +55,14 @@ if(NOT codesign_result EQUAL 0) message(WARNING "codesign failed for bundled libSDL3.dylib") endif() -# libSDL3.dylib was added after dylibbundler signed the bundle; re-sign the -# app so the bundle seal stays valid. +# libSDL3.dylib was added after dylibbundler signed the bundle. Nested code +# must be signed before the enclosing bundle, so sign libSDL3.dylib first +# (above), then re-seal the whole .app bundle. +get_filename_component(app_bundle "${APP_BINARY}/../../.." ABSOLUTE) execute_process( - COMMAND codesign --force --deep --sign - "${APP_BINARY}" + COMMAND codesign --force --deep --sign - "${app_bundle}" RESULT_VARIABLE app_codesign_result) if(NOT app_codesign_result EQUAL 0) - message(WARNING "codesign failed for ${APP_BINARY}") + message(WARNING "codesign failed for ${app_bundle}") endif() message(STATUS "Bundled ${SDL3_LIBRARY} for sdl2-compat")