diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 1860915..239141f 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -66,7 +66,7 @@ jobs: - name: Install System Dependencies via Homebrew run: | - brew install ninja boost catch2 cmake enet fmt ffmpeg glslang hidapi libvpx lld llvm nasm nlohmann-json openal-soft sdl2 molten-vk vulkan-headers vulkan-loader webp lz4 zstd openssl@3 pkg-config libusb opus python@3.12 + brew install ninja boost catch2 cmake enet fmt ffmpeg glslang hidapi libvpx lld llvm nasm nlohmann-json openal-soft sdl2 sdl3 molten-vk vulkan-headers vulkan-loader webp lz4 zstd openssl@3 pkg-config libusb opus python@3.12 - name: Clone Citron Source shell: bash @@ -192,6 +192,8 @@ jobs: "$MACDEPLOYQT_PATH" "$APP_BUNDLE_PATH" -libpath="$QT_LIB_PATH" -verbose=1 -always-overwrite FRAMEWORKS_PATH="$APP_BUNDLE_PATH/Contents/Frameworks" + EXECUTABLE_PATH="$APP_BUNDLE_PATH/Contents/MacOS/citron" + mkdir -p "$FRAMEWORKS_PATH" # Create Vulkan ICD json to ensure bundled MoltenVK is used (not homebrew's) echo "Setting up Vulkan ICD for bundled MoltenVK..." @@ -209,40 +211,62 @@ jobs: ICDEOF echo "Created MoltenVK ICD at $VULKAN_ICD_DIR/MoltenVK_icd.json" - # Copy missing transitive dependencies (like libsharpyuv from webp, liblcms2 from jxl) + # Copy missing transitive dependencies (like libsharpyuv from webp, liblcms2 from jxl). + # Scan the main executable too; macdeployqt can miss non-Qt Homebrew libraries linked by citron. echo "Checking for missing transitive dependencies..." - copy_missing_deps() { + find_bundled_binaries() { + printf '%s\n' "$EXECUTABLE_PATH" find "$FRAMEWORKS_PATH" -name "*.dylib" -type f | while read -r dylib; do - # Handle @rpath/ references - otool -L "$dylib" 2>/dev/null | grep -E "^\s+@rpath/" | awk '{print $1}' | sed 's|@rpath/||' | while read -r dep; do - if [ ! -f "$FRAMEWORKS_PATH/$dep" ]; then - HOMEBREW_LIB=$(find /opt/homebrew/lib /opt/homebrew/Cellar /usr/local/lib -name "$dep" 2>/dev/null | head -1) - if [ -n "$HOMEBREW_LIB" ] && [ -f "$HOMEBREW_LIB" ]; then - echo "Copying missing @rpath dependency: $dep" - cp "$HOMEBREW_LIB" "$FRAMEWORKS_PATH/" - chmod u+w "$FRAMEWORKS_PATH/$dep" - install_name_tool -id "@rpath/$dep" "$FRAMEWORKS_PATH/$dep" 2>/dev/null || true - fi - fi - done - # Handle absolute homebrew path references (critical for portability!) - otool -L "$dylib" 2>/dev/null | grep -E "^\s+(/opt/homebrew|/usr/local)" | awk '{print $1}' | while read -r abs_dep; do - dep_name=$(basename "$abs_dep") - if [ ! -f "$FRAMEWORKS_PATH/$dep_name" ] && [ -f "$abs_dep" ]; then - echo "Copying missing absolute-path dependency: $dep_name (from $abs_dep)" - cp "$abs_dep" "$FRAMEWORKS_PATH/" - chmod u+w "$FRAMEWORKS_PATH/$dep_name" - install_name_tool -id "@rpath/$dep_name" "$FRAMEWORKS_PATH/$dep_name" 2>/dev/null || true - fi - done + printf '%s\n' "$dylib" + done + find "$FRAMEWORKS_PATH" -name "*.framework" -type d | while read -r framework_dir; do + framework_name=$(basename "$framework_dir" .framework) + framework_binary="$framework_dir/Versions/A/$framework_name" + [ ! -f "$framework_binary" ] && framework_binary="$framework_dir/$framework_name" + [ -f "$framework_binary" ] && printf '%s\n' "$framework_binary" done } + + copy_deps_for_binary() { + local binary="$1" + [ -f "$binary" ] || return 0 + + # Handle @rpath/ references + otool -L "$binary" 2>/dev/null | grep -E "^\s+@rpath/" | awk '{print $1}' | sed 's|@rpath/||' | while read -r dep; do + if [ ! -f "$FRAMEWORKS_PATH/$dep" ]; then + HOMEBREW_LIB=$(find /opt/homebrew/lib /opt/homebrew/Cellar /usr/local/lib -name "$dep" 2>/dev/null | head -1) + if [ -n "$HOMEBREW_LIB" ] && [ -f "$HOMEBREW_LIB" ]; then + echo "Copying missing @rpath dependency for $(basename "$binary"): $dep" + cp "$HOMEBREW_LIB" "$FRAMEWORKS_PATH/" + chmod u+w "$FRAMEWORKS_PATH/$dep" + install_name_tool -id "@rpath/$dep" "$FRAMEWORKS_PATH/$dep" 2>/dev/null || true + fi + fi + done + + # Handle absolute homebrew path references (critical for portability!) + otool -L "$binary" 2>/dev/null | grep -E "^\s+(/opt/homebrew|/usr/local)" | awk '{print $1}' | while read -r abs_dep; do + dep_name=$(basename "$abs_dep") + if [ ! -f "$FRAMEWORKS_PATH/$dep_name" ] && [ -f "$abs_dep" ]; then + echo "Copying missing absolute-path dependency for $(basename "$binary"): $dep_name (from $abs_dep)" + cp "$abs_dep" "$FRAMEWORKS_PATH/" + chmod u+w "$FRAMEWORKS_PATH/$dep_name" + install_name_tool -id "@rpath/$dep_name" "$FRAMEWORKS_PATH/$dep_name" 2>/dev/null || true + fi + done + } + + copy_missing_deps() { + find_bundled_binaries | while read -r binary; do + copy_deps_for_binary "$binary" + done + } + # Run multiple passes to catch nested dependencies for i in 1 2 3 4 5; do echo "Pass $i: checking dependencies..." copy_missing_deps done - EXECUTABLE_PATH="$APP_BUNDLE_PATH/Contents/MacOS/citron" install_name_tool -add_rpath "@executable_path/../Frameworks" "$EXECUTABLE_PATH" 2>/dev/null || true @@ -299,8 +323,50 @@ jobs: [ -f "$framework_binary" ] && install_name_tool -id "@rpath/$framework_name.framework/Versions/A/$framework_name" "$framework_binary" 2>/dev/null || true done + find_bundled_binaries | while read -r binary; do + otool -L "$binary" 2>/dev/null | grep -E "^\s+/" | awk '{print $1}' | while read -r dep; do + dep_name=$(basename "$dep") + if [ -f "$FRAMEWORKS_PATH/$dep_name" ]; then + install_name_tool -change "$dep" "@rpath/$dep_name" "$binary" 2>/dev/null || true + fi + done + done + + # Some SDL3 consumers load "libSDL3.dylib" dynamically at runtime, so it will not + # necessarily appear in otool output. Homebrew/macdeployqt may only leave + # "libSDL3.0.dylib" in Frameworks, which makes that dlopen path fail. + SDL3_BREW_PREFIX="$(brew --prefix sdl3 2>/dev/null || true)" + if [ ! -f "$FRAMEWORKS_PATH/libSDL3.dylib" ]; then + if [ -n "$SDL3_BREW_PREFIX" ] && [ -f "$SDL3_BREW_PREFIX/lib/libSDL3.dylib" ]; then + echo "Copying libSDL3.dylib from Homebrew for runtime dlopen compatibility" + cp "$SDL3_BREW_PREFIX/lib/libSDL3.dylib" "$FRAMEWORKS_PATH/libSDL3.dylib" + elif [ -f "$FRAMEWORKS_PATH/libSDL3.0.dylib" ]; then + echo "Creating libSDL3.dylib from bundled libSDL3.0.dylib for runtime dlopen compatibility" + cp "$FRAMEWORKS_PATH/libSDL3.0.dylib" "$FRAMEWORKS_PATH/libSDL3.dylib" + fi + fi + if [ -f "$FRAMEWORKS_PATH/libSDL3.dylib" ]; then + chmod u+w "$FRAMEWORKS_PATH/libSDL3.dylib" + install_name_tool -id "@rpath/libSDL3.dylib" "$FRAMEWORKS_PATH/libSDL3.dylib" 2>/dev/null || true + fi + chmod -R u+w "$APP_BUNDLE_PATH" codesign --force --deep -s - "$APP_BUNDLE_PATH" + codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE_PATH" + + echo "Final executable SDL dependencies:" + otool -L "$EXECUTABLE_PATH" | grep -i SDL || true + + echo "Final executable rpaths:" + otool -l "$EXECUTABLE_PATH" | grep -A2 LC_RPATH || true + + echo "Checking for remaining Homebrew library references..." + if find_bundled_binaries | while read -r binary; do + otool -L "$binary" 2>/dev/null | grep -E "^\s+(/opt/homebrew|/usr/local)" && echo " in $binary" + done | tee /tmp/citron-homebrew-refs.txt; [ -s /tmp/citron-homebrew-refs.txt ]; then + echo "::error::Packaged app still contains Homebrew library references." + exit 1 + fi # Step 8: Create the final DMG echo "Creating final DMG..."