From ca90a89d68479efc4e9933991c0f2c9f88bf8ab7 Mon Sep 17 00:00:00 2001 From: Shane McGovern <45065431+Nexhas28@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:37:08 +0100 Subject: [PATCH] Ship pinned modern LuaJIT in the aarch64 Linux builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The linux-arm64 AppImage, the SBC PortMaster zip and the RG34XXSP zip shipped LuaJIT 2.1.0-beta3 (2017): Debian bullseye's libluajit-5.1-dev for the AppImage, PortMaster's love_11.5 runtime library for the zips. With the JIT enabled, beta3 segfaulted 3/3 runs on a TrimUI Brick; LuaJIT v2.1 commit c6ffc141 ran a 26-minute JIT-on soak without a crash. - Build LuaJIT c6ffc141 from a sha256-pinned tarball in a digest-pinned debian:bullseye (glibc 2.31) with the same apt sources as the AppImage builder; compile with PREFIX=/usr so no build path is baked into package.path, stage-install, and verify before use. - AppImage: compile it inside the builder instead of libluajit-5.1-dev, bundle it with jit/*.lua and its license; cache keys include the LuaJIT commit, recipe hash and Dockerfile hash. - PortMaster zips: replace libs.aarch64/libluajit-5.1.so.2 with the pinned build, pin the PortMaster runtime to commit 715f50fd with sha256 checks (rg34xxsp followed main), ship LuaJIT-COPYRIGHT, verify the finished zip. - verify_luajit.sh gates every package: version string, not beta, aarch64 ELF, /usr module path, max GLIBC <= 2.31; offline selftests cover it and the PortMaster helpers. - CI/release build LuaJIT once on an arm64 runner and pass it to the zip jobs via GEN1RECOMP_LUAJIT_LIB. The JIT stays off by default (LÖVE's jitsetup disables it on arm/arm64). POKEPORT_JIT=1 needs stability testing on H700, RK3566 and A133P devices before it can be enabled by default. --- .github/workflows/ci.yml | 52 +++++++++- .github/workflows/release.yml | 50 +++++++++- build-linux-arm-sbc.sh | 41 +++++--- build-rg34xxsp.sh | 51 +++++----- docs/anbernic-rg34xxsp.md | 26 +++++ docs/linux-arm-sbc.md | 38 ++++++++ docs/linux-arm64-build.md | 20 ++++ docs/portmaster-submission.md | 4 +- scripts/build_linux_arm64.sh | 7 ++ scripts/lib/fetch.sh | 63 ++++++++++++ scripts/linux-arm64/Dockerfile | 9 +- scripts/linux-arm64/build_appimage.sh | 75 +++++++++++++- scripts/linux-arm64/common.sh | 79 ++++----------- .../linux-arm64/selftest_build_linux_arm64.sh | 50 +++++++++- scripts/linux-arm64/verify_appimage.sh | 10 ++ scripts/luajit/build_luajit.sh | 82 ++++++++++++++++ scripts/luajit/compile_luajit.sh | 62 ++++++++++++ scripts/luajit/pins.sh | 31 ++++++ scripts/luajit/portmaster_runtime.sh | 74 ++++++++++++++ scripts/luajit/selftest_portmaster_runtime.sh | 97 +++++++++++++++++++ scripts/luajit/verify_luajit.sh | 60 ++++++++++++ 21 files changed, 868 insertions(+), 113 deletions(-) create mode 100755 scripts/lib/fetch.sh create mode 100755 scripts/luajit/build_luajit.sh create mode 100755 scripts/luajit/compile_luajit.sh create mode 100755 scripts/luajit/pins.sh create mode 100755 scripts/luajit/portmaster_runtime.sh create mode 100755 scripts/luajit/selftest_portmaster_runtime.sh create mode 100755 scripts/luajit/verify_luajit.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbf4b471..a5b91621 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -283,7 +283,7 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" exit 0 fi - if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_linux_arm64\.sh$|scripts/linux-arm64/|scripts/pack_love\.sh$|docs/linux-arm64-build\.md$|\.github/workflows/(ci|release)\.yml$)'; then + if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_linux_arm64\.sh$|scripts/linux-arm64/|scripts/luajit/|scripts/lib/|scripts/pack_love\.sh$|docs/linux-arm64-build\.md$|\.github/workflows/(ci|release)\.yml$)'; then echo "changed=true" >> "$GITHUB_OUTPUT" else echo "changed=false" >> "$GITHUB_OUTPUT" @@ -302,6 +302,8 @@ jobs: # so the slow native job below only ever starts on a sane tree. - name: Linux arm64 offline selftest run: bash scripts/linux-arm64/selftest_build_linux_arm64.sh + - name: PortMaster LuaJIT helpers offline selftest + run: bash scripts/luajit/selftest_portmaster_runtime.sh linux-arm64-build: name: Linux arm64 AppImage build @@ -486,12 +488,53 @@ jobs: if-no-files-found: error retention-days: 7 + luajit-linux-arm64: + name: build pinned LuaJIT (linux arm64) + # Native arm64 runner: emulated aarch64 builds have miscompiled LuaJIT. + # Built once here and shared by both PortMaster zip jobs below. + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v7 + # The zip scripts depend on these helpers; fail fast before the build. + - name: PortMaster LuaJIT helpers offline selftest + run: bash scripts/luajit/selftest_portmaster_runtime.sh + - name: Cache pinned LuaJIT + uses: actions/cache@v6 + with: + path: .bazinga/cache/luajit + key: luajit-linux-arm64-${{ hashFiles('scripts/luajit/**', 'scripts/lib/fetch.sh') }} + - name: Build pinned LuaJIT + run: | + set -euo pipefail + lib="$(scripts/luajit/build_luajit.sh)" + mkdir -p dist/luajit + cp "$lib" "$(dirname "$lib")/COPYRIGHT" dist/luajit/ + - name: Verify LuaJIT + run: scripts/luajit/verify_luajit.sh dist/luajit/libluajit-5.1.so.2 + - name: Upload LuaJIT + uses: actions/upload-artifact@v7 + with: + name: luajit-linux-arm64 + path: | + dist/luajit/libluajit-5.1.so.2 + dist/luajit/COPYRIGHT + if-no-files-found: error + retention-days: 7 + rg34xxsp-build: name: RG34XXSP PortMaster build + needs: luajit-linux-arm64 runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + - name: Download pinned LuaJIT + uses: actions/download-artifact@v8 + with: + name: luajit-linux-arm64 + path: dist/native/luajit-linux-arm64 - name: build RG34XXSP package + env: + GEN1RECOMP_LUAJIT_LIB: ${{ github.workspace }}/dist/native/luajit-linux-arm64/libluajit-5.1.so.2 run: ./build-rg34xxsp.sh --version 0.0.0 - name: upload RG34XXSP artifact uses: actions/upload-artifact@v7 @@ -503,12 +546,19 @@ jobs: linux-arm-sbc-build: name: Linux ARM SBC PortMaster build + needs: luajit-linux-arm64 runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + - name: Download pinned LuaJIT + uses: actions/download-artifact@v8 + with: + name: luajit-linux-arm64 + path: dist/native/luajit-linux-arm64 - name: build Linux ARM SBC package env: GEN1RECOMP_SOURCE_DIR: ${{ github.workspace }} + GEN1RECOMP_LUAJIT_LIB: ${{ github.workspace }}/dist/native/luajit-linux-arm64/libluajit-5.1.so.2 run: ./build-linux-arm-sbc.sh --version 0.0.0 - name: upload Linux ARM SBC artifact uses: actions/upload-artifact@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d9e4653..39daa196 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,7 +165,7 @@ jobs: uses: actions/cache@v6 with: path: .bazinga/cache/linux-arm64 - key: linux-arm64-deps-${{ hashFiles('scripts/linux-arm64/**', 'scripts/build_linux_arm64.sh') }} + key: linux-arm64-deps-${{ hashFiles('scripts/linux-arm64/**', 'scripts/luajit/**', 'scripts/build_linux_arm64.sh') }} - name: Build Linux arm64 AppImage env: @@ -767,9 +767,39 @@ jobs: if-no-files-found: error retention-days: 7 + luajit-linux-arm64: + name: build pinned LuaJIT (linux arm64) + needs: version + # Native arm64 runner: emulated aarch64 builds have miscompiled LuaJIT. + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v7 + - name: Cache pinned LuaJIT + uses: actions/cache@v6 + with: + path: .bazinga/cache/luajit + key: luajit-linux-arm64-${{ hashFiles('scripts/luajit/**', 'scripts/lib/fetch.sh') }} + - name: Build pinned LuaJIT + run: | + set -euo pipefail + lib="$(scripts/luajit/build_luajit.sh)" + mkdir -p dist/luajit + cp "$lib" "$(dirname "$lib")/COPYRIGHT" dist/luajit/ + - name: Verify LuaJIT + run: scripts/luajit/verify_luajit.sh dist/luajit/libluajit-5.1.so.2 + - name: Upload LuaJIT + uses: actions/upload-artifact@v7 + with: + name: luajit-linux-arm64 + path: | + dist/luajit/libluajit-5.1.so.2 + dist/luajit/COPYRIGHT + if-no-files-found: error + retention-days: 7 + rg34xxsp: name: build Anbernic RG34XXSP port - needs: [version, shaderfx-bridge] + needs: [version, shaderfx-bridge, luajit-linux-arm64] runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }} steps: # The self-hosted runner lives under the machine owner's home @@ -787,9 +817,16 @@ jobs: name: shaderfx-bridge-linux-arm64 path: dist/native/linux-arm64 + - name: Download the pinned LuaJIT + uses: actions/download-artifact@v8 + with: + name: luajit-linux-arm64 + path: dist/native/luajit-linux-arm64 + - name: Build Anbernic RG34XXSP port env: SHADERFX_BRIDGE_REQUIRED: "1" + GEN1RECOMP_LUAJIT_LIB: ${{ github.workspace }}/dist/native/luajit-linux-arm64/libluajit-5.1.so.2 run: | set -euo pipefail # Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5 @@ -807,7 +844,7 @@ jobs: linux-arm-sbc: name: build Linux ARM SBC PortMaster port - needs: [version, shaderfx-bridge] + needs: [version, shaderfx-bridge, luajit-linux-arm64] runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }} steps: # The self-hosted runner lives under the machine owner's home @@ -825,6 +862,12 @@ jobs: name: shaderfx-bridge-linux-arm64 path: dist/native/linux-arm64 + - name: Download the pinned LuaJIT + uses: actions/download-artifact@v8 + with: + name: luajit-linux-arm64 + path: dist/native/luajit-linux-arm64 + - name: Build Linux ARM SBC PortMaster port env: # The release workflow must package the commit being released. The @@ -833,6 +876,7 @@ jobs: GEN1RECOMP_SOURCE_DIR: ${{ github.workspace }} GEN1RECOMP_RELEASE_TAG: v${{ needs.version.outputs.version }} SHADERFX_BRIDGE_REQUIRED: "1" + GEN1RECOMP_LUAJIT_LIB: ${{ github.workspace }}/dist/native/luajit-linux-arm64/libluajit-5.1.so.2 run: | set -euo pipefail # Same aarch64 PortMaster-style pack for Linux ARM SBC PortMaster. The build diff --git a/build-linux-arm-sbc.sh b/build-linux-arm-sbc.sh index 6307018e..421e0606 100755 --- a/build-linux-arm-sbc.sh +++ b/build-linux-arm-sbc.sh @@ -11,6 +11,11 @@ # GEN1RECOMP_SOURCE_DIR="$PWD" ./build-linux-arm-sbc.sh --version X.Y.Z # ./build-linux-arm-sbc.sh --source /path/to/gen1recomp --version X.Y.Z # +# LuaJIT: the zip ships a pinned LuaJIT, compiled during the build, so building +# needs an aarch64 host with docker or podman. Alternatively set +# GEN1RECOMP_LUAJIT_LIB=/path/libluajit-5.1.so.2 with its COPYRIGHT beside it +# (or GEN1RECOMP_LUAJIT_COPYRIGHT=/path/to/COPYRIGHT). +# # Output: # dist/linux-arm-sbc/gen1recomp-sbc-portmaster.zip # @@ -45,8 +50,6 @@ SOURCE_DIR_OVERRIDE="${GEN1RECOMP_SOURCE_DIR:-}" SOURCE_TAG_OVERRIDE="${GEN1RECOMP_RELEASE_TAG:-}" VERSION="${GEN1RECOMP_VERSION:-}" -# Official PortMaster LÖVE 11.5 aarch64 runtime (small love stub + liblove). -PM_RUNTIME_BASE="https://raw.githubusercontent.com/PortsMaster/PortMaster-GUI/main/PortMaster/runtimes/love_${LOVE_VERSION}" RELEASES_LATEST_URL="https://github.com/bryanthaboi/gen1recomp/releases/latest" RELEASE_TARBALL_BASE="https://github.com/bryanthaboi/gen1recomp/archive/refs/tags" @@ -60,7 +63,7 @@ while [ $# -gt 0 ]; do --source) [ $# -ge 2 ] || fail "--source needs a directory"; SOURCE_DIR_OVERRIDE="$2"; shift ;; --release-tag) [ $# -ge 2 ] || fail "--release-tag needs a tag"; SOURCE_TAG_OVERRIDE="$2"; shift ;; -h|--help) - sed -n '2,24p' "$0" + sed -n '2,29p' "$0" exit 0 ;; *) fail "unknown argument: $1" ;; @@ -86,6 +89,10 @@ download() { mv "$dest.tmp" "$dest" } +# PortMaster pins + LuaJIT pin (LUAJIT_COMMIT, ...) + download/verify helpers. +# shellcheck source=scripts/luajit/portmaster_runtime.sh +. "$ROOT/scripts/luajit/portmaster_runtime.sh" + # --------------------------------------------------------------- source + game tree # Release builds use the latest published source archive. A local checkout is # an explicit override for development and for CI's just-built release source. @@ -171,15 +178,12 @@ fi say "fetching LÖVE $LOVE_VERSION aarch64 runtime" LOVE_BIN="$CACHE/love.aarch64" LOVE_LIB="$CACHE/liblove-11.5.so" -LUAJIT_LIB="$CACHE/libluajit-5.1.so.2" MODPLUG_LIB="$CACHE/libmodplug.so.1" OGG_LIB="$CACHE/libogg.so.0" -download "$PM_RUNTIME_BASE/love.aarch64" "$LOVE_BIN" -download "$PM_RUNTIME_BASE/libs.aarch64/liblove-11.5.so" "$LOVE_LIB" -download "$PM_RUNTIME_BASE/libs.aarch64/libluajit-5.1.so.2" "$LUAJIT_LIB" -download "$PM_RUNTIME_BASE/libs.aarch64/libmodplug.so.1" "$MODPLUG_LIB" -download "$PM_RUNTIME_BASE/libs.aarch64/libogg.so.0" "$OGG_LIB" +pm_fetch_runtime "$CACHE" + +pm_select_luajit # Sanity: love stub must be an aarch64 ELF. file "$LOVE_BIN" | grep -qi 'aarch64\|ARM aarch64' \ @@ -197,8 +201,9 @@ mkdir -p "$PORT_ROOT/$PORT_DIR_NAME/bin" \ cp -R "$GAME_SRC" "$PORT_ROOT/$PORT_DIR_NAME/lovegame" cp "$LOVE_BIN" "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64" chmod +x "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64" -cp "$LOVE_LIB" "$LUAJIT_LIB" "$MODPLUG_LIB" "$OGG_LIB" \ +cp "$LOVE_LIB" "$MODPLUG_LIB" "$OGG_LIB" \ "$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/" +pm_stage_luajit "$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64" "$PORT_ROOT/$PORT_DIR_NAME/licenses" BRIDGE_LIB="liblibrashader_bridge.so" BRIDGE_SRC="${SHADERFX_BRIDGE_LINUX_ARM64:-}" @@ -215,10 +220,14 @@ else fi # Drop a short license pointer for the bundled LÖVE bits. -cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <<'EOF' +cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <> "$PORT_ROOT/README.md" </dev/null || echo dev)" -# Official PortMaster LÖVE 11.5 aarch64 runtime (small love stub + liblove). -PM_RUNTIME_BASE="https://raw.githubusercontent.com/PortsMaster/PortMaster-GUI/main/PortMaster/runtimes/love_${LOVE_VERSION}" - say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; } fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } @@ -52,7 +54,7 @@ while [ $# -gt 0 ]; do case "$1" in --version) VERSION="$2"; shift ;; -h|--help) - sed -n '2,24p' "$0" + sed -n '2,29p' "$0" exit 0 ;; *) fail "unknown argument: $1" ;; @@ -66,16 +68,9 @@ command -v unzip >/dev/null || fail "unzip is required" mkdir -p "$CACHE" "$WORK" "$DIST" -download() { - local url="$1" dest="$2" - if [ -f "$dest" ] && [ -s "$dest" ]; then - return 0 - fi - say "downloading $(basename "$dest")" - curl -fL --progress-bar "$url" -o "$dest.tmp" \ - || fail "download failed: $url" - mv "$dest.tmp" "$dest" -} +# PortMaster pins + LuaJIT pin (LUAJIT_COMMIT, ...) + download/verify helpers. +# shellcheck source=scripts/luajit/portmaster_runtime.sh +. "$ROOT/scripts/luajit/portmaster_runtime.sh" # --------------------------------------------------------------- game tree # Unpacked directory (not a .love zip) so the player can drop a .gb next to @@ -129,15 +124,12 @@ fi say "fetching LÖVE $LOVE_VERSION aarch64 runtime" LOVE_BIN="$CACHE/love.aarch64" LOVE_LIB="$CACHE/liblove-11.5.so" -LUAJIT_LIB="$CACHE/libluajit-5.1.so.2" MODPLUG_LIB="$CACHE/libmodplug.so.1" OGG_LIB="$CACHE/libogg.so.0" -download "$PM_RUNTIME_BASE/love.aarch64" "$LOVE_BIN" -download "$PM_RUNTIME_BASE/libs.aarch64/liblove-11.5.so" "$LOVE_LIB" -download "$PM_RUNTIME_BASE/libs.aarch64/libluajit-5.1.so.2" "$LUAJIT_LIB" -download "$PM_RUNTIME_BASE/libs.aarch64/libmodplug.so.1" "$MODPLUG_LIB" -download "$PM_RUNTIME_BASE/libs.aarch64/libogg.so.0" "$OGG_LIB" +pm_fetch_runtime "$CACHE" + +pm_select_luajit # Sanity: love stub must be an aarch64 ELF. file "$LOVE_BIN" | grep -qi 'aarch64\|ARM aarch64' \ @@ -155,8 +147,9 @@ mkdir -p "$PORT_ROOT/$PORT_DIR_NAME/bin" \ cp -R "$GAME_SRC" "$PORT_ROOT/$PORT_DIR_NAME/lovegame" cp "$LOVE_BIN" "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64" chmod +x "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64" -cp "$LOVE_LIB" "$LUAJIT_LIB" "$MODPLUG_LIB" "$OGG_LIB" \ +cp "$LOVE_LIB" "$MODPLUG_LIB" "$OGG_LIB" \ "$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/" +pm_stage_luajit "$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64" "$PORT_ROOT/$PORT_DIR_NAME/licenses" BRIDGE_LIB="liblibrashader_bridge.so" BRIDGE_SRC="${SHADERFX_BRIDGE_LINUX_ARM64:-}" @@ -173,10 +166,14 @@ else fi # Drop a short license pointer for the bundled LÖVE bits. -cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <<'EOF' +cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <> "$PORT_ROOT/README.md" <`. The license ships as `LuaJIT-COPYRIGHT` in the AppImage. + The JIT stays off by default: LÖVE's `jitsetup` disables it on arm64, and + enabling it by default needs stability testing on several devices first. - `APPIMAGE_RUNTIME_TAG` / `APPIMAGE_RUNTIME_SHA256` — always a dated tag from [AppImage/type2-runtime](https://github.com/AppImage/type2-runtime/releases). The selftest fails the build if this ever points at `continuous`. diff --git a/docs/portmaster-submission.md b/docs/portmaster-submission.md index e56555bc..eb206842 100644 --- a/docs/portmaster-submission.md +++ b/docs/portmaster-submission.md @@ -62,7 +62,9 @@ cache are included. The project owner has explicitly permitted PortMaster distribution of the launcher, recorded in the included license. I tested this catalogue candidate on my TrimUI Brick and confirmed it works. -It uses PortMaster's LÖVE 11.5 runtime. I'm looking +It uses PortMaster's LÖVE 11.5 runtime, including that runtime's LuaJIT +2.1.0-beta3 (unlike the release zips, which bundle a pinned modern LuaJIT; see +https://github.com/bryanthaboi/gen1recomp/blob/dev/docs/linux-arm-sbc.md#bundled-luajit), so the JIT must stay off. I'm looking for testing on the firmware and resolutions listed above, especially import, save/reload, controller exit and suspend/resume. Please include device, firmware, package checksum, result and `gen1recomp/log.txt` when reporting a diff --git a/scripts/build_linux_arm64.sh b/scripts/build_linux_arm64.sh index 57a3cda4..69bdf6fb 100755 --- a/scripts/build_linux_arm64.sh +++ b/scripts/build_linux_arm64.sh @@ -125,6 +125,7 @@ download_pinned "$THEORA_URL" "$CACHE/$THEORA_TARBALL" "$THEORA_SHA256" download_pinned "$OGG_URL" "$CACHE/$OGG_TARBALL" "$OGG_SHA256" download_pinned "$VORBIS_URL" "$CACHE/$VORBIS_TARBALL" "$VORBIS_SHA256" download_pinned "$MPG123_URL" "$CACHE/$MPG123_TARBALL" "$MPG123_SHA256" +download_pinned "$LUAJIT_URL" "$CACHE/$LUAJIT_TARBALL" "$LUAJIT_SHA256" download_pinned "$APPIMAGE_RUNTIME_URL" "$CACHE/$APPIMAGE_RUNTIME_NAME" \ "$APPIMAGE_RUNTIME_SHA256" @@ -162,12 +163,18 @@ say "compiling and packaging inside $BUILDER_BASE_IMAGE" -e VORBIS_TARBALL="$VORBIS_TARBALL" \ -e MPG123_VERSION="$MPG123_VERSION" \ -e MPG123_TARBALL="$MPG123_TARBALL" \ + -e LUAJIT_COMMIT="$LUAJIT_COMMIT" \ + -e LUAJIT_TARBALL="$LUAJIT_TARBALL" \ + -e LUAJIT_VERSION_STRING="$LUAJIT_VERSION_STRING" \ + -e LUAJIT_MAX_GLIBC="$LUAJIT_MAX_GLIBC" \ + -e BUILDER_HASH="$BUILDER_HASH" \ -e APP_NAME="$APP_NAME" \ -e VERSION="$VERSION" \ -v "$CACHE:/cache" \ -v "$IN_DIR:/in:ro" \ -v "$OUT_DIR:/out" \ -v "$ROOT/scripts/linux-arm64:/scripts:ro" \ + -v "$ROOT/scripts/luajit:/luajit-scripts:ro" \ "$BUILDER_IMAGE" bash /scripts/build_appimage.sh # --------------------------------------------------------------- publish diff --git a/scripts/lib/fetch.sh b/scripts/lib/fetch.sh new file mode 100755 index 00000000..51915555 --- /dev/null +++ b/scripts/lib/fetch.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Sourceable download/hash/container helpers. No side effects on source. +# The caller must define say, warn and fail. + +# Print SHA-256 hex digest of PATH. Prefers sha256sum, falls back to shasum +# (same order-agnostic pair scripts/switch/common.sh uses). +sha256_file() { + local path="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$path" | awk '{print $1}' + else + fail "need sha256sum or shasum (install coreutils)" + fi +} + +# download_pinned URL DEST EXPECTED_SHA256 +# +# A cache hit is only trusted if it still hashes to the pin: a download +# truncated by a network drop would otherwise be reused forever, which is the +# same trap scripts/build.sh guards for the win64 zip and the x86_64 AppImage. +download_pinned() { + local url="$1" dest="$2" want="$3" got="" + if [ -f "$dest" ]; then + got="$(sha256_file "$dest")" + if [ "$got" = "$want" ]; then + return 0 + fi + warn "cached $(basename "$dest") has the wrong digest, re-downloading" + rm -f "$dest" + fi + say "downloading $(basename "$dest")" + # --retry-all-errors because plain --retry skips TLS handshake failures, + # which is how xiph.org drops these tarballs; the pin below still gates it. + # curl < 7.71 lacks the flag and rejects it, so it is only passed if offered. + local retry_all=() + # No grep -q: it would exit early and SIGPIPE curl, which pipefail reports. + if curl --help all 2>/dev/null | grep -- --retry-all-errors >/dev/null; then + retry_all=(--retry-all-errors) + fi + curl -fL --retry 5 --retry-delay 2 ${retry_all[@]+"${retry_all[@]}"} --progress-bar \ + "$url" -o "$dest.tmp" || fail "download failed: $url" + got="$(sha256_file "$dest.tmp")" + [ "$got" = "$want" ] || fail "$(printf '%s\n expected %s\n got %s' \ + "checksum mismatch for $(basename "$dest")" "$want" "$got")" + mv "$dest.tmp" "$dest" +} + +# Echo the container runtime to use: docker, else podman. +container_runtime() { + if [ -n "${GEN1_CONTAINER_RUNTIME:-}" ]; then + printf '%s' "$GEN1_CONTAINER_RUNTIME" + return 0 + fi + if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then + printf 'docker' + elif command -v podman >/dev/null 2>&1; then + printf 'podman' + else + return 1 + fi +} diff --git a/scripts/linux-arm64/Dockerfile b/scripts/linux-arm64/Dockerfile index 318d9f12..aa2fc14e 100644 --- a/scripts/linux-arm64/Dockerfile +++ b/scripts/linux-arm64/Dockerfile @@ -8,7 +8,8 @@ # # This image is arch-native: build it on an aarch64 host (Raspberry Pi 5, # ubuntu-24.04-arm runner, Apple Silicon Docker) — no qemu emulation. -FROM debian:bullseye +# Must match LUAJIT_BUILD_IMAGE in scripts/luajit/pins.sh (checked by the selftest). +FROM debian:bullseye@sha256:6f519a81440354a85eb592c5f32109ab80605f6b892455983a6f618bf87fabe9 ENV DEBIAN_FRONTEND=noninteractive @@ -18,7 +19,9 @@ ENV DEBIAN_FRONTEND=noninteractive # Note what is deliberately ABSENT: libsdl2-dev, libtheora-dev and # libopenal-dev. All three are built from source instead (see common.sh for # why), and having Debian's copies installed would let pkg-config hand LÖVE's -# configure the system ones and silently undo it. +# configure the system ones and silently undo it. libluajit-5.1-dev is absent +# for the same reason: LuaJIT is built from a pinned commit (scripts/luajit/), +# because bullseye ships 2.1.0-beta3, which segfaulted with the JIT on. # # The remaining lib*-dev set is LÖVE's optional-module surface. A missing one # does not fail configure, it silently drops a module (love.sound decoders, @@ -37,7 +40,7 @@ RUN printf 'deb http://archive.debian.org/debian bullseye main\ndeb http://archi build-essential pkg-config autoconf automake libtool cmake \ ca-certificates curl file xz-utils bzip2 zip unzip squashfs-tools \ libogg-dev libvorbis-dev \ - libmodplug-dev libmpg123-dev libfreetype6-dev libluajit-5.1-dev \ + libmodplug-dev libmpg123-dev libfreetype6-dev \ zlib1g-dev libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev \ libasound2-dev libpulse-dev libudev-dev libdbus-1-dev \ libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev \ diff --git a/scripts/linux-arm64/build_appimage.sh b/scripts/linux-arm64/build_appimage.sh index ad35af1d..c7609839 100755 --- a/scripts/linux-arm64/build_appimage.sh +++ b/scripts/linux-arm64/build_appimage.sh @@ -27,6 +27,11 @@ VORBIS_VERSION="${VORBIS_VERSION:?}" VORBIS_TARBALL="${VORBIS_TARBALL:?}" MPG123_VERSION="${MPG123_VERSION:?}" MPG123_TARBALL="${MPG123_TARBALL:?}" +LUAJIT_COMMIT="${LUAJIT_COMMIT:?}" +LUAJIT_TARBALL="${LUAJIT_TARBALL:?}" +LUAJIT_VERSION_STRING="${LUAJIT_VERSION_STRING:?}" +LUAJIT_MAX_GLIBC="${LUAJIT_MAX_GLIBC:?}" +BUILDER_HASH="${BUILDER_HASH:?}" APP_NAME="${APP_NAME:?}" VERSION="${VERSION:?}" JOBS="${JOBS:-$(nproc)}" @@ -46,12 +51,40 @@ mkdir -p "$WORK" # the only slow part (~5 min cold on a Pi 5) and is identical for every game # version. The key includes every source version, so bumping any of them # invalidates the cache instead of silently reusing a stale mix. -PREFIX="$CACHE/prefix-love$LOVE_VERSION-sdl$SDL2_VERSION-al$OPENAL_VERSION-theora$THEORA_VERSION-ogg$OGG_VERSION-vorbis$VORBIS_VERSION-mpg$MPG123_VERSION" +# The key also carries a hash of the LuaJIT build recipe, so changing it +# rebuilds LuaJIT instead of reusing a prefix built the old way. It also carries +# the builder image's Dockerfile hash, so a toolchain/package change rebuilds +# everything instead of reusing objects compiled by the old image. +LUAJIT_RECIPE="$(sha256sum /luajit-scripts/compile_luajit.sh | cut -c1-12)" +PREFIX="$CACHE/prefix-love$LOVE_VERSION-sdl$SDL2_VERSION-al$OPENAL_VERSION-theora$THEORA_VERSION-ogg$OGG_VERSION-vorbis$VORBIS_VERSION-mpg$MPG123_VERSION-luajit${LUAJIT_COMMIT:0:12}-${LUAJIT_RECIPE}-img${BUILDER_HASH}" export PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" # Our own libraries must win over the system ones during LÖVE's configure and # link, or the whole point of building them is lost. export LD_LIBRARY_PATH="$PREFIX/lib" +# ----------------------------------------------------------- compile LuaJIT +# Built from the pinned commit, not bullseye's 2.1.0-beta3 (which segfaulted +# with the JIT on). The cache key above carries the commit, so a prefix built +# against any other LuaJIT is never reused. +# COPYRIGHT is installed last by compile_luajit.sh, so it marks a finished install. +if [ -f "$PREFIX/lib/libluajit-5.1.so.2" ] && [ -f "$PREFIX/lib/pkgconfig/luajit.pc" ] \ + && [ -s "$PREFIX/share/doc/luajit/COPYRIGHT" ]; then + say "reusing cached LuaJIT ${LUAJIT_COMMIT:0:12}" + bash /luajit-scripts/verify_luajit.sh "$PREFIX/lib/libluajit-5.1.so.2" \ + || fail "the cached LuaJIT in $PREFIX failed verification; re-run with --clean-cache" +else + say "compiling LuaJIT ${LUAJIT_COMMIT:0:12}" + bash /luajit-scripts/compile_luajit.sh "$CACHE/$LUAJIT_TARBALL" "$PREFIX" +fi +# Debian's copy must not be able to win: a stale builder image that still has +# libluajit-5.1-dev would let pkg-config hand LOVE the 2017 beta3. +if ls /usr/lib/aarch64-linux-gnu/libluajit-5.1.so* >/dev/null 2>&1; then + fail "the system libluajit-5.1 is installed in the builder image; rebuild it with --rebuild-image" +fi +luajit_prefix="$(pkg-config --variable=prefix luajit)" +[ "$luajit_prefix" = "$PREFIX" ] \ + || fail "pkg-config resolves luajit to '$luajit_prefix', not $PREFIX" + # ------------------------------------------------------ compile audio codecs # Ordered by dependency: vorbis needs ogg, and theora needs ogg too. All three # are small, plain autotools builds -- well under a minute each. @@ -250,6 +283,38 @@ for soname in libSDL2-2.0.so.0 libopenal.so.1 libfreetype.so.6 \ || fail "liblove is not linked against $soname (a -dev package went missing)" done +# liblove must really be linked against OUR LuaJIT. LD_LIBRARY_PATH points at +# $PREFIX/lib for this whole script, so a plain ldd would resolve there no +# matter how liblove was linked. Prove it without that crutch instead: +# (a) every RPATH/RUNPATH entry stays inside $PREFIX (luajit.pc adds none; +# libtool adds $PREFIX/lib because liblove links the .la libraries built +# there -- SDL2, ogg, vorbis, theora, mpg123 -- and (b) relies on that), +# (b) with LD_LIBRARY_PATH unset, the loader still finds libluajit-5.1.so.2, +# and only at $PREFIX/lib -- via the checked rpath or the system search +# path, so no other LuaJIT on the system could have satisfied the link, and +# (c) when compiled in this run, configure recorded our include dir. +love_rpath="$(objdump -p "$love_lib" | awk '/RPATH|RUNPATH/ {print $2}')" +if [ -n "$love_rpath" ]; then + IFS=: read -ra rpath_dirs <<<"$love_rpath" + for rpath_dir in "${rpath_dirs[@]}"; do + case "$rpath_dir" in + "$PREFIX"|"$PREFIX"/*) ;; + *) fail "liblove has an RPATH/RUNPATH entry outside $PREFIX: $rpath_dir" ;; + esac + done +fi +bare_ldd="$(env -u LD_LIBRARY_PATH ldd "$love_lib")" +luajit_resolved="$(awk '$1 == "libluajit-5.1.so.2" {print $3}' <<<"$bare_ldd")" +[ -n "$luajit_resolved" ] && [ "$luajit_resolved" != "not" ] \ + && [ "$(readlink -f "$luajit_resolved")" = "$(readlink -f "$PREFIX/lib/libluajit-5.1.so.2")" ] \ + || fail "without LD_LIBRARY_PATH liblove resolves libluajit-5.1.so.2 to '${luajit_resolved:-nothing}', not the pinned build in $PREFIX/lib" +say "liblove -> libluajit-5.1.so.2 (no LD_LIBRARY_PATH): $luajit_resolved" +if [ -f "$WORK/love-src/config.log" ]; then + grep -qF -- "-I$PREFIX/include/luajit-2.1" "$WORK/love-src/config.log" \ + || fail "LÖVE's configure did not record -I$PREFIX/include/luajit-2.1" + say "config.log: $(grep -F -m1 -- "-I$PREFIX/include/luajit-2.1" "$WORK/love-src/config.log")" +fi + # --------------------------------------------------------------- AppDir # Layout mirrors LÖVE's own x86_64 AppImage exactly (bin/ lib/ share/ at the # AppDir root, not usr/-prefixed), so the AppRun contract below -- and the @@ -324,6 +389,8 @@ chmod 0644 "$APPDIR/lib/liblove-$LOVE_VERSION.so" BUNDLED["liblove-$LOVE_VERSION.so"]=1 bundle_needed "$APPDIR/bin/love" bundle_needed "$APPDIR/lib/liblove-$LOVE_VERSION.so" +bash /luajit-scripts/verify_luajit.sh "$APPDIR/lib/libluajit-5.1.so.2" \ + || fail "bundled libluajit-5.1.so.2 is not the pinned build" say "bundled $(ls "$APPDIR/lib" | wc -l) libraries: $(ls "$APPDIR/lib" | tr '\n' ' ')" # ------------------------------------------------- host dependency contract @@ -363,8 +430,8 @@ say "host dependency contract holds (glibc, libstdc++ and the font stack only)" # LÖVE loads jit.* (jit.status, the profiler) through LUA_PATH; without these # the modules are simply absent, so ship them the way upstream's image does. -jit_share="$(ls -d /usr/share/luajit-* 2>/dev/null | head -1)" -[ -n "$jit_share" ] || fail "luajit jit/*.lua modules not found under /usr/share" +jit_share="$(ls -d "$PREFIX"/share/luajit-* 2>/dev/null | head -1)" +[ -n "$jit_share" ] || fail "luajit jit/*.lua modules not found under $PREFIX/share" LUAJIT_SHARE_DIR="$(basename "$jit_share")" mkdir -p "$APPDIR/share/$LUAJIT_SHARE_DIR" "$APPDIR/share/lua/5.1" "$APPDIR/lib/lua/5.1" cp -R "$jit_share/jit" "$APPDIR/share/$LUAJIT_SHARE_DIR/" @@ -445,6 +512,8 @@ chmod +x "$APPDIR/AppRun" [ -f "$PREFIX/license.txt" ] || fail "LÖVE license.txt missing from the build prefix" cp "$PREFIX/license.txt" "$APPDIR/license.love2d.txt" +[ -s "$PREFIX/share/doc/luajit/COPYRIGHT" ] || fail "LuaJIT COPYRIGHT missing from the build prefix" +cp "$PREFIX/share/doc/luajit/COPYRIGHT" "$APPDIR/LuaJIT-COPYRIGHT" # --------------------------------------------------------------- fuse image # An AppImage is just . gzip at 128K blocks matches what diff --git a/scripts/linux-arm64/common.sh b/scripts/linux-arm64/common.sh index 292f1416..9a8d9259 100755 --- a/scripts/linux-arm64/common.sh +++ b/scripts/linux-arm64/common.sh @@ -8,6 +8,19 @@ if [ -z "${ROOT:-}" ]; then fi export ROOT +# Pinned LuaJIT (shared with the PortMaster builds). Built from source in the +# builder container rather than taken from bullseye's libluajit-5.1-dev. +# shellcheck source=../luajit/pins.sh +. "$ROOT/scripts/luajit/pins.sh" + +say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; } +fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } + +# sha256_file, download_pinned, container_runtime (shared with scripts/luajit). +# shellcheck source=../lib/fetch.sh +. "$ROOT/scripts/lib/fetch.sh" + # ---------------------------------------------------------------- pins # LÖVE ships no aarch64 binary of any kind -- the 11.5 release has win32/win64, # macOS, Android, iOS and an x86_64 AppImage, and that is the whole list. So @@ -104,69 +117,15 @@ APPIMAGE_RUNTIME_SHA256="00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78 # AppImage cover Raspberry Pi OS bullseye/bookworm/trixie, Ubuntu 20.04+ and # the aarch64 handheld distros. Building on a newer base would silently # restrict the artifact to that base and newer. -BUILDER_BASE_IMAGE="debian:bullseye" -BUILDER_IMAGE="${GEN1_LINUX_ARM64_IMAGE:-gen1recomp-linux-arm64-builder}" +BUILDER_BASE_IMAGE="$LUAJIT_BUILD_IMAGE" +# The tag is derived from the Dockerfile content, so any change to the image +# (packages, apt sources, base digest) yields a new tag and a stale local image +# (e.g. one still carrying libluajit-5.1-dev) is never reused. +BUILDER_HASH="$(sha256_file "$ROOT/scripts/linux-arm64/Dockerfile" | cut -c1-12)" +BUILDER_IMAGE="${GEN1_LINUX_ARM64_IMAGE:-gen1recomp-linux-arm64-builder:$BUILDER_HASH}" APP_NAME="gen1recomp" -say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } -warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; } -fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } - -# Print SHA-256 hex digest of PATH. Prefers sha256sum, falls back to shasum -# (same order-agnostic pair scripts/switch/common.sh uses). -sha256_file() { - local path="$1" - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$path" | awk '{print $1}' - elif command -v shasum >/dev/null 2>&1; then - shasum -a 256 "$path" | awk '{print $1}' - else - fail "need sha256sum or shasum (install coreutils)" - fi -} - -# download_pinned URL DEST EXPECTED_SHA256 -# -# A cache hit is only trusted if it still hashes to the pin: a download -# truncated by a network drop would otherwise be reused forever, which is the -# same trap scripts/build.sh guards for the win64 zip and the x86_64 AppImage. -download_pinned() { - local url="$1" dest="$2" want="$3" got="" - if [ -f "$dest" ]; then - got="$(sha256_file "$dest")" - if [ "$got" = "$want" ]; then - return 0 - fi - warn "cached $(basename "$dest") has the wrong digest, re-downloading" - rm -f "$dest" - fi - say "downloading $(basename "$dest")" - # --retry-all-errors because plain --retry skips TLS handshake failures, - # which is how xiph.org drops these tarballs; the pin below still gates it. - curl -fL --retry 5 --retry-delay 2 --retry-all-errors --progress-bar \ - "$url" -o "$dest.tmp" || fail "download failed: $url" - got="$(sha256_file "$dest.tmp")" - [ "$got" = "$want" ] || fail "$(printf '%s\n expected %s\n got %s' \ - "checksum mismatch for $(basename "$dest")" "$want" "$got")" - mv "$dest.tmp" "$dest" -} - -# Echo the container runtime to use: docker, else podman. -container_runtime() { - if [ -n "${GEN1_CONTAINER_RUNTIME:-}" ]; then - printf '%s' "$GEN1_CONTAINER_RUNTIME" - return 0 - fi - if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then - printf 'docker' - elif command -v podman >/dev/null 2>&1; then - printf 'podman' - else - return 1 - fi -} - fail_need_container() { fail "$(cat <<'EOF' the aarch64 AppImage is compiled inside a Debian bullseye container and needs diff --git a/scripts/linux-arm64/selftest_build_linux_arm64.sh b/scripts/linux-arm64/selftest_build_linux_arm64.sh index 9f3f91a8..f28027f3 100755 --- a/scripts/linux-arm64/selftest_build_linux_arm64.sh +++ b/scripts/linux-arm64/selftest_build_linux_arm64.sh @@ -60,11 +60,46 @@ printf '%s' "$LOVE_SRC_URL" | grep -q "/$LOVE_VERSION/$LOVE_SRC_TARBALL\$" \ say "checking the builder base image" # Building on anything newer than bullseye silently raises the glibc floor and # strands every user on an older distro, with no symptom until they run it. -grep -q '^FROM debian:bullseye$' "$SCRIPT_DIR/Dockerfile" \ - || fail "Dockerfile no longer builds on debian:bullseye (that raises the glibc floor)" -[ "$BUILDER_BASE_IMAGE" = "debian:bullseye" ] \ +grep -qxF "FROM $LUAJIT_BUILD_IMAGE" "$SCRIPT_DIR/Dockerfile" \ + || fail "Dockerfile FROM is not LUAJIT_BUILD_IMAGE ($LUAJIT_BUILD_IMAGE)" +printf '%s' "$LUAJIT_BUILD_IMAGE" | grep -Eq '^debian:bullseye@sha256:[0-9a-f]{64}$' \ + || fail "LUAJIT_BUILD_IMAGE is not a digest-pinned debian:bullseye (that raises the glibc floor)" +[ "$BUILDER_BASE_IMAGE" = "$LUAJIT_BUILD_IMAGE" ] \ || fail "BUILDER_BASE_IMAGE disagrees with the Dockerfile" +while IFS= read -r apt_line; do + [ -n "$apt_line" ] || continue + grep -qF -- "$apt_line" "$SCRIPT_DIR/Dockerfile" \ + || fail "build_luajit.sh and the builder image must use the same apt sources (Dockerfile lacks: $apt_line)" +done <<<"$LUAJIT_APT_SOURCES" + +say "checking verify_luajit.sh rejection paths" +# Synthetic files only: header = magic, 64-bit (02), little-endian (01), zeros, +# e_machine at offset 18-19. +lj_dir="$(mktemp -d "${TMPDIR:-/tmp}/gen1recomp-verify-luajit.XXXXXX")" +temp_dir="" +trap 'rm -rf "$lj_dir" ${temp_dir:+"$temp_dir"}' EXIT +lj_hdr() { # $1 = e_machine as two hex bytes, e.g. b7 00 + printf '\177ELF\002\001'; head -c 12 /dev/zero; printf "\\x$1\\x$2" +} +lj_make() { # $1 file, $2 e_machine lo, $3 hi, $4 version, $5 path, $6 glibc + { lj_hdr "$2" "$3"; printf '\0%s\0%s\0%s\0' "$4" "$5" "$6"; } > "$1" +} +lj_path='/usr/share/luajit-2.1/?.lua' +lj_verify() { bash "$ROOT/scripts/luajit/verify_luajit.sh" "$1" >/dev/null 2>&1; } +lj_make "$lj_dir/ok" b7 00 "$LUAJIT_VERSION_STRING" "$lj_path" GLIBC_2.17 +lj_verify "$lj_dir/ok" || fail "verify_luajit.sh rejects a valid synthetic aarch64 library" +printf '%s %s GLIBC_2.17' "$LUAJIT_VERSION_STRING" "$lj_path" > "$lj_dir/text" +lj_make "$lj_dir/x86" 3e 00 "$LUAJIT_VERSION_STRING" "$lj_path" GLIBC_2.17 +lj_make "$lj_dir/nopath" b7 00 "$LUAJIT_VERSION_STRING" "/opt/build/share/luajit-2.1/?.lua" GLIBC_2.17 +lj_make "$lj_dir/glibc" b7 00 "$LUAJIT_VERSION_STRING" "$lj_path" GLIBC_2.34 +lj_make "$lj_dir/beta" b7 00 "$LUAJIT_VERSION_STRING LuaJIT 2.1.0-beta3" "$lj_path" GLIBC_2.17 +for bad in text x86 nopath glibc beta; do + if lj_verify "$lj_dir/$bad"; then + fail "verify_luajit.sh accepted the bad '$bad' library" + fi +done + say "checking the dependency exclude list" # Extract the live regex from the build script and classify known sonames # through it, so a future edit cannot quietly start bundling glibc or stop @@ -121,11 +156,17 @@ say "checking the dlopen guarantees" # turns an optional runtime capability into a mandatory startup dependency. # If a future edit drops the source build and reaches for the -dev package # again, the AppImage silently stops starting on lean systems. -for forbidden_pkg in libsdl2-dev libtheora-dev libopenal-dev; do +for forbidden_pkg in libsdl2-dev libtheora-dev libopenal-dev libluajit-5.1-dev; do if grep -qE "^ +.*\b$forbidden_pkg\b" "$SCRIPT_DIR/Dockerfile"; then fail "Dockerfile installs $forbidden_pkg; that library is built from source on purpose" fi done +grep -qF 'compile_luajit.sh' "$SCRIPT_DIR/build_appimage.sh" \ + || fail "build_appimage.sh no longer builds the pinned LuaJIT" +grep -qF 'luajit${LUAJIT_COMMIT:0:12}-${LUAJIT_RECIPE}-img${BUILDER_HASH}' "$SCRIPT_DIR/build_appimage.sh" \ + || fail "the PREFIX cache key does not include the LuaJIT commit, recipe hash and builder image hash" +printf '%s' "$LUAJIT_SHA256" | grep -Eq '^[0-9a-f]{64}$' \ + || fail "LUAJIT_SHA256 is not a sha256 digest: $LUAJIT_SHA256" grep -qF -- '--enable-alsa-shared' "$SCRIPT_DIR/build_appimage.sh" \ || fail "SDL2 is no longer configured to dlopen its audio backends" grep -qF -- '--enable-x11-shared' "$SCRIPT_DIR/build_appimage.sh" \ @@ -158,7 +199,6 @@ done say "checking the shared game.love payload" temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/gen1recomp-linux-arm64-selftest.XXXXXX")" -trap 'rm -rf "$temp_dir"' EXIT "$ROOT/scripts/pack_love.sh" \ --output "$temp_dir/game.love" \ --listing "$temp_dir/love-listing.txt" \ diff --git a/scripts/linux-arm64/verify_appimage.sh b/scripts/linux-arm64/verify_appimage.sh index 4ff60225..9c1b5f1e 100755 --- a/scripts/linux-arm64/verify_appimage.sh +++ b/scripts/linux-arm64/verify_appimage.sh @@ -8,6 +8,8 @@ image="${1:?usage: verify_appimage.sh }" [ -f "$image" ] || { echo "::error::no such AppImage: $image"; exit 1; } image="$(cd "$(dirname "$image")" && pwd)/$(basename "$image")" +verify_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../luajit" && pwd)" + workdir="$(mktemp -d)" trap 'rm -rf "$workdir"' EXIT cd "$workdir" @@ -20,6 +22,14 @@ for required in AppRun bin/love game.love lib/liblove-11.5.so; do || { echo "::error::AppImage is missing $required"; exit 1; } done +# The pinned LuaJIT, not bullseye's 2.1.0-beta3 (segfaults with the JIT on). +[ -f "squashfs-root/lib/libluajit-5.1.so.2" ] \ + || { echo "::error::AppImage is missing lib/libluajit-5.1.so.2"; exit 1; } +bash "$verify_dir/verify_luajit.sh" squashfs-root/lib/libluajit-5.1.so.2 \ + || { echo "::error::bundled LuaJIT is not the pinned build"; exit 1; } +[ -s "squashfs-root/LuaJIT-COPYRIGHT" ] \ + || { echo "::error::AppImage is missing LuaJIT-COPYRIGHT"; exit 1; } + bridge="squashfs-root/liblibrashader_bridge.so" if [ "${SHADERFX_BRIDGE_REQUIRED:-}" = "1" ] && [ ! -e "$bridge" ]; then echo "::error::AppImage is missing liblibrashader_bridge.so" diff --git a/scripts/luajit/build_luajit.sh b/scripts/luajit/build_luajit.sh new file mode 100755 index 00000000..b01bdefe --- /dev/null +++ b/scripts/luajit/build_luajit.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Build the pinned LuaJIT for aarch64 Linux inside a debian:bullseye container. +# +# Usage: scripts/luajit/build_luajit.sh +# Prints the path of the library on stdout; progress goes to stderr. +# Output: .bazinga/cache/luajit/--/{libluajit-5.1.so.2,COPYRIGHT} +# +# Must run on an aarch64 host (docker or podman). Emulated (qemu) aarch64 +# builds have miscompiled LuaJIT before, so other hosts are refused. + +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" +# shellcheck source=pins.sh +. "$HERE/pins.sh" + +say() { printf '\033[1;32m==>\033[0m %s\n' "$*" >&2; } +warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; } +fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } +# shellcheck source=../lib/fetch.sh +. "$ROOT/scripts/lib/fetch.sh" + +case "$(uname -m)" in + aarch64|arm64) ;; + *) fail "LuaJIT must be built on an aarch64 host (got $(uname -m)); emulated builds are unreliable. Build on arm64 or pass GEN1RECOMP_LUAJIT_LIB to the zip scripts." ;; +esac + +CACHE="$ROOT/.bazinga/cache/luajit" +# Keyed by the build recipe and the pinned image digest, so changing either rebuilds. +RECIPE_HASH="$(sha256_file "$HERE/compile_luajit.sh")" +IMAGE_DIGEST="${LUAJIT_BUILD_IMAGE##*sha256:}" +OUT_DIR="$CACHE/$LUAJIT_COMMIT-${RECIPE_HASH:0:12}-${IMAGE_DIGEST:0:12}" +OUT="$OUT_DIR/libluajit-5.1.so.2" +mkdir -p "$CACHE" "$OUT_DIR" + +if [ -f "$OUT" ] && [ -s "$OUT_DIR/COPYRIGHT" ] \ + && "$HERE/verify_luajit.sh" "$OUT" >/dev/null 2>&1; then + say "LuaJIT $LUAJIT_COMMIT already built" + echo "$OUT" + exit 0 +fi + +# Pinned tarball (a cache hit must still match the pin). curl's progress goes +# to stderr, but keep stdout clean regardless: it carries only the lib path. +TARBALL="$CACHE/$LUAJIT_TARBALL" +download_pinned "$LUAJIT_URL" "$TARBALL" "$LUAJIT_SHA256" >&2 + +RT="$(container_runtime)" \ + || fail "docker or podman is required to build LuaJIT (or set GEN1RECOMP_LUAJIT_LIB for the zip scripts)" + +STAGE="$(mktemp -d "$CACHE/stage.XXXXXX")" +trap 'rm -rf "$STAGE"' EXIT +mkdir -p "$STAGE/out" + +say "building LuaJIT $LUAJIT_COMMIT in $RT ($LUAJIT_BUILD_IMAGE, arm64)" +"$RT" run --rm --platform linux/arm64 \ + -e LUAJIT_VERSION_STRING="$LUAJIT_VERSION_STRING" \ + -e LUAJIT_MAX_GLIBC="$LUAJIT_MAX_GLIBC" \ + -e LUAJIT_APT_SOURCES="$LUAJIT_APT_SOURCES" \ + -v "$HERE:/luajit-scripts:ro" \ + -v "$TARBALL:/in/$LUAJIT_TARBALL:ro" \ + -v "$STAGE/out:/out" \ + "$LUAJIT_BUILD_IMAGE" bash -euo pipefail -c ' + # Bullseye is EOL; same sources/apt.conf as scripts/linux-arm64/Dockerfile. + printf "%s\n" "$LUAJIT_APT_SOURCES" > /etc/apt/sources.list + rm -f /etc/apt/sources.list.d/* + printf "Acquire::Check-Valid-Until \"false\";\nAcquire::Retries \"5\";\nAcquire::http::Pipeline-Depth \"0\";\nAcquire::http::No-Cache \"true\";\n" > /etc/apt/apt.conf.d/99builder + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq build-essential binutils ca-certificates >/dev/null + bash /luajit-scripts/compile_luajit.sh "/in/'"$LUAJIT_TARBALL"'" /tmp/prefix + cp -L /tmp/prefix/lib/libluajit-5.1.so.2 /out/libluajit-5.1.so.2 + cp /tmp/prefix/share/doc/luajit/COPYRIGHT /out/COPYRIGHT + ' >&2 || fail "LuaJIT container build failed" + +"$HERE/verify_luajit.sh" "$STAGE/out/libluajit-5.1.so.2" || fail "built library failed verification" +[ -s "$STAGE/out/COPYRIGHT" ] || fail "built COPYRIGHT is missing or empty" +# Install: COPYRIGHT first, library last, so the cache-hit test is atomic. +mv -f "$STAGE/out/COPYRIGHT" "$OUT_DIR/COPYRIGHT" +mv -f "$STAGE/out/libluajit-5.1.so.2" "$OUT" +say "built $OUT" +echo "$OUT" diff --git a/scripts/luajit/compile_luajit.sh b/scripts/luajit/compile_luajit.sh new file mode 100755 index 00000000..172f6813 --- /dev/null +++ b/scripts/luajit/compile_luajit.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Compile the pinned LuaJIT. Runs INSIDE an aarch64 Linux container +# (debian:bullseye + build-essential; binutils for readelf). +# +# Usage: compile_luajit.sh +# Env: LUAJIT_VERSION_STRING string the built library must contain (required) +# LUAJIT_MAX_GLIBC highest GLIBC_x.y symbol allowed (required) +# +# No -mcpu flag: the validated recipe is plain `make amalg` with GC64. + +set -euo pipefail + +fail() { printf 'error: %s\n' "$*" >&2; exit 1; } + +[ $# -eq 2 ] || fail "usage: compile_luajit.sh " +TARBALL="$1" +PREFIX="$2" +[ -f "$TARBALL" ] || fail "tarball not found: $TARBALL" +: "${LUAJIT_VERSION_STRING:?LUAJIT_VERSION_STRING must be set}" +: "${LUAJIT_MAX_GLIBC:?LUAJIT_MAX_GLIBC must be set}" + +SRC_TMP="$(mktemp -d)" +trap 'rm -rf "$SRC_TMP"' EXIT +tar -xzf "$TARBALL" -C "$SRC_TMP" +SRC="$(find "$SRC_TMP" -mindepth 1 -maxdepth 1 -type d -print -quit)" +[ -n "$SRC" ] || fail "tarball had no top-level directory" + +cd "$SRC" +# Compile with PREFIX=/usr, install with the real prefix. LuaJIT bakes +# LUA_ROOT=$(PREFIX) into its default package.path/cpath at compile time, so +# compiling with the install prefix would leak a build-time directory +# (/tmp/prefix, /cache/prefix-...) into the shipped library. The top-level +# install only depends on src/luajit, so it does not recompile. +make -j"$(nproc)" amalg PREFIX=/usr XCFLAGS=-DLUAJIT_ENABLE_GC64 >&2 +# Install into a staging root first (luajit.pc still says prefix=$PREFIX), so a +# failed check below never leaves a half-trusted tree in $PREFIX. +STAGE="$(mktemp -d)" +trap 'rm -rf "$SRC_TMP" "$STAGE"' EXIT +make install PREFIX="$PREFIX" DESTDIR="$STAGE" >&2 +mkdir -p "$STAGE$PREFIX/share/doc/luajit" +cp COPYRIGHT "$STAGE$PREFIX/share/doc/luajit/COPYRIGHT" + +LIB="$(readlink -f "$STAGE$PREFIX/lib/libluajit-5.1.so.2")" +[ -f "$LIB" ] || fail "missing $STAGE$PREFIX/lib/libluajit-5.1.so.2" + +soname="$(readelf -d "$LIB" | sed -n 's/.*SONAME.*\[\(.*\)\]/\1/p')" +[ "$soname" = "libluajit-5.1.so.2" ] || fail "unexpected SONAME: $soname" +if grep -aqF "$PREFIX" "$LIB"; then + fail "library contains the build prefix '$PREFIX' (baked-in module path)" +fi + +# Version, module path, ELF class/arch and glibc ceiling: the same checks the +# host-side gate runs (verify_luajit.sh needs only od, grep and awk). +bash "$(dirname "$0")/verify_luajit.sh" "$LIB" >&2 + +# COPYRIGHT goes in last: callers treat it as the "install finished" marker, so +# an interrupted copy never looks like a complete cached prefix. +mv "$STAGE$PREFIX/share/doc/luajit/COPYRIGHT" "$STAGE/COPYRIGHT" +mkdir -p "$PREFIX/share/doc/luajit" +cp -a "$STAGE$PREFIX/." "$PREFIX/" +cp "$STAGE/COPYRIGHT" "$PREFIX/share/doc/luajit/COPYRIGHT" +echo "luajit ok: staged and installed to $PREFIX" >&2 diff --git a/scripts/luajit/pins.sh b/scripts/luajit/pins.sh new file mode 100755 index 00000000..ec3cfc8c --- /dev/null +++ b/scripts/luajit/pins.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Pinned LuaJIT for the aarch64 Linux builds (PortMaster zips, AppImage). +# Source this file; it only sets variables and has no side effects. +# +# Why: PortMaster's LOVE 11.5 runtime ships LuaJIT 2.1.0-beta3 (2017), which +# segfaulted 3/3 runs with the JIT on (TrimUI Brick). This pinned v2.1-branch +# commit ran a 26-minute JIT-on soak without a crash. See docs/linux-arm-sbc.md. +# +# The commit's .relver is already expanded, so the built library contains the +# string "LuaJIT 2.1.1788856981" (beta3 contains "LuaJIT 2.1.0-beta3"). + +# shellcheck disable=SC2034 +LUAJIT_COMMIT="c6ffc141a8762b41703f9287d63d93622a13dd8f" +LUAJIT_URL="https://github.com/LuaJIT/LuaJIT/archive/$LUAJIT_COMMIT.tar.gz" +LUAJIT_SHA256="6e5fec07750add912e7c3eae0c194d24cd6d023714e1f04a0298a5b4819e4457" +LUAJIT_TARBALL="LuaJIT-$LUAJIT_COMMIT.tar.gz" +LUAJIT_VERSION_STRING="LuaJIT 2.1.1788856981" +# Built on debian:bullseye (glibc 2.31); the library must not need newer. +# The standalone build (build_luajit.sh) pins the image by digest so a re-tag +# cannot silently change the toolchain; the digest is part of its cache key. +# To re-pin: `docker buildx imagetools inspect debian:bullseye` -> the top-level +# "Digest:" line; put it here AND in the FROM line of +# scripts/linux-arm64/Dockerfile (the selftest checks the two are equal). +LUAJIT_BUILD_IMAGE="debian:bullseye@sha256:6f519a81440354a85eb592c5f32109ab80605f6b892455983a6f618bf87fabe9" +LUAJIT_MAX_GLIBC="2.31" +# apt sources for the build container (bullseye is EOL; security comes from a +# dated snapshot). Newline-separated; must match scripts/linux-arm64/Dockerfile +# so both LuaJIT builds see the same package state. +LUAJIT_APT_SOURCES="deb http://archive.debian.org/debian bullseye main +deb http://archive.debian.org/debian bullseye-updates main +deb http://snapshot.debian.org/archive/debian-security/20260831T000000Z bullseye-security main" diff --git a/scripts/luajit/portmaster_runtime.sh b/scripts/luajit/portmaster_runtime.sh new file mode 100755 index 00000000..ab03316a --- /dev/null +++ b/scripts/luajit/portmaster_runtime.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Shared PortMaster zip helpers for build-linux-arm-sbc.sh and build-rg34xxsp.sh. +# Source this file; it has no side effects at source time. +# The caller must define ROOT, say, warn and fail. + +# shellcheck source=scripts/luajit/pins.sh +. "$ROOT/scripts/luajit/pins.sh" +# shellcheck source=scripts/lib/fetch.sh +. "$ROOT/scripts/lib/fetch.sh" + +# shellcheck disable=SC2034 +LOVE_VERSION="${LOVE_VERSION:-11.5}" + +# Official PortMaster LÖVE 11.5 aarch64 runtime (small love stub + liblove). +# Pinned to the last PortMaster-GUI commit that touched runtimes/love_11.5, and +# every file is sha256-checked. libluajit is deliberately NOT taken from +# here: that one is LuaJIT 2.1.0-beta3 (2017), which crashes with the JIT on. +PM_RUNTIME_COMMIT="715f50fd277febf942a66d59ac31e5a9c36c2f69" +PM_RUNTIME_BASE="https://raw.githubusercontent.com/PortsMaster/PortMaster-GUI/$PM_RUNTIME_COMMIT/PortMaster/runtimes/love_${LOVE_VERSION}" +PM_SHA_LOVE="c6c64453ed6163c8aeb14d303bad1e4d9ecc4d628097b3c6420ada2fa6a8690f" +PM_SHA_LIBLOVE="a50c83729cb0e1c4d8389eaaa827f0a6e7f3362814d0504ca84074dd0e47c379" +PM_SHA_MODPLUG="026b2287c9d68f920cf541b68b94f1a691168e4bbf301a8793f4114808aa8f95" +PM_SHA_OGG="66015f344b53895affd3ff153f402097ebaa2add33246bea2d7fca27b682e07a" + +# pm_fetch_runtime CACHE_DIR: download + sha256-check the 4 runtime files. +pm_fetch_runtime() { + local dir="$1" + download_pinned "$PM_RUNTIME_BASE/love.aarch64" "$dir/love.aarch64" "$PM_SHA_LOVE" + download_pinned "$PM_RUNTIME_BASE/libs.aarch64/liblove-11.5.so" "$dir/liblove-11.5.so" "$PM_SHA_LIBLOVE" + download_pinned "$PM_RUNTIME_BASE/libs.aarch64/libmodplug.so.1" "$dir/libmodplug.so.1" "$PM_SHA_MODPLUG" + download_pinned "$PM_RUNTIME_BASE/libs.aarch64/libogg.so.0" "$dir/libogg.so.0" "$PM_SHA_OGG" +} + +# pm_select_luajit: a pinned modern build, never PortMaster's beta3. Either +# supplied via GEN1RECOMP_LUAJIT_LIB (CI release jobs) or built in a container. +# Sets LUAJIT_LIB and LUAJIT_COPYRIGHT. +pm_select_luajit() { + if [ -n "${GEN1RECOMP_LUAJIT_LIB:-}" ]; then + LUAJIT_LIB="$GEN1RECOMP_LUAJIT_LIB" + [ -f "$LUAJIT_LIB" ] || fail "GEN1RECOMP_LUAJIT_LIB does not exist: $LUAJIT_LIB" + LUAJIT_COPYRIGHT="$(dirname "$LUAJIT_LIB")/COPYRIGHT" + [ -s "$LUAJIT_COPYRIGHT" ] || LUAJIT_COPYRIGHT="${GEN1RECOMP_LUAJIT_COPYRIGHT:-}" + [ -s "$LUAJIT_COPYRIGHT" ] \ + || fail "LuaJIT COPYRIGHT not found next to $LUAJIT_LIB; set GEN1RECOMP_LUAJIT_COPYRIGHT" + else + say "building pinned LuaJIT $LUAJIT_COMMIT" + LUAJIT_LIB="$("$ROOT/scripts/luajit/build_luajit.sh")" \ + || fail "LuaJIT build failed (needs an aarch64 host with docker/podman, or set GEN1RECOMP_LUAJIT_LIB)" + LUAJIT_COPYRIGHT="$(dirname "$LUAJIT_LIB")/COPYRIGHT" + fi + [ -f "$LUAJIT_LIB" ] && [ -s "$LUAJIT_COPYRIGHT" ] || fail "LuaJIT library or COPYRIGHT missing" + "$ROOT/scripts/luajit/verify_luajit.sh" "$LUAJIT_LIB" || fail "LuaJIT library failed verification: $LUAJIT_LIB" +} + +# pm_stage_luajit LIBS_DIR LICENSES_DIR +pm_stage_luajit() { + : "${LUAJIT_LIB:?call pm_select_luajit before pm_stage_luajit}" + : "${LUAJIT_COPYRIGHT:?call pm_select_luajit before pm_stage_luajit}" + cp "$LUAJIT_LIB" "$1/libluajit-5.1.so.2" + cp "$LUAJIT_COPYRIGHT" "$2/LuaJIT-COPYRIGHT" +} + +# pm_verify_zip ZIP PORT_DIR_NAME WORK_DIR: verify the LuaJIT that actually +# landed in the zip, not just the input file. +pm_verify_zip() { + local zip="$1" port="$2" work="$3" vdir + vdir="$(mktemp -d "$work/verify.XXXXXX")" + unzip -q "$zip" -d "$vdir" + "$ROOT/scripts/luajit/verify_luajit.sh" "$vdir/$port/libs.aarch64/libluajit-5.1.so.2" \ + || { rm -rf "$vdir"; fail "zip LuaJIT verification failed"; } + [ -s "$vdir/$port/licenses/LuaJIT-COPYRIGHT" ] \ + || { rm -rf "$vdir"; fail "zip is missing licenses/LuaJIT-COPYRIGHT"; } + rm -rf "$vdir" +} diff --git a/scripts/luajit/selftest_portmaster_runtime.sh b/scripts/luajit/selftest_portmaster_runtime.sh new file mode 100755 index 00000000..5772dd49 --- /dev/null +++ b/scripts/luajit/selftest_portmaster_runtime.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Offline test of scripts/luajit/portmaster_runtime.sh (no docker, no network). +# Usage: bash scripts/luajit/selftest_portmaster_runtime.sh +# Each case runs in a subshell with a stub fail() that exits that subshell, so +# failures can be asserted. + +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# shellcheck source=pins.sh +. "$HERE/pins.sh" + +say() { :; } +warn() { :; } +fail() { printf 'stub fail: %s\n' "$*" >&2; exit 1; } +# shellcheck source=portmaster_runtime.sh +. "$HERE/portmaster_runtime.sh" + +bad() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +# expect_ok NAME CMD... / expect_fail NAME CMD...: run CMD in a subshell. +expect_ok() { local n="$1"; shift; ( "$@" ) >/dev/null 2>&1 || bad "$n: expected success"; } +expect_fail() { local n="$1"; shift; if ( "$@" ) >/dev/null 2>&1; then bad "$n: expected failure"; fi; } + +# Synthetic aarch64 "library" that passes verify_luajit.sh. +lj_make() { + { printf '\177ELF\002\001'; head -c 12 /dev/zero; printf '\xb7\x00' + printf '\0%s\0%s\0%s\0' "$LUAJIT_VERSION_STRING" '/usr/share/luajit-2.1/?.lua' GLIBC_2.17 + } > "$1" +} + +mkdir -p "$TMP/good" "$TMP/nocopy" "$TMP/emptycopy" +lj_make "$TMP/good/libluajit-5.1.so.2" +echo "LuaJIT license" > "$TMP/good/COPYRIGHT" +lj_make "$TMP/nocopy/libluajit-5.1.so.2" +lj_make "$TMP/emptycopy/libluajit-5.1.so.2" +: > "$TMP/emptycopy/COPYRIGHT" +echo "other license" > "$TMP/alt-COPYRIGHT" + +case_stage_without_select() { unset LUAJIT_LIB LUAJIT_COPYRIGHT; pm_stage_luajit "$TMP" "$TMP"; } +expect_fail "stage without select" case_stage_without_select + +case_missing_lib() { GEN1RECOMP_LUAJIT_LIB="$TMP/nope.so" pm_select_luajit; } +expect_fail "missing GEN1RECOMP_LUAJIT_LIB" case_missing_lib + +case_empty_copyright() { + GEN1RECOMP_LUAJIT_LIB="$TMP/emptycopy/libluajit-5.1.so.2" GEN1RECOMP_LUAJIT_COPYRIGHT= pm_select_luajit +} +expect_fail "empty COPYRIGHT" case_empty_copyright + +case_no_copyright_anywhere() { + GEN1RECOMP_LUAJIT_LIB="$TMP/nocopy/libluajit-5.1.so.2" GEN1RECOMP_LUAJIT_COPYRIGHT= pm_select_luajit +} +expect_fail "no COPYRIGHT anywhere" case_no_copyright_anywhere + +# Verification is really reached: a bad library is rejected. +printf 'not a library' > "$TMP/emptycopy/bad.so" +echo x > "$TMP/emptycopy/COPYRIGHT" +case_bad_lib() { GEN1RECOMP_LUAJIT_LIB="$TMP/emptycopy/bad.so" pm_select_luajit; } +expect_fail "bad library" case_bad_lib + +case_env_copyright() { + export GEN1RECOMP_LUAJIT_LIB="$TMP/nocopy/libluajit-5.1.so.2" + export GEN1RECOMP_LUAJIT_COPYRIGHT="$TMP/alt-COPYRIGHT" + pm_select_luajit + [ "$LUAJIT_LIB" = "$GEN1RECOMP_LUAJIT_LIB" ] || exit 1 + [ "$LUAJIT_COPYRIGHT" = "$TMP/alt-COPYRIGHT" ] || exit 1 +} +expect_ok "COPYRIGHT via env" case_env_copyright + +# Stage + zip round trip. +build_tree() { # $1 = tree root + mkdir -p "$1/port/libs.aarch64" "$1/port/licenses" + GEN1RECOMP_LUAJIT_LIB="$TMP/good/libluajit-5.1.so.2" pm_select_luajit + pm_stage_luajit "$1/port/libs.aarch64" "$1/port/licenses" +} +case_stage() { + build_tree "$TMP/tree" + cmp "$TMP/good/libluajit-5.1.so.2" "$TMP/tree/port/libs.aarch64/libluajit-5.1.so.2" + cmp "$TMP/good/COPYRIGHT" "$TMP/tree/port/licenses/LuaJIT-COPYRIGHT" +} +expect_ok "stage copies both files" case_stage + +command -v zip >/dev/null && command -v unzip >/dev/null || { echo "zip/unzip missing; skipping zip cases" >&2; echo "selftest_portmaster_runtime: ok (zip cases skipped)"; exit 0; } + +( cd "$TMP/tree" && zip -q -r "$TMP/good.zip" port ) +rm "$TMP/tree/port/licenses/LuaJIT-COPYRIGHT" +( cd "$TMP/tree" && zip -q -r "$TMP/nocopy.zip" port ) + +mkdir -p "$TMP/work" +expect_ok "verify good zip" pm_verify_zip "$TMP/good.zip" port "$TMP/work" +expect_fail "verify zip without COPYRIGHT" pm_verify_zip "$TMP/nocopy.zip" port "$TMP/work" + +echo "selftest_portmaster_runtime: ok" diff --git a/scripts/luajit/verify_luajit.sh b/scripts/luajit/verify_luajit.sh new file mode 100755 index 00000000..2b44fcfb --- /dev/null +++ b/scripts/luajit/verify_luajit.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Host-side check of a built libluajit-5.1.so.2 (works on macOS and Linux, no +# readelf needed). Fails if the library is not the pinned LuaJIT, is the old +# 2.1.0-beta3, or needs a glibc newer than the pin allows. +# +# Usage: scripts/luajit/verify_luajit.sh + +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=pins.sh +. "$HERE/pins.sh" + +fail() { printf 'error: %s\n' "$*" >&2; exit 1; } + +[ $# -eq 1 ] || fail "usage: verify_luajit.sh " +LIB="$1" +[ -f "$LIB" ] || fail "not a file: $LIB" + +# 64-bit (byte 4 = 02), little-endian (byte 5 = 01), e_machine 0xb7 = aarch64. +hdr="$(od -An -tx1 -N20 "$LIB" | tr -d ' \n')" +[ "${hdr:0:8}" = "7f454c46" ] && [ "${hdr:8:2}" = "02" ] && [ "${hdr:10:2}" = "01" ] \ + && [ "${hdr:36:4}" = "b700" ] \ + || fail "$LIB is not a 64-bit little-endian aarch64 ELF" + +grep -aqF "$LUAJIT_VERSION_STRING" "$LIB" \ + || fail "$LIB does not contain '$LUAJIT_VERSION_STRING'" +if grep -aq '2\.1\.0-beta' "$LIB"; then + fail "$LIB contains '2.1.0-beta' (old LuaJIT)" +fi + +# Built with PREFIX=/usr; a build-prefix path here means a stale or wrong build. +grep -aqF '/usr/share/luajit-2.1/?.lua' "$LIB" \ + || fail "$LIB does not contain the default module path /usr/share/luajit-2.1/?.lua" + +# Compare dotted versions numerically without relying on sort -V. +ver_gt() { + awk -v a="$1" -v b="$2" 'BEGIN { + na = split(a, x, "."); nb = split(b, y, ".") + n = na > nb ? na : nb + for (i = 1; i <= n; i++) { + if ((x[i] + 0) > (y[i] + 0)) exit 0 + if ((x[i] + 0) < (y[i] + 0)) exit 1 + } + exit 1 + }' +} + +maxg="" +while IFS= read -r v; do + v="${v#GLIBC_}" + [ -n "$v" ] || continue + if [ -z "$maxg" ] || ver_gt "$v" "$maxg"; then maxg="$v"; fi +done < <(grep -ao 'GLIBC_[0-9][0-9.]*' "$LIB" | sort -u) + +[ -n "$maxg" ] || fail "$LIB has no GLIBC_ version strings" +if ver_gt "$maxg" "$LUAJIT_MAX_GLIBC"; then + fail "$LIB requires GLIBC_$maxg > $LUAJIT_MAX_GLIBC" +fi +echo "verify_luajit ok: '$LUAJIT_VERSION_STRING', max GLIBC=$maxg (limit $LUAJIT_MAX_GLIBC)" >&2