diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bb5b18d..35533cd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,11 @@ name: ci # on: push: - # Integration branch + release branch. PRs already run via pull_request - # (any base); this list is only for post-merge push runs. branches: [dev, main] + # PRs into dev only: a dev -> main ship PR reuses the required checks the + # dev push already put on the same head SHA, so it needs no second run. pull_request: + branches: [dev] # a force-push while CI is mid-run should cancel the stale run, not queue concurrency: @@ -318,52 +319,10 @@ jobs: run: | set -euo pipefail scripts/build_linux_arm64.sh --version 0.0.0 + # Shared with the release workflow so shipped images get the same + # self-contained / glibc-floor checks as PR builds. - name: Verify the AppImage is self-contained and bullseye-compatible - run: | - set -euo pipefail - image="dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage" - - # --appimage-extract needs no FUSE, so this works on a runner - # without /dev/fuse and still exercises the real payload. - "$image" --appimage-extract >/dev/null - for required in AppRun bin/love game.love lib/liblove-11.5.so; do - [ -e "squashfs-root/$required" ] \ - || { echo "::error::AppImage is missing $required"; exit 1; } - done - - # Every bundled object must resolve once AppRun's LD_LIBRARY_PATH is - # applied; an unresolved soname here is a user-visible launch crash. - # - # This runs on a HEADLESS runner on purpose, and that is the point. - # The first version of this build bundled Debian's SDL2, which - # hard-links libpulse/libasound/libX11/libwayland, so it only ever - # started on a full desktop -- a bare runner is what exposed it. - missing="$(LD_LIBRARY_PATH="$PWD/squashfs-root/lib" \ - ldd squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \ - | grep 'not found' || true)" - [ -z "$missing" ] || { echo "::error::unresolved deps:"; echo "$missing"; exit 1; } - - # Nothing may hard-link a driver, session or audio-stack library: - # those must be reached through dlopen so the AppImage runs on a box - # with only ALSA, only Wayland, or only KMSDRM. - linked="$(for f in squashfs-root/bin/love squashfs-root/lib/*.so*; do - objdump -p "$f" 2>/dev/null | awk '/NEEDED/{print $2}' - done | sort -u | grep -E '^lib(pulse|asound|X11|wayland|GL|EGL|drm|gbm|xcb|cairo|sndio|dbus)' || true)" - [ -z "$linked" ] \ - || { echo "::error::these must be dlopened, not linked:"; echo "$linked"; exit 1; } - - # The whole point of compiling on bullseye. If a future change moves - # the builder to a newer base, the glibc floor silently rises and - # every user on an older distro gets "GLIBC_2.xx not found" -- catch - # it here instead of in a release. - floor="$(objdump -T squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \ - | grep -o 'GLIBC_[0-9.]*' | sort -V | tail -1)" - echo "highest required glibc symbol version: $floor" - [ -n "$floor" ] \ - || { echo "::error::found no versioned glibc symbols -- objdump read nothing"; exit 1; } - highest="$(printf '%s\n' "$floor" "GLIBC_2.31" | sort -V | tail -1)" - [ "$highest" = "GLIBC_2.31" ] \ - || { echo "::error::AppImage requires $floor, above the bullseye 2.31 floor"; exit 1; } + run: bash scripts/linux-arm64/verify_appimage.sh dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage - name: Upload the AppImage uses: actions/upload-artifact@v7 with: @@ -400,7 +359,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - run: sudo apt-get update && sudo apt-get install -y luajit - run: python3 -m pip install --upgrade pillow # the fixture PNGs are committed (they are 8x8 placeholders, not @@ -421,13 +379,8 @@ jobs: print(f"\n{len(paths)} fixture assets valid") PY - # the fingerprint golden is the parity tripwire; prove it still - # matches the dataset on a clean checkout - - name: fingerprint gate - run: luajit tests/engine/gate_fingerprint.lua - - - name: parity-guarantee meta-test - run: luajit tests/engine/gate_meta_coverage.lua + # the fingerprint parity gates (gate_fingerprint / gate_meta_coverage) + # run in the headless job via run_engine; this job only guards the PNGs # Only the differ is under test here, and the job is named for that. The # capture half of the golden pipeline does not exist: a POKEPORT_DRIVER diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7384709..b41bc888 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,6 +161,8 @@ jobs: scripts/build_linux_arm64.sh \ --version "${{ needs.version.outputs.version }}" \ --game-love .bazinga/work/game.love + - name: Verify the AppImage is self-contained and bullseye-compatible + run: bash scripts/linux-arm64/verify_appimage.sh "dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage" - name: Upload Linux arm64 release uses: actions/upload-artifact@v7 with: @@ -285,7 +287,7 @@ jobs: retention-days: 1 release: - needs: [version, xbox-uwp, linux-arm64, native-tls-win] + needs: [version, love-payload, xbox-uwp, linux-arm64, native-tls-win] runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }} steps: @@ -307,6 +309,15 @@ jobs: name: gen1tls-win-x64 path: dist/native/win-x64 + # The same game.love the arm64 AppImage and Xbox UWP builds fused, so + # every release asset ships one identical payload (build.sh's own pack + # would omit PATCH_NOTES.md and mobile/ios/app-repo.json). + - name: Download shared payload + uses: actions/download-artifact@v8 + with: + name: gen1recomp-release-love + path: dist/payload + - name: Import signing certificate into a temporary keychain if: github.repository == 'bryanthaboi/gen1recomp' run: | @@ -357,7 +368,8 @@ jobs: echo "::error::gen1tls.dll missing at $GEN1TLS_DLL (native-tls-win job)" exit 1 fi - scripts/build.sh all --version "${{ needs.version.outputs.version }}" --no-notarize + scripts/build.sh all --version "${{ needs.version.outputs.version }}" --no-notarize \ + --game-love dist/payload/game.love unzip -l dist/win/gen1recomp-win64.zip | grep -F gen1tls.dll \ || { echo "::error::Windows zip is missing gen1tls.dll"; exit 1; } diff --git a/docs/linux-arm64-build.md b/docs/linux-arm64-build.md index b02e0ced..b36a34ab 100644 --- a/docs/linux-arm64-build.md +++ b/docs/linux-arm64-build.md @@ -105,8 +105,9 @@ trixie. This is a statement about the *compile environment*, not about where the artifact runs — building on your own newer distro would silently raise that floor and strand every user on an older one, with no symptom until they -download it. CI enforces the floor: `linux-arm64-build` fails if the highest -required glibc symbol version climbs above 2.31. +download it. `scripts/linux-arm64/verify_appimage.sh` enforces the floor in +both CI (`linux-arm64-build`) and the release workflow: the build fails if +the highest required glibc symbol version climbs above 2.31. ### Why five libraries are built from source @@ -172,13 +173,15 @@ Three jobs, path-gated on `scripts/build_linux_arm64.sh`, exclude list still classifies known sonames correctly, that AppRun still launches `game.love` with `--fused`, and that the host-arch guard actually fires. Needs no container and no arm64 machine. -- **`linux-arm64-build`** (`ubuntu-24.04-arm`) — the real build, then extracts - the artifact and asserts the layout, that every bundled object resolves - under AppRun's `LD_LIBRARY_PATH`, and that the glibc floor is still ≤ 2.31. - Uploads the AppImage for 7 days. +- **`linux-arm64-build`** (`ubuntu-24.04-arm`) — the real build, then + `scripts/linux-arm64/verify_appimage.sh` extracts the artifact and asserts + the layout, that every bundled object resolves under AppRun's + `LD_LIBRARY_PATH`, and that the glibc floor is still ≤ 2.31. Uploads the + AppImage for 7 days. - **release** — `linux-arm64` runs on `ubuntu-24.04-arm`, reuses the shared - `game.love` from the `love-payload` job, and the AppImage is staged and - published like every other release asset. + `game.love` from the `love-payload` job, runs the same + `verify_appimage.sh` checks on the shipped image, and the AppImage is + staged and published like every other release asset. Unlike the Switch job, none of this needs secrets or self-hosted hardware, so it runs on fork PRs too. diff --git a/scripts/build.sh b/scripts/build.sh index 0336ea78..10b3aec8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -6,6 +6,7 @@ # # Usage: scripts/build.sh [mac|win|linux|android|ios|all] [--version X.Y.Z] [--identity "Developer ID Application: ..."] # [--notary-profile NAME] [--no-notarize] +# [--game-love PATH] # fuse a prebuilt payload (scripts/pack_love.sh) instead of packing one # [--release] # ios only: release config instead of debug # # Output: dist/mac/gen1recomp-macos.zip @@ -36,6 +37,7 @@ NOTARY_PROFILE="notary-profile" NOTARIZE=true IOS_RELEASE=false IOS_IPA=false +GAME_LOVE_IN="" say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; } @@ -48,6 +50,7 @@ while [ $# -gt 0 ]; do --identity) IDENTITY="$2"; shift ;; --notary-profile) NOTARY_PROFILE="$2"; shift ;; --no-notarize) NOTARIZE=false ;; + --game-love) GAME_LOVE_IN="${2:?--game-love needs a path}"; shift ;; --release) IOS_RELEASE=true ;; --ipa) IOS_IPA=true ;; *) fail "unknown argument: $1" ;; @@ -62,17 +65,23 @@ mkdir -p "$CACHE" "$WORK" "$DIST/mac" "$DIST/win" "$DIST/linux" # launcher's Edit button on a save row opens it in-process (main.lua), and # `--editor` / POKEPORT_EDITOR=1 opens it standalone. It is required through # love.filesystem's require path, so it has to live inside the archive. -say "packing game.love" LOVE_FILE="$WORK/game.love" rm -f "$LOVE_FILE" -# The launcher UI kit lives at src/ui/kit (inside src/, packed wholesale); -# the vendored libs/flexlove tree it replaced is gone. -(cd "$ROOT" && zip -q -9 -r "$LOVE_FILE" \ - main.lua conf.lua src data assets tools/save-editor \ - tools/rom_manifest.json tools/rom_manifest_blue.json \ - tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \ - tools/rom_manifest_silver.json \ - -x '*.DS_Store' 'data/generated/*' 'assets/generated/*') +if [ -n "$GAME_LOVE_IN" ]; then + [ -f "$GAME_LOVE_IN" ] || fail "--game-love: no such file: $GAME_LOVE_IN" + say "using prebuilt payload: $GAME_LOVE_IN" + cp "$GAME_LOVE_IN" "$LOVE_FILE" +else + say "packing game.love" + # The launcher UI kit lives at src/ui/kit (inside src/, packed wholesale); + # the vendored libs/flexlove tree it replaced is gone. + (cd "$ROOT" && zip -q -9 -r "$LOVE_FILE" \ + main.lua conf.lua src data assets tools/save-editor \ + tools/rom_manifest.json tools/rom_manifest_blue.json \ + tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \ + tools/rom_manifest_silver.json \ + -x '*.DS_Store' 'data/generated/*' 'assets/generated/*') +fi # Materialize the listing once and grep the file: piping unzip straight into # grep -q under `set -o pipefail` SIGPIPEs unzip when grep exits early on a # match, and the pipeline's failure reads as "missing " for whichever @@ -107,18 +116,26 @@ say "game.love: $(du -h "$LOVE_FILE" | cut -f1)" # mistaken for a release. The stamp is then read back out of the archive and the # build fails if it did not take. if printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then - say "stamping engine version $VERSION into game.love" - stamp_dir="$WORK/stamp" - rm -rf "$stamp_dir" - mkdir -p "$stamp_dir/src/core" - sed -E "s/(engine[[:space:]]*=[[:space:]]*\")[^\"]*(\")/\1$VERSION\2/" \ - "$ROOT/src/core/Version.lua" > "$stamp_dir/src/core/Version.lua" - (cd "$stamp_dir" && zip -q "$LOVE_FILE" src/core/Version.lua) - version_re="$(printf '%s' "$VERSION" | sed 's/\./\\./g')" - unzip -p "$LOVE_FILE" src/core/Version.lua \ - | grep -Eq "engine[[:space:]]*=[[:space:]]*\"$version_re\"" \ - || fail "version stamp failed: game.love does not report engine $VERSION" - say "stamped engine version: $VERSION" + if [ -n "$GAME_LOVE_IN" ]; then + version_re="$(printf '%s' "$VERSION" | sed 's/\./\\./g')" + unzip -p "$LOVE_FILE" src/core/Version.lua \ + | grep -Eq "engine[[:space:]]*=[[:space:]]*\"$version_re\"" \ + || fail "prebuilt payload does not report engine $VERSION (pack it with pack_love.sh --version $VERSION)" + say "prebuilt payload already stamped: $VERSION" + else + say "stamping engine version $VERSION into game.love" + stamp_dir="$WORK/stamp" + rm -rf "$stamp_dir" + mkdir -p "$stamp_dir/src/core" + sed -E "s/(engine[[:space:]]*=[[:space:]]*\")[^\"]*(\")/\1$VERSION\2/" \ + "$ROOT/src/core/Version.lua" > "$stamp_dir/src/core/Version.lua" + (cd "$stamp_dir" && zip -q "$LOVE_FILE" src/core/Version.lua) + version_re="$(printf '%s' "$VERSION" | sed 's/\./\\./g')" + unzip -p "$LOVE_FILE" src/core/Version.lua \ + | grep -Eq "engine[[:space:]]*=[[:space:]]*\"$version_re\"" \ + || fail "version stamp failed: game.love does not report engine $VERSION" + say "stamped engine version: $VERSION" + fi else say "version '$VERSION' is not X.Y.Z, shipping default engine (no stamp)" fi diff --git a/scripts/linux-arm64/verify_appimage.sh b/scripts/linux-arm64/verify_appimage.sh new file mode 100755 index 00000000..b437ebc5 --- /dev/null +++ b/scripts/linux-arm64/verify_appimage.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Verifies a built arm64 AppImage is self-contained and bullseye-compatible. +# Usage: scripts/linux-arm64/verify_appimage.sh + +set -euo pipefail + +image="${1:?usage: verify_appimage.sh }" +[ -f "$image" ] || { echo "::error::no such AppImage: $image"; exit 1; } +image="$(cd "$(dirname "$image")" && pwd)/$(basename "$image")" + +workdir="$(mktemp -d)" +trap 'rm -rf "$workdir"' EXIT +cd "$workdir" + +# --appimage-extract needs no FUSE, so this works on a runner +# without /dev/fuse and still exercises the real payload. +"$image" --appimage-extract >/dev/null +for required in AppRun bin/love game.love lib/liblove-11.5.so; do + [ -e "squashfs-root/$required" ] \ + || { echo "::error::AppImage is missing $required"; exit 1; } +done + +# Every bundled object must resolve once AppRun's LD_LIBRARY_PATH is +# applied; an unresolved soname here is a user-visible launch crash. +# +# This runs on a HEADLESS runner on purpose, and that is the point. +# The first version of this build bundled Debian's SDL2, which +# hard-links libpulse/libasound/libX11/libwayland, so it only ever +# started on a full desktop -- a bare runner is what exposed it. +missing="$(LD_LIBRARY_PATH="$PWD/squashfs-root/lib" \ + ldd squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \ + | grep 'not found' || true)" +[ -z "$missing" ] || { echo "::error::unresolved deps:"; echo "$missing"; exit 1; } + +# Nothing may hard-link a driver, session or audio-stack library: +# those must be reached through dlopen so the AppImage runs on a box +# with only ALSA, only Wayland, or only KMSDRM. +linked="$(for f in squashfs-root/bin/love squashfs-root/lib/*.so*; do + objdump -p "$f" 2>/dev/null | awk '/NEEDED/{print $2}' +done | sort -u | grep -E '^lib(pulse|asound|X11|wayland|GL|EGL|drm|gbm|xcb|cairo|sndio|dbus)' || true)" +[ -z "$linked" ] \ + || { echo "::error::these must be dlopened, not linked:"; echo "$linked"; exit 1; } + +# The whole point of compiling on bullseye. If a future change moves +# the builder to a newer base, the glibc floor silently rises and +# every user on an older distro gets "GLIBC_2.xx not found" -- catch +# it here instead of in a release. +floor="$(objdump -T squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \ + | grep -o 'GLIBC_[0-9.]*' | sort -V | tail -1)" +echo "highest required glibc symbol version: $floor" +[ -n "$floor" ] \ + || { echo "::error::found no versioned glibc symbols -- objdump read nothing"; exit 1; } +highest="$(printf '%s\n' "$floor" "GLIBC_2.31" | sort -V | tail -1)" +[ "$highest" = "GLIBC_2.31" ] \ + || { echo "::error::AppImage requires $floor, above the bullseye 2.31 floor"; exit 1; } + +echo "AppImage verified: $image"