From 9a23321a56975dec097a1d5d5ab1d69f7cd3fe26 Mon Sep 17 00:00:00 2001 From: Francis Date: Tue, 12 May 2026 15:41:41 +0200 Subject: [PATCH 1/4] fix(ci): package release runtimes before smoke tests --- .github/workflows/release-linux.yml | 8 +++---- .github/workflows/release-macos.yml | 34 +++++++++++++-------------- .github/workflows/release-windows.yml | 15 +++++++----- docs/frontends/DESKTOP.md | 8 +++---- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index d09a7df1..2994717e 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -73,14 +73,12 @@ jobs: cp README.md LICENSE-MIT LICENSE-APACHE "${package_dir}/" sdl_libs=(target/release-max/libSDL3*.so*) - if ((${#sdl_libs[@]} == 0)); then - echo "expected SDL3 shared libraries under target/release-max" >&2 - exit 1 + if ((${#sdl_libs[@]} > 0)); then + cp "${sdl_libs[@]}" "${package_dir}/" + patchelf --set-rpath '$ORIGIN' "${package_dir}/gb-desktop" fi - cp "${sdl_libs[@]}" "${package_dir}/" chmod +x "${package_dir}/gb-desktop" - patchelf --set-rpath '$ORIGIN' "${package_dir}/gb-desktop" "${package_dir}/gb-desktop" --help tar -C dist -czf "${tar_path}" "${package_name}" diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index 05a866f6..5426cf7c 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -49,25 +49,23 @@ jobs: cp README.md LICENSE-MIT LICENSE-APACHE "${package_dir}/" sdl_libs=(target/release-max/libSDL3*.dylib) - if ((${#sdl_libs[@]} == 0)); then - echo "expected SDL3 dylibs under target/release-max" >&2 - exit 1 + if ((${#sdl_libs[@]} > 0)); then + for dylib in "${sdl_libs[@]}"; do + dylib_name="$(basename "${dylib}")" + cp "${dylib}" "${macos_dir}/${dylib_name}" + chmod +w "${macos_dir}/${dylib_name}" + install_name_tool -id "@executable_path/${dylib_name}" "${macos_dir}/${dylib_name}" + done + + bundled_sdl="$(basename "${sdl_libs[0]}")" + while IFS= read -r dependency; do + case "${dependency}" in + *libSDL3*.dylib*) + install_name_tool -change "${dependency}" "@executable_path/${bundled_sdl}" "${macos_dir}/gb-desktop" + ;; + esac + done < <(otool -L "${macos_dir}/gb-desktop" | tail -n +2 | awk '{print $1}') fi - for dylib in "${sdl_libs[@]}"; do - dylib_name="$(basename "${dylib}")" - cp "${dylib}" "${macos_dir}/${dylib_name}" - chmod +w "${macos_dir}/${dylib_name}" - install_name_tool -id "@executable_path/${dylib_name}" "${macos_dir}/${dylib_name}" - done - - bundled_sdl="$(basename "${sdl_libs[0]}")" - while IFS= read -r dependency; do - case "${dependency}" in - *libSDL3*.dylib*) - install_name_tool -change "${dependency}" "@executable_path/${bundled_sdl}" "${macos_dir}/gb-desktop" - ;; - esac - done < <(otool -L "${macos_dir}/gb-desktop" | tail -n +2 | awk '{print $1}') chmod +x "${macos_dir}/gb-desktop" codesign --force --deep --sign - "${app_root}" diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index f1e79a59..968112ae 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -26,9 +26,6 @@ jobs: - name: Build desktop release run: cargo build --profile release-max -p gb-desktop --features gb-desktop/sdl3-build-from-source - - name: Smoke test desktop CLI - run: target\release-max\gb-desktop.exe --help - - name: Package Windows binary shell: pwsh run: | @@ -42,9 +39,15 @@ jobs: Copy-Item -Path "target\release-max\gb-desktop.exe" -Destination $packageDir -Force Copy-Item -Path "README.md", "LICENSE-MIT", "LICENSE-APACHE" -Destination $packageDir -Force - Get-ChildItem -Path "target\release-max" -Recurse -Filter "*.dll" -File | - Sort-Object -Property FullName -Unique | - Copy-Item -Destination $packageDir -Force + $packageDlls = @( + Get-ChildItem -Path "target\release-max" -Recurse -Filter "*.dll" -File | + Sort-Object -Property FullName -Unique + ) + if ($packageDlls.Count -gt 0) { + $packageDlls | Copy-Item -Destination $packageDir -Force + } + + & (Join-Path $packageDir "gb-desktop.exe") --help Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -Force diff --git a/docs/frontends/DESKTOP.md b/docs/frontends/DESKTOP.md index 5d541e9f..bd07730c 100644 --- a/docs/frontends/DESKTOP.md +++ b/docs/frontends/DESKTOP.md @@ -36,13 +36,13 @@ cargo run --profile release-max -p gb-desktop -- [path/to/rom.gb] ## Release packages -The GitHub release workflows package only the SDL3 desktop frontend. They all build `gb-desktop` with `cargo build --profile release-max -p gb-desktop --features gb-desktop/sdl3-build-from-source`, then run `gb-desktop --help` from the packaged runtime layout so the artifact proves that the executable can find its bundled SDL3 runtime before upload. +The GitHub release workflows package only the SDL3 desktop frontend. They all build `gb-desktop` with `cargo build --profile release-max -p gb-desktop --features gb-desktop/sdl3-build-from-source`, then run `gb-desktop --help` from the packaged runtime layout so the artifact proves that the executable can start with whatever SDL3 runtime form the source-build path emitted before upload. The workflows live under `.github/workflows/`: -- `release-windows.yml` creates `gb-cycle-windows-x86_64.zip` on `windows-latest`, containing `gb-desktop.exe`, `README.md`, both license files, and every SDL3 `.dll` copied by the source-build path under `target/release-max`. -- `release-linux.yml` creates `gb-cycle-linux-x86_64.tar.gz` on `ubuntu-latest`, containing `gb-desktop`, `README.md`, both license files, and `libSDL3*.so*` from `target/release-max`; the binary is patched with an `$ORIGIN` rpath so it loads the bundled SDL3 shared object from the unpacked artifact directory without requiring `LD_LIBRARY_PATH`. -- `release-macos.yml` creates only the Apple Silicon `gb-cycle-macos-aarch64.zip` package on `macos-latest`, containing `GB Cycle.app`, `README.md`, both license files, and `libSDL3*.dylib` inside `Contents/MacOS`; the dylib install names are rewritten to `@executable_path`, the app is ad-hoc signed after that rewrite, and the bundle reuses `crates/gb-desktop/macos/Info.plist` so the release artifact carries the same Pocket Camera usage string as the local macOS launcher. +- `release-windows.yml` creates `gb-cycle-windows-x86_64.zip` on `windows-latest`, containing `gb-desktop.exe`, `README.md`, both license files, and every `.dll` emitted by the source-build path under `target/release-max`. +- `release-linux.yml` creates `gb-cycle-linux-x86_64.tar.gz` on `ubuntu-latest`, containing `gb-desktop`, `README.md`, both license files, and any `libSDL3*.so*` emitted under `target/release-max`; when a shared object is present, the binary is patched with an `$ORIGIN` rpath so it loads the bundled SDL3 shared object from the unpacked artifact directory without requiring `LD_LIBRARY_PATH`. +- `release-macos.yml` creates only the Apple Silicon `gb-cycle-macos-aarch64.zip` package on `macos-latest`, containing `GB Cycle.app`, `README.md`, both license files, and any `libSDL3*.dylib` emitted under `target/release-max` inside `Contents/MacOS`; when a dylib is present, the dylib install names are rewritten to `@executable_path` before ad-hoc signing, and the bundle reuses `crates/gb-desktop/macos/Info.plist` so the release artifact carries the same Pocket Camera usage string as the local macOS launcher. Manual `workflow_dispatch` runs are for artifact validation and upload the package through `actions/upload-artifact`. Tag pushes matching `v*` additionally attach the same package to the GitHub Release via `softprops/action-gh-release`; do not use a non-release branch tag unless you intentionally want to test the release-asset path and then clean up the temporary release/tag afterward. From e0ea90914f448ceda2260b673dd079fabc24886f Mon Sep 17 00:00:00 2001 From: Francis Date: Tue, 12 May 2026 15:57:23 +0200 Subject: [PATCH 2/4] fix(ci): validate sdl release linkage --- .github/workflows/release-linux.yml | 7 +++++++ .github/workflows/release-macos.yml | 4 ++++ docs/frontends/DESKTOP.md | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 2994717e..7227c10d 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -76,6 +76,13 @@ jobs: if ((${#sdl_libs[@]} > 0)); then cp "${sdl_libs[@]}" "${package_dir}/" patchelf --set-rpath '$ORIGIN' "${package_dir}/gb-desktop" + else + ldd_output="$(ldd "${package_dir}/gb-desktop" 2>&1 || true)" + if grep -q 'libSDL3' <<<"${ldd_output}"; then + printf '%s\n' "${ldd_output}" + echo "gb-desktop dynamically links SDL3 but no bundled libSDL3 shared object was emitted" >&2 + exit 1 + fi fi chmod +x "${package_dir}/gb-desktop" diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index 5426cf7c..8cefb406 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -65,6 +65,10 @@ jobs: ;; esac done < <(otool -L "${macos_dir}/gb-desktop" | tail -n +2 | awk '{print $1}') + elif otool -L "${macos_dir}/gb-desktop" | grep -q 'libSDL3'; then + otool -L "${macos_dir}/gb-desktop" + echo "gb-desktop dynamically links SDL3 but no bundled libSDL3 dylib was emitted" >&2 + exit 1 fi chmod +x "${macos_dir}/gb-desktop" diff --git a/docs/frontends/DESKTOP.md b/docs/frontends/DESKTOP.md index bd07730c..c3506878 100644 --- a/docs/frontends/DESKTOP.md +++ b/docs/frontends/DESKTOP.md @@ -41,8 +41,8 @@ The GitHub release workflows package only the SDL3 desktop frontend. They all bu The workflows live under `.github/workflows/`: - `release-windows.yml` creates `gb-cycle-windows-x86_64.zip` on `windows-latest`, containing `gb-desktop.exe`, `README.md`, both license files, and every `.dll` emitted by the source-build path under `target/release-max`. -- `release-linux.yml` creates `gb-cycle-linux-x86_64.tar.gz` on `ubuntu-latest`, containing `gb-desktop`, `README.md`, both license files, and any `libSDL3*.so*` emitted under `target/release-max`; when a shared object is present, the binary is patched with an `$ORIGIN` rpath so it loads the bundled SDL3 shared object from the unpacked artifact directory without requiring `LD_LIBRARY_PATH`. -- `release-macos.yml` creates only the Apple Silicon `gb-cycle-macos-aarch64.zip` package on `macos-latest`, containing `GB Cycle.app`, `README.md`, both license files, and any `libSDL3*.dylib` emitted under `target/release-max` inside `Contents/MacOS`; when a dylib is present, the dylib install names are rewritten to `@executable_path` before ad-hoc signing, and the bundle reuses `crates/gb-desktop/macos/Info.plist` so the release artifact carries the same Pocket Camera usage string as the local macOS launcher. +- `release-linux.yml` creates `gb-cycle-linux-x86_64.tar.gz` on `ubuntu-latest`, containing `gb-desktop`, `README.md`, both license files, and any `libSDL3*.so*` emitted under `target/release-max`; when a shared object is present, the binary is patched with an `$ORIGIN` rpath so it loads the bundled SDL3 shared object from the unpacked artifact directory without requiring `LD_LIBRARY_PATH`, and when no shared object is present the workflow fails if `ldd` still reports an SDL3 dynamic dependency. +- `release-macos.yml` creates only the Apple Silicon `gb-cycle-macos-aarch64.zip` package on `macos-latest`, containing `GB Cycle.app`, `README.md`, both license files, and any `libSDL3*.dylib` emitted under `target/release-max` inside `Contents/MacOS`; when a dylib is present, the dylib install names are rewritten to `@executable_path` before ad-hoc signing, when no dylib is present the workflow fails if `otool -L` still reports an SDL3 dynamic dependency, and the bundle reuses `crates/gb-desktop/macos/Info.plist` so the release artifact carries the same Pocket Camera usage string as the local macOS launcher. Manual `workflow_dispatch` runs are for artifact validation and upload the package through `actions/upload-artifact`. Tag pushes matching `v*` additionally attach the same package to the GitHub Release via `softprops/action-gh-release`; do not use a non-release branch tag unless you intentionally want to test the release-asset path and then clean up the temporary release/tag afterward. From e1eb35c1e301a290a82b2b9342be098f320062cc Mon Sep 17 00:00:00 2001 From: Francis Date: Tue, 12 May 2026 19:37:37 +0200 Subject: [PATCH 3/4] fix(ci): find sdl release libraries recursively --- .github/workflows/release-linux.yml | 8 +++++++- .github/workflows/release-macos.yml | 8 +++++++- docs/frontends/DESKTOP.md | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 7227c10d..a8c88738 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -72,7 +72,13 @@ jobs: cp target/release-max/gb-desktop "${package_dir}/" cp README.md LICENSE-MIT LICENSE-APACHE "${package_dir}/" - sdl_libs=(target/release-max/libSDL3*.so*) + sdl_libs=() + while IFS= read -r sdl_lib; do + sdl_libs+=("${sdl_lib}") + done < <( + find target/release-max \( -type f -o -type l \) -name 'libSDL3*.so*' -print | + sort -u + ) if ((${#sdl_libs[@]} > 0)); then cp "${sdl_libs[@]}" "${package_dir}/" patchelf --set-rpath '$ORIGIN' "${package_dir}/gb-desktop" diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index 8cefb406..1f53173b 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -48,7 +48,13 @@ jobs: printf 'APPL????' > "${app_root}/Contents/PkgInfo" cp README.md LICENSE-MIT LICENSE-APACHE "${package_dir}/" - sdl_libs=(target/release-max/libSDL3*.dylib) + sdl_libs=() + while IFS= read -r sdl_lib; do + sdl_libs+=("${sdl_lib}") + done < <( + find target/release-max \( -type f -o -type l \) -name 'libSDL3*.dylib' -print | + sort -u + ) if ((${#sdl_libs[@]} > 0)); then for dylib in "${sdl_libs[@]}"; do dylib_name="$(basename "${dylib}")" diff --git a/docs/frontends/DESKTOP.md b/docs/frontends/DESKTOP.md index c3506878..10bec46b 100644 --- a/docs/frontends/DESKTOP.md +++ b/docs/frontends/DESKTOP.md @@ -40,9 +40,9 @@ The GitHub release workflows package only the SDL3 desktop frontend. They all bu The workflows live under `.github/workflows/`: -- `release-windows.yml` creates `gb-cycle-windows-x86_64.zip` on `windows-latest`, containing `gb-desktop.exe`, `README.md`, both license files, and every `.dll` emitted by the source-build path under `target/release-max`. -- `release-linux.yml` creates `gb-cycle-linux-x86_64.tar.gz` on `ubuntu-latest`, containing `gb-desktop`, `README.md`, both license files, and any `libSDL3*.so*` emitted under `target/release-max`; when a shared object is present, the binary is patched with an `$ORIGIN` rpath so it loads the bundled SDL3 shared object from the unpacked artifact directory without requiring `LD_LIBRARY_PATH`, and when no shared object is present the workflow fails if `ldd` still reports an SDL3 dynamic dependency. -- `release-macos.yml` creates only the Apple Silicon `gb-cycle-macos-aarch64.zip` package on `macos-latest`, containing `GB Cycle.app`, `README.md`, both license files, and any `libSDL3*.dylib` emitted under `target/release-max` inside `Contents/MacOS`; when a dylib is present, the dylib install names are rewritten to `@executable_path` before ad-hoc signing, when no dylib is present the workflow fails if `otool -L` still reports an SDL3 dynamic dependency, and the bundle reuses `crates/gb-desktop/macos/Info.plist` so the release artifact carries the same Pocket Camera usage string as the local macOS launcher. +- `release-windows.yml` creates `gb-cycle-windows-x86_64.zip` on `windows-latest`, containing `gb-desktop.exe`, `README.md`, both license files, and every `.dll` emitted recursively by the source-build path under `target/release-max`. +- `release-linux.yml` creates `gb-cycle-linux-x86_64.tar.gz` on `ubuntu-latest`, containing `gb-desktop`, `README.md`, both license files, and any `libSDL3*.so*` found recursively under `target/release-max`; when a shared object is present, the binary is patched with an `$ORIGIN` rpath so it loads the bundled SDL3 shared object from the unpacked artifact directory without requiring `LD_LIBRARY_PATH`, and when no shared object is present the workflow fails if `ldd` still reports an SDL3 dynamic dependency. +- `release-macos.yml` creates only the Apple Silicon `gb-cycle-macos-aarch64.zip` package on `macos-latest`, containing `GB Cycle.app`, `README.md`, both license files, and any `libSDL3*.dylib` found recursively under `target/release-max` inside `Contents/MacOS`; when a dylib is present, the dylib install names are rewritten to `@executable_path` before ad-hoc signing, when no dylib is present the workflow fails if `otool -L` still reports an SDL3 dynamic dependency, and the bundle reuses `crates/gb-desktop/macos/Info.plist` so the release artifact carries the same Pocket Camera usage string as the local macOS launcher. Manual `workflow_dispatch` runs are for artifact validation and upload the package through `actions/upload-artifact`. Tag pushes matching `v*` additionally attach the same package to the GitHub Release via `softprops/action-gh-release`; do not use a non-release branch tag unless you intentionally want to test the release-asset path and then clean up the temporary release/tag afterward. From 954d8985e78c3caddae9cce81d484ff4627a47eb Mon Sep 17 00:00:00 2001 From: Francis Date: Tue, 12 May 2026 19:46:39 +0200 Subject: [PATCH 4/4] fix(ci): deduplicate sdl release libraries --- .github/workflows/release-linux.yml | 14 +++++++++++++- .github/workflows/release-macos.yml | 14 +++++++++++++- docs/frontends/DESKTOP.md | 4 ++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index a8c88738..9348b28d 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -73,8 +73,20 @@ jobs: cp README.md LICENSE-MIT LICENSE-APACHE "${package_dir}/" sdl_libs=() + sdl_lib_names=() while IFS= read -r sdl_lib; do - sdl_libs+=("${sdl_lib}") + sdl_name="$(basename "${sdl_lib}")" + sdl_seen=false + for existing_name in "${sdl_lib_names[@]}"; do + if [[ "${existing_name}" == "${sdl_name}" ]]; then + sdl_seen=true + break + fi + done + if [[ "${sdl_seen}" == false ]]; then + sdl_lib_names+=("${sdl_name}") + sdl_libs+=("${sdl_lib}") + fi done < <( find target/release-max \( -type f -o -type l \) -name 'libSDL3*.so*' -print | sort -u diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index 1f53173b..8498595e 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -49,8 +49,20 @@ jobs: cp README.md LICENSE-MIT LICENSE-APACHE "${package_dir}/" sdl_libs=() + sdl_lib_names=() while IFS= read -r sdl_lib; do - sdl_libs+=("${sdl_lib}") + sdl_name="$(basename "${sdl_lib}")" + sdl_seen=false + for existing_name in "${sdl_lib_names[@]}"; do + if [[ "${existing_name}" == "${sdl_name}" ]]; then + sdl_seen=true + break + fi + done + if [[ "${sdl_seen}" == false ]]; then + sdl_lib_names+=("${sdl_name}") + sdl_libs+=("${sdl_lib}") + fi done < <( find target/release-max \( -type f -o -type l \) -name 'libSDL3*.dylib' -print | sort -u diff --git a/docs/frontends/DESKTOP.md b/docs/frontends/DESKTOP.md index 10bec46b..e1b66013 100644 --- a/docs/frontends/DESKTOP.md +++ b/docs/frontends/DESKTOP.md @@ -41,8 +41,8 @@ The GitHub release workflows package only the SDL3 desktop frontend. They all bu The workflows live under `.github/workflows/`: - `release-windows.yml` creates `gb-cycle-windows-x86_64.zip` on `windows-latest`, containing `gb-desktop.exe`, `README.md`, both license files, and every `.dll` emitted recursively by the source-build path under `target/release-max`. -- `release-linux.yml` creates `gb-cycle-linux-x86_64.tar.gz` on `ubuntu-latest`, containing `gb-desktop`, `README.md`, both license files, and any `libSDL3*.so*` found recursively under `target/release-max`; when a shared object is present, the binary is patched with an `$ORIGIN` rpath so it loads the bundled SDL3 shared object from the unpacked artifact directory without requiring `LD_LIBRARY_PATH`, and when no shared object is present the workflow fails if `ldd` still reports an SDL3 dynamic dependency. -- `release-macos.yml` creates only the Apple Silicon `gb-cycle-macos-aarch64.zip` package on `macos-latest`, containing `GB Cycle.app`, `README.md`, both license files, and any `libSDL3*.dylib` found recursively under `target/release-max` inside `Contents/MacOS`; when a dylib is present, the dylib install names are rewritten to `@executable_path` before ad-hoc signing, when no dylib is present the workflow fails if `otool -L` still reports an SDL3 dynamic dependency, and the bundle reuses `crates/gb-desktop/macos/Info.plist` so the release artifact carries the same Pocket Camera usage string as the local macOS launcher. +- `release-linux.yml` creates `gb-cycle-linux-x86_64.tar.gz` on `ubuntu-latest`, containing `gb-desktop`, `README.md`, both license files, and any `libSDL3*.so*` found recursively under `target/release-max` and deduplicated by library filename; when a shared object is present, the binary is patched with an `$ORIGIN` rpath so it loads the bundled SDL3 shared object from the unpacked artifact directory without requiring `LD_LIBRARY_PATH`, and when no shared object is present the workflow fails if `ldd` still reports an SDL3 dynamic dependency. +- `release-macos.yml` creates only the Apple Silicon `gb-cycle-macos-aarch64.zip` package on `macos-latest`, containing `GB Cycle.app`, `README.md`, both license files, and any `libSDL3*.dylib` found recursively under `target/release-max` and deduplicated by library filename inside `Contents/MacOS`; when a dylib is present, the dylib install names are rewritten to `@executable_path` before ad-hoc signing, when no dylib is present the workflow fails if `otool -L` still reports an SDL3 dynamic dependency, and the bundle reuses `crates/gb-desktop/macos/Info.plist` so the release artifact carries the same Pocket Camera usage string as the local macOS launcher. Manual `workflow_dispatch` runs are for artifact validation and upload the package through `actions/upload-artifact`. Tag pushes matching `v*` additionally attach the same package to the GitHub Release via `softprops/action-gh-release`; do not use a non-release branch tag unless you intentionally want to test the release-asset path and then clean up the temporary release/tag afterward.