mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #871 from ratherDashing/linux-arm64-appimage
This commit is contained in:
@@ -260,6 +260,117 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
linux-arm64-changes:
|
||||
name: detect Linux arm64 changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.paths.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: paths
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
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
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
linux-arm64-selftest:
|
||||
name: Linux arm64 offline selftest
|
||||
needs: linux-arm64-changes
|
||||
if: needs.linux-arm64-changes.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Deliberately on x86_64: everything this gate checks (pins, the
|
||||
# host-arch guard, the dependency exclude list, the AppRun fusion
|
||||
# contract) is answerable without a container or an aarch64 machine,
|
||||
# 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
|
||||
|
||||
linux-arm64-build:
|
||||
name: Linux arm64 AppImage build
|
||||
needs: [linux-arm64-changes, linux-arm64-selftest]
|
||||
if: |
|
||||
always()
|
||||
&& needs.linux-arm64-changes.outputs.changed == 'true'
|
||||
&& needs.linux-arm64-selftest.result == 'success'
|
||||
# No fork restriction, unlike switch-build: this needs no secrets and no
|
||||
# self-hosted hardware, just GitHub's free arm64 runner for public repos,
|
||||
# so contributors get the same coverage on their own PRs.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Build the aarch64 AppImage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build_linux_arm64.sh --version 0.0.0
|
||||
- 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; }
|
||||
- name: Upload the AppImage
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-linux-arm64
|
||||
path: |
|
||||
dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage
|
||||
dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
headless:
|
||||
name: headless suites (no ROM)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -140,6 +140,36 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
linux-arm64:
|
||||
name: build Linux arm64 AppImage
|
||||
needs: [version, love-payload]
|
||||
# GitHub's free arm64 runner for public repos. It has to be arm64: the
|
||||
# AppImage compiles LÖVE natively inside a Debian bullseye arm64
|
||||
# container, and the qemu-emulated alternative takes hours.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Download shared payload
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-release-love
|
||||
path: .bazinga/work
|
||||
- name: Build Linux arm64 AppImage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build_linux_arm64.sh \
|
||||
--version "${{ needs.version.outputs.version }}" \
|
||||
--game-love .bazinga/work/game.love
|
||||
- name: Upload Linux arm64 release
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-linux-arm64-release
|
||||
path: |
|
||||
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage
|
||||
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
xbox-uwp:
|
||||
name: build Xbox UWP release
|
||||
needs: [version, love-payload]
|
||||
@@ -222,7 +252,7 @@ jobs:
|
||||
}
|
||||
|
||||
release:
|
||||
needs: [version, xbox-uwp]
|
||||
needs: [version, xbox-uwp, linux-arm64]
|
||||
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
||||
|
||||
steps:
|
||||
@@ -367,6 +397,13 @@ jobs:
|
||||
name: gen1recomp-xbox-uwp-release
|
||||
path: dist/xbox-uwp
|
||||
|
||||
- name: Download Linux arm64 release
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-linux-arm64-release
|
||||
path: dist/linux-arm64
|
||||
|
||||
- name: Stage release assets
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
id: assets
|
||||
@@ -379,6 +416,15 @@ jobs:
|
||||
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
|
||||
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
|
||||
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
|
||||
|
||||
# arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
|
||||
# its own runner because LÖVE publishes no aarch64 binary and the
|
||||
# AppImage has to be compiled natively; ships as a runnable
|
||||
# AppImage rather than a zip so `chmod +x && ./it` just works.
|
||||
arm64_appimage="dist/linux-arm64/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
[ -f "$arm64_appimage" ] || { echo "::error::$arm64_appimage not found (expected from the linux-arm64 job)"; exit 1; }
|
||||
cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
apk="$(find dist/android/debug -name '*.apk' | head -1)"
|
||||
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
|
||||
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
||||
@@ -512,6 +558,7 @@ jobs:
|
||||
"dist/release/gen1recomp-${v}-macos.zip"
|
||||
"dist/release/gen1recomp-${v}-windows.zip"
|
||||
"dist/release/gen1recomp-${v}-linux.zip"
|
||||
"dist/release/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
"dist/release/gen1recomp-${v}-android.apk"
|
||||
"dist/release/gen1recomp-${v}-ios.ipa"
|
||||
"dist/release/gen1recomp-${v}-switch.zip"
|
||||
|
||||
@@ -205,6 +205,25 @@ even on a different computer, as long as the same folder comes along.
|
||||
already written to either location is touched automatically, so copy files
|
||||
over yourself if you want to carry existing progress across the switch.
|
||||
|
||||
## Linux on arm64 (Raspberry Pi)
|
||||
|
||||
Alongside the x86_64 `gen1recomp-*-linux.zip`, every release ships
|
||||
`gen1recomp-*-linux-arm64.AppImage` for 64-bit ARM desktop Linux — Raspberry
|
||||
Pi 4/5, Armbian and other SBC distros, and arm64 VMs on Apple Silicon:
|
||||
|
||||
```sh
|
||||
chmod +x gen1recomp-*-linux-arm64.AppImage
|
||||
./gen1recomp-*-linux-arm64.AppImage
|
||||
```
|
||||
|
||||
LÖVE publishes no aarch64 binary of any kind, so this artifact compiles the
|
||||
engine — and SDL2, OpenAL and the codecs — from source inside a Debian
|
||||
bullseye arm64 container. It needs only glibc 2.29+, libstdc++, freetype and
|
||||
zlib on the host; OpenGL, X11, Wayland, KMSDRM, ALSA and PulseAudio are all
|
||||
dlopened, so the same image runs on a full desktop, a Wayland-only session or
|
||||
a KMSDRM handheld with no X server. Build instructions and the reasoning are
|
||||
in [docs/linux-arm64-build.md](docs/linux-arm64-build.md).
|
||||
|
||||
## iOS
|
||||
|
||||
Every release ships `gen1recomp-*-ios.ipa`. Sideload it with AltStore
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
# Linux arm64 (aarch64) AppImage
|
||||
|
||||
Releases ship `gen1recomp-<version>-linux-arm64.AppImage` alongside the
|
||||
existing x86_64 `gen1recomp-<version>-linux.zip`. It targets 64-bit ARM
|
||||
desktop Linux: Raspberry Pi 4/5 running Raspberry Pi OS, Armbian and other
|
||||
SBC distros, arm64 VMs on Apple Silicon, Ampere/Graviton desktops, and the
|
||||
aarch64 handhelds that run a full distro.
|
||||
|
||||
> The Anbernic RG34XXSP has its own PortMaster-style pack
|
||||
> (`gen1recomp-*-rg34xxsp-stockos64-mod.zip`, see
|
||||
> [anbernic-rg34xxsp.md](anbernic-rg34xxsp.md)). That one bundles PortMaster's
|
||||
> LÖVE runtime and expects the device's own SDL; this AppImage is the generic
|
||||
> desktop-Linux artifact and shares nothing with it but the `game.love`.
|
||||
|
||||
## For players
|
||||
|
||||
```sh
|
||||
chmod +x gen1recomp-*-linux-arm64.AppImage
|
||||
./gen1recomp-*-linux-arm64.AppImage
|
||||
```
|
||||
|
||||
Then use **Import ROM** in the launcher to point it at your own legal Red /
|
||||
Blue / Yellow cartridge dump, exactly as on every other platform.
|
||||
|
||||
If your system has no FUSE (`dlopen(): error loading libfuse.so.2`), either
|
||||
install it (`sudo apt install libfuse2`) or run without it:
|
||||
|
||||
```sh
|
||||
./gen1recomp-*-linux-arm64.AppImage --appimage-extract-and-run
|
||||
```
|
||||
|
||||
### What the host has to provide
|
||||
|
||||
Very little, and this is enforced by an assertion in the build rather than by
|
||||
good intentions. The only libraries the AppImage requires at startup are:
|
||||
|
||||
```
|
||||
glibc 2.29+ libstdc++ libfreetype6 zlib
|
||||
```
|
||||
|
||||
Everything else — OpenGL/Mesa, X11, Wayland, KMSDRM, ALSA, PulseAudio — is
|
||||
**dlopened**, so it is used when present and skipped when absent. That means
|
||||
one image runs on a full desktop, on a Wayland-only session, on a
|
||||
KMSDRM-only handheld with no X server, and on a box with ALSA but no
|
||||
PulseAudio, without a different build for each.
|
||||
|
||||
That property does not come for free from Debian's packages, and getting it
|
||||
is most of what the build below is doing; see
|
||||
[Why five libraries are built from source](#why-five-libraries-are-built-from-source).
|
||||
|
||||
## For builders
|
||||
|
||||
```sh
|
||||
scripts/build_linux_arm64.sh --version 0.1.0
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
dist/linux-arm64/gen1recomp-<version>-linux-arm64.AppImage
|
||||
dist/linux-arm64/gen1recomp-<version>-linux-arm64.AppImage.sha256
|
||||
```
|
||||
|
||||
Useful flags: `--game-love PATH` reuses an already-packed payload (CI does
|
||||
this so every platform ships identical bytes), `--rebuild-image` forces the
|
||||
builder container to rebuild, `--clean-cache` throws away the pinned
|
||||
downloads and the compiled LÖVE prefix.
|
||||
|
||||
### Requirements
|
||||
|
||||
An **aarch64 host** with **docker or podman**. A Raspberry Pi 5 is the
|
||||
reference machine (a cold build takes about 10 minutes on one — six libraries
|
||||
plus the engine; rebuilds reuse the cached prefix and take seconds). Apple Silicon with Docker
|
||||
Desktop and GitHub's `ubuntu-24.04-arm` runner both work too.
|
||||
|
||||
The script refuses to run on x86_64 rather than falling back to qemu-user
|
||||
emulation: that path takes hours and has produced miscompiled LuaJIT.
|
||||
|
||||
### Why this is not just another `scripts/build.sh` target
|
||||
|
||||
`scripts/build.sh linux` downloads LÖVE's official `love-11.5-x86_64.AppImage`,
|
||||
unpacks its squashfs, drops `game.love` in, and glues it back together. That
|
||||
trick is not available here — **LÖVE publishes no aarch64 binary at all.** The
|
||||
11.5 release has win32, win64, macOS, Android, iOS and one x86_64 AppImage,
|
||||
and that is the entire list.
|
||||
|
||||
So this build compiles LÖVE 11.5 from the official `linux-src` tarball and
|
||||
assembles the AppImage from scratch. Every pinned input — the LÖVE source, the
|
||||
five libraries built alongside it, and the AppImage type-2 runtime — is
|
||||
SHA-256 verified on the host before the container ever sees it, and the
|
||||
container itself runs with no network access.
|
||||
|
||||
### Why the build happens in a Debian bullseye container
|
||||
|
||||
glibc is backward compatible but not forward compatible: a binary linked
|
||||
against glibc 2.41 will not start on a system with 2.31, and there is no way
|
||||
to fix that after the fact. Compiling on the oldest base we support is
|
||||
therefore the only thing that makes one artifact work everywhere.
|
||||
|
||||
Bullseye (glibc 2.31) is that base. The resulting binaries actually come out
|
||||
needing only **glibc 2.29** and **GLIBCXX_3.4.21**, so the AppImage covers
|
||||
everything from Ubuntu 20.04 and Raspberry Pi OS bullseye through current
|
||||
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.
|
||||
|
||||
### Why five libraries are built from source
|
||||
|
||||
SDL2, OpenAL, libtheora, libogg/libvorbis and libmpg123 are compiled rather
|
||||
than installed from bullseye. In every case the reason is *correctness*, not
|
||||
a newer version number — Debian builds these for a system where every
|
||||
dependency is installed and co-versioned, which is the opposite of an
|
||||
AppImage's situation. Each one broke the build in a different way, and all
|
||||
three failure modes are now assertions that fail the build instead of
|
||||
shipping.
|
||||
|
||||
**1. Hard-linked backends (SDL2, OpenAL).** Debian's `libSDL2` lists
|
||||
`libpulse`, `libasound`, `libX11` and `libwayland-client` as `DT_NEEDED` —
|
||||
resolved by the loader at startup, not dlopened. An AppImage bundling it
|
||||
refuses to start unless the host has *all four*. It appeared to work in
|
||||
testing only because a desktop Pi has all four; a headless CI runner is what
|
||||
exposed it. Debian's OpenAL does the same via `libsndio`, which itself
|
||||
hard-links `libasound`. Built from source with `--enable-*-shared` and
|
||||
`ALSOFT_DLOPEN`, both dlopen their backends instead.
|
||||
|
||||
**2. A stray link (libtheora).** Debian's `libtheoradec.so.1` is linked
|
||||
against `libcairo.so.2` — a packaging artifact, since a video decoder has no
|
||||
business drawing vector graphics — and cairo drags in X11, xcb, fontconfig
|
||||
and freetype. `--disable-examples` produces a `libtheoradec` needing only
|
||||
`libogg`.
|
||||
|
||||
**3. SONAME collision with the host (ogg, vorbis, mpg123).** The subtle one.
|
||||
OpenAL dlopens ALSA, ALSA's config loads its PulseAudio hook plugin, and that
|
||||
plugin pulls the *host's* `libsndfile` into our process. `libsndfile` links
|
||||
`libogg`, `libvorbis` and `libmpg123` — the same three we bundle. The loader
|
||||
resolves a SONAME exactly once per process, so the host's `libsndfile` binds
|
||||
to *our* copies:
|
||||
|
||||
```
|
||||
openal -> libasound -> libasound_module_conf_pulse -> libsndfile (host, new)
|
||||
`-> mpg123_info2 -> libmpg123 (ours, bullseye 1.26)
|
||||
```
|
||||
|
||||
`mpg123_info2` arrived in mpg123 1.32, so the plugin failed to relocate, ALSA
|
||||
config collapsed, and the game ran with **no audio device at all**. Not
|
||||
bundling these instead would make `libogg`/`libvorbis`/`libmpg123` mandatory
|
||||
host packages; building them current means our copies *satisfy* the host's
|
||||
`libsndfile` rather than starving it.
|
||||
|
||||
The same collision is why the font stack — freetype, fontconfig, libpng,
|
||||
brotli, zlib — is left to the host entirely. Bundling a bullseye freetype
|
||||
2.10.4 meant a host `libcairo` could not find `FT_Get_Transform` (added in
|
||||
2.11) and the game died at startup. Leaving the whole stack to the host keeps
|
||||
it self-consistent, while `liblove` — compiled against 2.10.4 — only ever
|
||||
asks for symbols every supported host already has.
|
||||
|
||||
The general rule this all reduces to: **never bundle a library the host's own
|
||||
stack may also load, unless yours is at least as new as theirs.**
|
||||
|
||||
### CI
|
||||
|
||||
Three jobs, path-gated on `scripts/build_linux_arm64.sh`,
|
||||
`scripts/linux-arm64/`, `scripts/pack_love.sh` and this document:
|
||||
|
||||
- **`linux-arm64-selftest`** (`ubuntu-latest`, x86_64) — offline gate. Checks
|
||||
the pins are real digests on a dated tag rather than the moving
|
||||
`continuous` one, that the Dockerfile still builds on bullseye, that the
|
||||
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.
|
||||
- **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.
|
||||
|
||||
Unlike the Switch job, none of this needs secrets or self-hosted hardware, so
|
||||
it runs on fork PRs too.
|
||||
|
||||
### Updating the pins
|
||||
|
||||
Both pins live in `scripts/linux-arm64/common.sh`:
|
||||
|
||||
- `LOVE_VERSION` / `LOVE_SRC_SHA256` — bumping any version invalidates the
|
||||
cached prefix automatically (its name is keyed by every source version at
|
||||
once, so a partial rebuild cannot mix vintages). Check that bullseye still
|
||||
has `-dev` packages new enough for the new release; `build_appimage.sh`
|
||||
asserts every optional module actually linked, because LÖVE's `configure`
|
||||
exits 0 and silently drops a module when one is missing.
|
||||
- `SDL2_*`, `OPENAL_*`, `THEORA_*`, `OGG_*`, `VORBIS_*`, `MPG123_*` — the
|
||||
source-built libraries. Bumping these is usually safe and occasionally
|
||||
necessary: `libmpg123` in particular must stay at least as new as what a
|
||||
target host's `libsndfile` expects, which is asserted for `mpg123_info2`.
|
||||
- `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`.
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the aarch64 (arm64) Linux AppImage.
|
||||
#
|
||||
# scripts/build.sh's `linux` target only produces x86_64: it unpacks LÖVE's
|
||||
# official x86_64 AppImage and re-fuses it, and no aarch64 equivalent is
|
||||
# published. This script compiles LÖVE 11.5 from the official linux-src
|
||||
# tarball inside a Debian bullseye arm64 container and fuses game.love into a
|
||||
# type-2 AppImage, so one artifact covers Raspberry Pi OS, Armbian, Ubuntu
|
||||
# arm64 and the aarch64 handhelds.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/build_linux_arm64.sh [--version X.Y.Z] [--game-love PATH]
|
||||
# [--rebuild-image] [--clean-cache]
|
||||
#
|
||||
# Output:
|
||||
# dist/linux-arm64/gen1recomp-<version>-linux-arm64.AppImage
|
||||
# dist/linux-arm64/gen1recomp-<version>-linux-arm64.AppImage.sha256
|
||||
#
|
||||
# Requirements: docker or podman on an aarch64 host (a Raspberry Pi 5, an
|
||||
# ubuntu-24.04-arm runner or Apple Silicon Docker all work). Nothing is
|
||||
# cross-compiled and no qemu emulation is involved.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
. "$ROOT/scripts/linux-arm64/common.sh"
|
||||
|
||||
HERE="$ROOT/.bazinga"
|
||||
CACHE="$HERE/cache/linux-arm64"
|
||||
WORK="$HERE/work/linux-arm64"
|
||||
DIST="$ROOT/dist/linux-arm64"
|
||||
|
||||
VERSION="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo dev)"
|
||||
GAME_LOVE=""
|
||||
REBUILD_IMAGE=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version) VERSION="${2:?--version needs a value}"; shift ;;
|
||||
--game-love) GAME_LOVE="${2:?--game-love needs a path}"; shift ;;
|
||||
--rebuild-image) REBUILD_IMAGE=1 ;;
|
||||
--clean-cache) rm -rf "$CACHE" ;;
|
||||
-h|--help)
|
||||
sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# --------------------------------------------------------------- host checks
|
||||
# aarch64 only. The container is arch-native; running it under qemu-user on an
|
||||
# x86_64 host "works" but takes hours and has produced miscompiled LuaJIT
|
||||
# before, so refuse rather than hand back a build nobody can trust.
|
||||
host_arch="$(uname -m)"
|
||||
case "$host_arch" in
|
||||
aarch64|arm64) ;;
|
||||
*) fail "this build must run on an aarch64 host (found: $host_arch).
|
||||
Use a Raspberry Pi 5 / arm64 VM / Apple Silicon, or the ubuntu-24.04-arm CI runner." ;;
|
||||
esac
|
||||
|
||||
RUNTIME="$(container_runtime)" || fail_need_container
|
||||
say "container runtime: $RUNTIME"
|
||||
|
||||
mkdir -p "$CACHE" "$WORK" "$DIST"
|
||||
|
||||
# --------------------------------------------------------------- game.love
|
||||
# Shared packer, same include/exclude set and the same verification gates as
|
||||
# every other platform, so this artifact can never drift from the desktop one.
|
||||
if [ -n "$GAME_LOVE" ]; then
|
||||
[ -f "$GAME_LOVE" ] || fail "--game-love: no such file: $GAME_LOVE"
|
||||
say "using prebuilt payload: $GAME_LOVE"
|
||||
else
|
||||
GAME_LOVE="$WORK/game.love"
|
||||
"$ROOT/scripts/pack_love.sh" \
|
||||
--output "$GAME_LOVE" \
|
||||
--listing "$WORK/love-listing.txt" \
|
||||
--version "$VERSION"
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------- icon
|
||||
# One source of truth for every platform's launcher icon (scripts/build.sh
|
||||
# resizes the same file with sips on macOS). Pillow is already a project
|
||||
# dependency via tools/build_data.py; without it, ship the 1024px original
|
||||
# rather than failing the build over an icon.
|
||||
IN_DIR="$WORK/in"
|
||||
rm -rf "$IN_DIR"; mkdir -p "$IN_DIR"
|
||||
ICON_SRC="$ROOT/assets/logo/gen1recomp_cover.png"
|
||||
[ -f "$ICON_SRC" ] || fail "missing icon source: $ICON_SRC"
|
||||
if ! python3 - "$ICON_SRC" "$IN_DIR/icon.png" <<'PY' 2>/dev/null
|
||||
import sys
|
||||
from PIL import Image
|
||||
with Image.open(sys.argv[1]) as image:
|
||||
image.convert("RGBA").resize((512, 512), Image.LANCZOS).save(sys.argv[2])
|
||||
PY
|
||||
then
|
||||
warn "Pillow not available, shipping the unresized icon"
|
||||
cp "$ICON_SRC" "$IN_DIR/icon.png"
|
||||
fi
|
||||
cp "$GAME_LOVE" "$IN_DIR/game.love"
|
||||
|
||||
# --------------------------------------------------------------- downloads
|
||||
# Fetched on the host and checksum-pinned here so the container never needs
|
||||
# network access and every input is verified in exactly one place.
|
||||
download_pinned "$LOVE_SRC_URL" "$CACHE/$LOVE_SRC_TARBALL" "$LOVE_SRC_SHA256"
|
||||
download_pinned "$SDL2_URL" "$CACHE/$SDL2_TARBALL" "$SDL2_SHA256"
|
||||
download_pinned "$OPENAL_URL" "$CACHE/$OPENAL_TARBALL" "$OPENAL_SHA256"
|
||||
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 "$APPIMAGE_RUNTIME_URL" "$CACHE/$APPIMAGE_RUNTIME_NAME" \
|
||||
"$APPIMAGE_RUNTIME_SHA256"
|
||||
|
||||
# --------------------------------------------------------------- builder image
|
||||
if [ "$REBUILD_IMAGE" = 1 ] || ! "$RUNTIME" image inspect "$BUILDER_IMAGE" >/dev/null 2>&1; then
|
||||
say "building $BUILDER_IMAGE ($BUILDER_BASE_IMAGE)"
|
||||
"$RUNTIME" build -t "$BUILDER_IMAGE" \
|
||||
-f "$ROOT/scripts/linux-arm64/Dockerfile" "$ROOT/scripts/linux-arm64" \
|
||||
|| fail "failed to build the $BUILDER_BASE_IMAGE builder image"
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------- build
|
||||
OUT_DIR="$WORK/out"
|
||||
rm -rf "$OUT_DIR"; mkdir -p "$OUT_DIR"
|
||||
|
||||
# --user keeps the AppImage owned by the invoking user instead of root; podman
|
||||
# maps root in the container to the host user already, so only docker needs it.
|
||||
user_args=()
|
||||
if [ "$RUNTIME" = "docker" ]; then
|
||||
user_args=(--user "$(id -u):$(id -g)")
|
||||
fi
|
||||
|
||||
say "compiling and packaging inside $BUILDER_BASE_IMAGE"
|
||||
"$RUNTIME" run --rm ${user_args[@]+"${user_args[@]}"} \
|
||||
-e LOVE_VERSION="$LOVE_VERSION" \
|
||||
-e SDL2_VERSION="$SDL2_VERSION" \
|
||||
-e SDL2_TARBALL="$SDL2_TARBALL" \
|
||||
-e OPENAL_VERSION="$OPENAL_VERSION" \
|
||||
-e OPENAL_TARBALL="$OPENAL_TARBALL" \
|
||||
-e THEORA_VERSION="$THEORA_VERSION" \
|
||||
-e THEORA_TARBALL="$THEORA_TARBALL" \
|
||||
-e OGG_VERSION="$OGG_VERSION" \
|
||||
-e OGG_TARBALL="$OGG_TARBALL" \
|
||||
-e VORBIS_VERSION="$VORBIS_VERSION" \
|
||||
-e VORBIS_TARBALL="$VORBIS_TARBALL" \
|
||||
-e MPG123_VERSION="$MPG123_VERSION" \
|
||||
-e MPG123_TARBALL="$MPG123_TARBALL" \
|
||||
-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" \
|
||||
"$BUILDER_IMAGE" bash /scripts/build_appimage.sh
|
||||
|
||||
# --------------------------------------------------------------- publish
|
||||
built="$OUT_DIR/$APP_NAME-$VERSION-linux-arm64.AppImage"
|
||||
[ -f "$built" ] || fail "container produced no AppImage at $built"
|
||||
|
||||
# The runtime is a static-pie ELF and the payload starts where its section
|
||||
# headers end; a truncated cat would still be "a file", so prove both halves
|
||||
# survived before shipping.
|
||||
head -c 4 "$built" | od -An -tx1 | tr -d ' \n' | grep -q '^7f454c46$' \
|
||||
|| fail "built AppImage is not an ELF"
|
||||
e_shoff=$(od -An -j40 -N8 -tu8 "$built" | tr -d ' ')
|
||||
e_shentsize=$(od -An -j58 -N2 -tu2 "$built" | tr -d ' ')
|
||||
e_shnum=$(od -An -j60 -N2 -tu2 "$built" | tr -d ' ')
|
||||
sfs_offset=$((e_shoff + e_shentsize * e_shnum))
|
||||
[ "$(dd if="$built" bs=1 skip="$sfs_offset" count=4 2>/dev/null)" = "hsqs" ] \
|
||||
|| fail "no squashfs payload at offset $sfs_offset (runtime/payload fusion failed)"
|
||||
|
||||
out="$DIST/$(basename "$built")"
|
||||
rm -f "$out" "$out.sha256"
|
||||
mv "$built" "$out"
|
||||
chmod +x "$out"
|
||||
printf '%s %s\n' "$(sha256_file "$out")" "$(basename "$out")" > "$out.sha256"
|
||||
|
||||
say "Linux arm64 build: $out ($(du -h "$out" | cut -f1))"
|
||||
say "sha256: $(cut -d' ' -f1 "$out.sha256")"
|
||||
@@ -0,0 +1,45 @@
|
||||
# Build environment for the aarch64 Linux AppImage.
|
||||
#
|
||||
# Debian bullseye on purpose: it ships glibc 2.31, the oldest runtime we
|
||||
# promise to support. Everything linked here therefore runs on bullseye and
|
||||
# every later distro (glibc is backward compatible, not forward), which is
|
||||
# what makes the resulting AppImage portable across Raspberry Pi OS, Armbian,
|
||||
# Ubuntu 20.04+, and the aarch64 handheld distros.
|
||||
#
|
||||
# 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
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# build-essential/autoconf: LÖVE 11.5's linux-src tarball is autotools.
|
||||
# squashfs-tools: packs the AppDir into the AppImage payload.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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,
|
||||
# love.font, love.video), so they are pinned here deliberately and asserted
|
||||
# after the build.
|
||||
#
|
||||
# The X11/Wayland/audio -dev packages are here for SDL2's *build*, not for
|
||||
# runtime linkage: SDL detects each backend at compile time and then dlopens
|
||||
# it, so these headers decide which backends exist at all while adding no
|
||||
# DT_NEEDED entry to the shipped library.
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
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 \
|
||||
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 \
|
||||
libxinerama-dev libxss-dev libxkbcommon-dev \
|
||||
libwayland-dev wayland-protocols libdrm-dev libgbm-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /work
|
||||
Executable
+427
@@ -0,0 +1,427 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compiles LÖVE for aarch64 and fuses game.love into a self-contained
|
||||
# AppImage. Runs INSIDE the Debian bullseye container from Dockerfile --
|
||||
# scripts/build_linux_arm64.sh is the entry point on the host.
|
||||
#
|
||||
# Mounts the host provides:
|
||||
# /cache pinned downloads + the compiled LÖVE prefix (persists between runs)
|
||||
# /in read-only inputs: game.love, icon.png
|
||||
# /out the finished AppImage lands here
|
||||
#
|
||||
# Environment:
|
||||
# LOVE_VERSION, APP_NAME, VERSION passed through from the host script
|
||||
# JOBS make -j (defaults to nproc)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOVE_VERSION="${LOVE_VERSION:?}"
|
||||
SDL2_VERSION="${SDL2_VERSION:?}"
|
||||
SDL2_TARBALL="${SDL2_TARBALL:?}"
|
||||
OPENAL_VERSION="${OPENAL_VERSION:?}"
|
||||
OPENAL_TARBALL="${OPENAL_TARBALL:?}"
|
||||
THEORA_VERSION="${THEORA_VERSION:?}"
|
||||
THEORA_TARBALL="${THEORA_TARBALL:?}"
|
||||
OGG_VERSION="${OGG_VERSION:?}"
|
||||
OGG_TARBALL="${OGG_TARBALL:?}"
|
||||
VORBIS_VERSION="${VORBIS_VERSION:?}"
|
||||
VORBIS_TARBALL="${VORBIS_TARBALL:?}"
|
||||
MPG123_VERSION="${MPG123_VERSION:?}"
|
||||
MPG123_TARBALL="${MPG123_TARBALL:?}"
|
||||
APP_NAME="${APP_NAME:?}"
|
||||
VERSION="${VERSION:?}"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
|
||||
CACHE="/cache"
|
||||
IN="/in"
|
||||
OUT="/out"
|
||||
WORK="/tmp/build"
|
||||
|
||||
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$WORK"
|
||||
|
||||
# --------------------------------------------------------------- prefix
|
||||
# Everything we compile lands in one prefix, cached because the compiling is
|
||||
# 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"
|
||||
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 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.
|
||||
build_autotools() { # $1 = label $2 = version $3 = tarball $4 = probe lib $5.. = configure args
|
||||
local label="$1" version="$2" tarball="$3" probe="$4"; shift 4
|
||||
if [ -f "$PREFIX/lib/$probe" ]; then
|
||||
say "reusing cached $label $version"
|
||||
return 0
|
||||
fi
|
||||
say "compiling $label $version"
|
||||
local src="$WORK/$label-src"
|
||||
rm -rf "$src"; mkdir -p "$src"
|
||||
case "$tarball" in
|
||||
*.tar.bz2) tar -xjf "$CACHE/$tarball" -C "$src" --strip-components=1 ;;
|
||||
*) tar -xzf "$CACHE/$tarball" -C "$src" --strip-components=1 ;;
|
||||
esac
|
||||
(
|
||||
cd "$src"
|
||||
# Several of these tarballs predate aarch64's entry in config.guess; the
|
||||
# distro's copies know about it, so refresh them or configure bails out
|
||||
# with "cannot guess build type".
|
||||
for helper in config.guess config.sub; do
|
||||
[ -f "$helper" ] && cp "/usr/share/misc/$helper" . 2>/dev/null
|
||||
done
|
||||
./configure --prefix="$PREFIX" --disable-static "$@" >/dev/null
|
||||
make -j"$JOBS" >/dev/null
|
||||
make install >/dev/null
|
||||
)
|
||||
}
|
||||
|
||||
build_autotools ogg "$OGG_VERSION" "$OGG_TARBALL" libogg.so.0
|
||||
build_autotools vorbis "$VORBIS_VERSION" "$VORBIS_TARBALL" libvorbis.so.0
|
||||
# mpg123's ports/ tree and the command-line player are irrelevant here; only
|
||||
# libmpg123 gets linked, and --disable-modules keeps the output-backend
|
||||
# plugins (and their dlopen of ALSA/pulse) out of the shipped library.
|
||||
build_autotools mpg123 "$MPG123_VERSION" "$MPG123_TARBALL" libmpg123.so.0 \
|
||||
--disable-modules --with-audio=dummy --disable-lfs-alias
|
||||
|
||||
# The symbol that was missing when this was bullseye's copy. Assert it, so a
|
||||
# version bump that quietly regresses below the host's expectations fails the
|
||||
# build instead of silently killing audio again.
|
||||
objdump -T "$PREFIX/lib/libmpg123.so.0" | grep -q 'mpg123_info2' \
|
||||
|| fail "bundled libmpg123 lacks mpg123_info2; the host's libsndfile will fail to relocate"
|
||||
|
||||
# ------------------------------------------------------------ compile SDL2
|
||||
# --enable-*-shared (the defaults, made explicit so a future SDL release
|
||||
# cannot flip them under us) is the entire reason this is built from source:
|
||||
# each backend is dlopened at runtime rather than becoming a DT_NEEDED entry,
|
||||
# so the AppImage starts on a host with only ALSA, or only Wayland, or only
|
||||
# KMSDRM, instead of demanding all of them at once the way Debian's build does.
|
||||
if [ -f "$PREFIX/lib/libSDL2-2.0.so.0" ]; then
|
||||
say "reusing cached SDL2 $SDL2_VERSION"
|
||||
else
|
||||
say "compiling SDL2 $SDL2_VERSION (jobs: $JOBS)"
|
||||
rm -rf "$WORK/sdl-src"; mkdir -p "$WORK/sdl-src"
|
||||
tar -xzf "$CACHE/$SDL2_TARBALL" -C "$WORK/sdl-src" --strip-components=1
|
||||
(
|
||||
cd "$WORK/sdl-src"
|
||||
./configure --prefix="$PREFIX" --disable-static \
|
||||
--enable-alsa --enable-alsa-shared \
|
||||
--enable-pulseaudio --enable-pulseaudio-shared \
|
||||
--enable-video-x11 --enable-x11-shared \
|
||||
--enable-video-wayland --enable-wayland-shared \
|
||||
--enable-video-kmsdrm --enable-kmsdrm-shared \
|
||||
--enable-libudev --disable-sndio --disable-jack --disable-esd \
|
||||
--disable-arts --disable-nas --disable-oss >/dev/null
|
||||
make -j"$JOBS" >/dev/null
|
||||
make install >/dev/null
|
||||
)
|
||||
fi
|
||||
|
||||
# Prove the dlopen intent actually took. If SDL ever hard-links an audio or
|
||||
# video backend again, the AppImage silently regains a startup dependency on
|
||||
# the host having that exact stack -- which is the bug this replaced.
|
||||
sdl_lib="$PREFIX/lib/libSDL2-2.0.so.0"
|
||||
[ -f "$sdl_lib" ] || fail "SDL2 build produced no libSDL2-2.0.so.0"
|
||||
for forbidden in libpulse libasound libX11 libwayland libdrm libgbm libsndio; do
|
||||
if objdump -p "$sdl_lib" | grep -q "NEEDED.*$forbidden"; then
|
||||
fail "SDL2 hard-links $forbidden; it must dlopen its backends (--enable-*-shared)"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------- compile openal-soft
|
||||
# ALSOFT_DLOPEN keeps the ALSA and PulseAudio backends behind dlopen, and
|
||||
# sndio is switched off outright -- Debian enables it, which is what chained
|
||||
# libopenal -> libsndio -> libasound into a mandatory startup dependency.
|
||||
if [ -f "$PREFIX/lib/libopenal.so.1" ]; then
|
||||
say "reusing cached openal-soft $OPENAL_VERSION"
|
||||
else
|
||||
say "compiling openal-soft $OPENAL_VERSION (jobs: $JOBS)"
|
||||
rm -rf "$WORK/openal-src"; mkdir -p "$WORK/openal-src"
|
||||
tar -xzf "$CACHE/$OPENAL_TARBALL" -C "$WORK/openal-src" --strip-components=1
|
||||
(
|
||||
cd "$WORK/openal-src"
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="$PREFIX" \
|
||||
-DALSOFT_DLOPEN=ON \
|
||||
-DALSOFT_BACKEND_SNDIO=OFF \
|
||||
-DALSOFT_BACKEND_OSS=OFF \
|
||||
-DALSOFT_BACKEND_JACK=OFF \
|
||||
-DALSOFT_EXAMPLES=OFF \
|
||||
-DALSOFT_UTILS=OFF \
|
||||
-DALSOFT_TESTS=OFF \
|
||||
-DLIBTYPE=SHARED >/dev/null
|
||||
cmake --build build -j"$JOBS" >/dev/null
|
||||
cmake --install build >/dev/null
|
||||
)
|
||||
fi
|
||||
|
||||
openal_lib="$PREFIX/lib/libopenal.so.1"
|
||||
[ -f "$openal_lib" ] || fail "openal-soft build produced no libopenal.so.1"
|
||||
for forbidden in libsndio libasound libpulse libjack; do
|
||||
if objdump -p "$openal_lib" | grep -q "NEEDED.*$forbidden"; then
|
||||
fail "openal hard-links $forbidden; backends must stay behind dlopen"
|
||||
fi
|
||||
done
|
||||
|
||||
# --------------------------------------------------- compile libtheora
|
||||
# --disable-examples is what drops Debian's libcairo link (and with it libX11,
|
||||
# libxcb, libfontconfig and libfreetype as startup dependencies). The encoder
|
||||
# is dead weight for a player, but libtheoradec is what LÖVE actually links.
|
||||
if [ -f "$PREFIX/lib/libtheoradec.so.1" ]; then
|
||||
say "reusing cached libtheora $THEORA_VERSION"
|
||||
else
|
||||
say "compiling libtheora $THEORA_VERSION"
|
||||
rm -rf "$WORK/theora-src"; mkdir -p "$WORK/theora-src"
|
||||
tar -xjf "$CACHE/$THEORA_TARBALL" -C "$WORK/theora-src" --strip-components=1
|
||||
(
|
||||
cd "$WORK/theora-src"
|
||||
# theora 1.1.1 predates the aarch64 config.guess, so refresh the autotools
|
||||
# helper scripts or configure rejects the host outright.
|
||||
for helper in config.guess config.sub; do
|
||||
cp "/usr/share/misc/$helper" . 2>/dev/null || true
|
||||
done
|
||||
./configure --prefix="$PREFIX" --disable-static \
|
||||
--disable-examples --disable-spec --disable-doc >/dev/null
|
||||
make -j"$JOBS" >/dev/null
|
||||
make install >/dev/null
|
||||
)
|
||||
fi
|
||||
|
||||
theora_lib="$PREFIX/lib/libtheoradec.so.1"
|
||||
[ -f "$theora_lib" ] || fail "libtheora build produced no libtheoradec.so.1"
|
||||
if objdump -p "$theora_lib" | grep -q "NEEDED.*libcairo"; then
|
||||
fail "libtheoradec still links libcairo (--disable-examples stopped working)"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------ compile LÖVE
|
||||
if [ -x "$PREFIX/bin/love" ] && [ -f "$PREFIX/lib/liblove-$LOVE_VERSION.so" ]; then
|
||||
say "reusing cached LÖVE $LOVE_VERSION aarch64 build"
|
||||
else
|
||||
say "compiling LÖVE $LOVE_VERSION for aarch64 (jobs: $JOBS)"
|
||||
rm -rf "$WORK/love-src"
|
||||
mkdir -p "$WORK/love-src"
|
||||
tar -xzf "$CACHE/love-$LOVE_VERSION-linux-src.tar.gz" \
|
||||
-C "$WORK/love-src" --strip-components=1
|
||||
(
|
||||
cd "$WORK/love-src"
|
||||
# No --disable-* flags on purpose: configure silently drops a love module
|
||||
# when its -dev package is absent, so the Dockerfile pins the full set and
|
||||
# the assertions below prove each one actually linked. CPPFLAGS/LDFLAGS
|
||||
# point at our prefix so the SDL2 and theora just built above win over
|
||||
# anything the base image might still provide.
|
||||
./configure --prefix="$PREFIX" --disable-static \
|
||||
CPPFLAGS="-I$PREFIX/include" LDFLAGS="-L$PREFIX/lib" >/dev/null
|
||||
make -j"$JOBS" >/dev/null
|
||||
make install >/dev/null
|
||||
# Keep LÖVE's license inside the cached prefix: the unpacked source tree
|
||||
# is thrown away, so a later cache-hit run would otherwise have nothing
|
||||
# to ship and the AppImage would go out without its engine license.
|
||||
cp license.txt "$PREFIX/license.txt"
|
||||
)
|
||||
fi
|
||||
|
||||
love_bin="$PREFIX/bin/love"
|
||||
love_lib="$PREFIX/lib/liblove-$LOVE_VERSION.so"
|
||||
[ -x "$love_bin" ] || fail "LÖVE build produced no bin/love"
|
||||
[ -f "$love_lib" ] || fail "LÖVE build produced no lib/liblove-$LOVE_VERSION.so"
|
||||
file "$love_bin" | grep -q 'ARM aarch64' \
|
||||
|| fail "built love is not an aarch64 ELF (got: $(file -b "$love_bin"))"
|
||||
|
||||
# A configure run that lost an optional dependency still exits 0 and still
|
||||
# builds -- the loss only shows up as a missing love module at runtime, i.e.
|
||||
# in a shipped artifact. Assert the decoder/font/video libs really linked.
|
||||
for soname in libSDL2-2.0.so.0 libopenal.so.1 libfreetype.so.6 \
|
||||
libmodplug.so.1 libmpg123.so.0 libvorbisfile.so.3 \
|
||||
libtheoradec.so.1 libluajit-5.1.so.2; do
|
||||
objdump -p "$love_lib" | grep -q "NEEDED.*$soname" \
|
||||
|| fail "liblove is not linked against $soname (a -dev package went missing)"
|
||||
done
|
||||
|
||||
# --------------------------------------------------------------- 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
|
||||
# FUSE_PATH fusion scripts/build.sh performs on the x86_64 image -- stay the
|
||||
# same idea on both architectures.
|
||||
APPDIR="$WORK/AppDir"
|
||||
rm -rf "$APPDIR"
|
||||
mkdir -p "$APPDIR/bin" "$APPDIR/lib" "$APPDIR/share"
|
||||
|
||||
cp "$love_bin" "$APPDIR/bin/love"
|
||||
chmod +x "$APPDIR/bin/love"
|
||||
|
||||
# ------------------------------------------------------ bundle dependencies
|
||||
# Walk the DT_NEEDED graph from love + liblove, copying in everything that is
|
||||
# not host-provided. Recursion stops at excluded libraries, so the driver and
|
||||
# session subtrees behind SDL2 are never pulled in.
|
||||
#
|
||||
# Three reasons a library MUST come from the host, and every entry below is
|
||||
# one of them:
|
||||
#
|
||||
# 1. Driver/session coupled. A bundled libGL would bypass Mesa's V3D driver
|
||||
# on the Pi; a bundled libpulse/libdbus would fight the user's running
|
||||
# session. GL/EGL/gbm/drm, X11/xcb/wayland/xkbcommon, dbus, pulse, alsa,
|
||||
# systemd/udev. Note that after the source builds above, none of these are
|
||||
# DT_NEEDED of anything we ship -- SDL2 and OpenAL dlopen them, so they are
|
||||
# used when present and skipped when absent.
|
||||
#
|
||||
# 2. Loader coupled. glibc's pieces cannot be mixed with the host's ld.so at
|
||||
# all, and libstdc++/libgcc_s must be at least as new as the compiler --
|
||||
# bullseye's gcc 10 is older than any supported host's, so the host copy
|
||||
# always satisfies us.
|
||||
#
|
||||
# 3. The font/compression stack: freetype, fontconfig, libpng, brotli, zlib.
|
||||
# These are shared with whatever the host's own graphics libraries have
|
||||
# already loaded, and mixing vintages inside one process breaks the older
|
||||
# copy. Bundling a bullseye freetype 2.10.4 is what made a host cairo fail
|
||||
# to find FT_Get_Transform (added in 2.11) and killed the game at startup.
|
||||
# Leaving the whole stack to the host keeps it self-consistent, and
|
||||
# liblove -- compiled against 2.10.4 -- only ever asks for symbols every
|
||||
# supported host already has.
|
||||
EXCLUDE_RE='^(ld-linux-aarch64\.so\.1|libc\.so\.6|libm\.so\.6|libdl\.so\.2|libpthread\.so\.0|librt\.so\.1|libresolv\.so\.2|libutil\.so\.1|libanl\.so\.1|libnsl\.so\.[0-9]+|libstdc\+\+\.so\.6|libgcc_s\.so\.1|lib(GL|GLX|GLdispatch|OpenGL|EGL|GLESv[12]|glapi|gbm|drm)\..*|libX[a-z0-9]*\..*|libxcb.*|libwayland-.*|libxkbcommon.*|libdbus-1\..*|libpulse.*|libasound\..*|libsndfile\..*|libFLAC\..*|libopus\..*|libsystemd\..*|libudev\..*|libselinux\..*|libcap\..*|libgcrypt\..*|libgpg-error\..*|liblzma\..*|libzstd\..*|liblz4\..*|libffi\..*|libexpat\..*|libbsd\..*|libmd\..*|libuuid\..*|libg(lib|object|module|thread)-2\..*|libfontconfig\..*|libfreetype\..*|libpng[0-9]*\..*|libbrotli.*|libz\.so\..*|libwrap\..*|libasyncns\..*|libtirpc\..*|lib(gssapi_krb5|krb5|k5crypto|com_err|krb5support|keyutils)\..*|libpcre.*)$'
|
||||
|
||||
# soname -> absolute path, harvested from the full ldd closure of both roots.
|
||||
declare -A RESOLVED=()
|
||||
while read -r soname _arrow path _addr; do
|
||||
[ -n "${path:-}" ] || continue
|
||||
[ -e "$path" ] || continue
|
||||
RESOLVED["$soname"]="$path"
|
||||
done < <(ldd "$love_bin" "$love_lib" | awk '/=>/ {print $1, $2, $3, $4}')
|
||||
|
||||
declare -A BUNDLED=()
|
||||
bundle_needed() { # $1 = ELF whose DT_NEEDED entries to walk
|
||||
local soname target
|
||||
while read -r soname; do
|
||||
[ -n "$soname" ] || continue
|
||||
if [[ "$soname" =~ $EXCLUDE_RE ]]; then continue; fi
|
||||
if [ -n "${BUNDLED[$soname]:-}" ]; then continue; fi
|
||||
target="${RESOLVED[$soname]:-}"
|
||||
[ -n "$target" ] || fail "cannot resolve $soname (needed by $(basename "$1"))"
|
||||
# Copy dereferenced and under the soname: the AppDir must not depend on
|
||||
# the builder's libSDL2-2.0.so.0 -> libSDL2-2.0.so.0.14.0 symlink chain.
|
||||
cp -L "$target" "$APPDIR/lib/$soname"
|
||||
chmod 0644 "$APPDIR/lib/$soname"
|
||||
BUNDLED["$soname"]=1
|
||||
bundle_needed "$APPDIR/lib/$soname"
|
||||
done < <(objdump -p "$1" | awk '/NEEDED/ {print $2}')
|
||||
}
|
||||
|
||||
say "bundling shared libraries"
|
||||
cp "$love_lib" "$APPDIR/lib/liblove-$LOVE_VERSION.so"
|
||||
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"
|
||||
say "bundled $(ls "$APPDIR/lib" | wc -l) libraries: $(ls "$APPDIR/lib" | tr '\n' ' ')"
|
||||
|
||||
# ------------------------------------------------- host dependency contract
|
||||
# The portability promise, stated as an assertion instead of a paragraph in a
|
||||
# README: these are the ONLY sonames the shipped objects may require from the
|
||||
# host. Everything driver-, session- or audio-related has to be reached
|
||||
# through dlopen, so the AppImage starts on a box with no PulseAudio, no X11
|
||||
# or no ALSA and simply uses whatever it does find.
|
||||
#
|
||||
# The original build failed exactly here and nobody noticed until CI ran on a
|
||||
# headless runner: Debian's SDL2 hard-links libpulse/libasound/libX11/
|
||||
# libwayland, so the image only ever started on a full desktop.
|
||||
HOST_ALLOWED_RE='^(ld-linux-aarch64\.so\.1|libc\.so\.6|libm\.so\.6|libdl\.so\.2|libpthread\.so\.0|librt\.so\.1|libstdc\+\+\.so\.6|libgcc_s\.so\.1|libatomic\.so\.1|libfreetype\.so\.6|libpng[0-9]*\.so\.[0-9]+|libz\.so\.1|libbrotli(dec|common)\.so\.1)$'
|
||||
|
||||
unexpected=""
|
||||
for object in "$APPDIR/bin/love" "$APPDIR"/lib/*.so*; do
|
||||
while read -r soname; do
|
||||
[ -n "$soname" ] || continue
|
||||
# Satisfied from inside the AppDir, so not a host requirement at all.
|
||||
if [ -n "${BUNDLED[$soname]:-}" ]; then continue; fi
|
||||
if [[ "$soname" =~ $HOST_ALLOWED_RE ]]; then continue; fi
|
||||
unexpected="$unexpected $(basename "$object") -> $soname"$'\n'
|
||||
done < <(objdump -p "$object" | awk '/NEEDED/ {print $2}')
|
||||
done
|
||||
[ -z "$unexpected" ] || fail "$(printf '%s\n%s' \
|
||||
"these objects hard-require host libraries outside the allowed set (they must be dlopened, not linked):" \
|
||||
"$unexpected")"
|
||||
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"
|
||||
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/"
|
||||
|
||||
# --------------------------------------------------------------- branding
|
||||
cp "$IN/game.love" "$APPDIR/game.love"
|
||||
# The .desktop's Icon= resolves against the AppDir root by basename, and
|
||||
# .DirIcon is what appimaged and file-manager thumbnailers read.
|
||||
cp "$IN/icon.png" "$APPDIR/$APP_NAME.png"
|
||||
cp "$IN/icon.png" "$APPDIR/.DirIcon"
|
||||
|
||||
cat > "$APPDIR/$APP_NAME.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=gen1recomp
|
||||
Comment=Pokémon Gen 1 recompilation
|
||||
Exec=$APP_NAME
|
||||
Icon=$APP_NAME
|
||||
Categories=Game;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
# AppRun follows LÖVE's own, with FUSE_PATH committed to instead of shipped
|
||||
# commented out: this image is a game, not the engine, so it must never fall
|
||||
# through to LÖVE's "no game" screen.
|
||||
cat > "$APPDIR/AppRun" <<EOF
|
||||
#!/bin/sh
|
||||
# gen1recomp aarch64 AppImage launcher.
|
||||
|
||||
if [ -z "\$APPDIR" ]; then
|
||||
APPDIR="\$(dirname "\$(readlink -f "\$0")")"
|
||||
fi
|
||||
|
||||
export LD_LIBRARY_PATH="\$APPDIR/lib/:\$LD_LIBRARY_PATH"
|
||||
|
||||
if [ -z "\$XDG_DATA_DIRS" ]; then
|
||||
XDG_DATA_DIRS="/usr/local/share/:/usr/share/"
|
||||
fi
|
||||
export XDG_DATA_DIRS="\$APPDIR/share/:\$XDG_DATA_DIRS"
|
||||
|
||||
if [ -z "\$LUA_PATH" ]; then
|
||||
LUA_PATH=";"
|
||||
fi
|
||||
export LUA_PATH="\$APPDIR/share/$LUAJIT_SHARE_DIR/?.lua;\$APPDIR/share/lua/5.1/?.lua;\$LUA_PATH"
|
||||
|
||||
if [ -z "\$LUA_CPATH" ]; then
|
||||
LUA_CPATH=";"
|
||||
fi
|
||||
export LUA_CPATH="\$APPDIR/lib/lua/5.1/?.so;\$LUA_CPATH"
|
||||
|
||||
exec "\$APPDIR/bin/love" --fused "\$APPDIR/game.love" "\$@"
|
||||
EOF
|
||||
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"
|
||||
|
||||
# --------------------------------------------------------------- fuse image
|
||||
# An AppImage is just <runtime ELF><squashfs>. gzip at 128K blocks matches what
|
||||
# LÖVE's official image uses and what every type-2 runtime can read; zstd would
|
||||
# be smaller but is not universally supported by older runtimes users may have
|
||||
# registered through appimaged.
|
||||
say "packing squashfs"
|
||||
sfs="$WORK/payload.squashfs"
|
||||
rm -f "$sfs"
|
||||
mksquashfs "$APPDIR" "$sfs" \
|
||||
-comp gzip -b 131072 -noappend -all-root -no-xattrs -quiet >/dev/null
|
||||
|
||||
out="$OUT/$APP_NAME-$VERSION-linux-arm64.AppImage"
|
||||
rm -f "$out"
|
||||
cat "$CACHE/runtime-aarch64" "$sfs" > "$out"
|
||||
chmod +x "$out"
|
||||
|
||||
say "AppImage: $(basename "$out") ($(du -h "$out" | cut -f1))"
|
||||
Executable
+180
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers and pins for the aarch64 Linux AppImage build.
|
||||
# Source from other scripts: . "$(dirname "$0")/common.sh"
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
if [ -z "${ROOT:-}" ]; then
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
fi
|
||||
export ROOT
|
||||
|
||||
# ---------------------------------------------------------------- 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
|
||||
# this port compiles the official linux-src tarball instead of unpacking a
|
||||
# prebuilt image the way scripts/build.sh does for x86_64.
|
||||
LOVE_VERSION="11.5"
|
||||
LOVE_SRC_TARBALL="love-$LOVE_VERSION-linux-src.tar.gz"
|
||||
LOVE_SRC_URL="https://github.com/love2d/love/releases/download/$LOVE_VERSION/$LOVE_SRC_TARBALL"
|
||||
LOVE_SRC_SHA256="066e0843f71aa9fd28b8eaf27d41abb74bfaef7556153ac2e3cf08eafc874c39"
|
||||
|
||||
# SDL2 is built from source rather than taken from bullseye, and this is a
|
||||
# correctness requirement, not a version preference. Debian's libSDL2 lists
|
||||
# libpulse, libasound, libX11 and libwayland-client as DT_NEEDED -- hard links
|
||||
# resolved by the loader at startup -- so an AppImage bundling it refuses to
|
||||
# launch unless the host has ALL FOUR installed. That is wrong for an artifact
|
||||
# whose whole job is to run on arbitrary arm64 systems: an ALSA-only handheld
|
||||
# or a minimal Wayland box would die before main(). Built from source, SDL
|
||||
# defaults to dlopening every audio and video backend (--enable-*-shared), so
|
||||
# it loads whichever the host actually has and degrades gracefully.
|
||||
# The newer version is a bonus: 2.30 has a far better controller database and
|
||||
# real KMSDRM support, both of which matter on Pi-class and handheld hardware.
|
||||
SDL2_VERSION="2.30.12"
|
||||
SDL2_TARBALL="SDL2-$SDL2_VERSION.tar.gz"
|
||||
SDL2_URL="https://github.com/libsdl-org/SDL/releases/download/release-$SDL2_VERSION/$SDL2_TARBALL"
|
||||
SDL2_SHA256="ac356ea55e8b9dd0b2d1fa27da40ef7e238267ccf9324704850d5d47375b48ea"
|
||||
|
||||
# libtheora likewise. Debian's libtheoradec.so.1 is linked against libcairo --
|
||||
# a packaging artifact, since a video decoder has no business drawing vector
|
||||
# graphics -- and cairo drags in libX11, libxcb, libfontconfig and libfreetype
|
||||
# as hard dependencies. LOVE needs theora for love.video, so that link would
|
||||
# put the entire X11 and font stack on the critical path at startup, and it is
|
||||
# what caused the FT_Get_Transform crash this build hit on a trixie host.
|
||||
# Upstream's tarball with --disable-examples produces a libtheoradec that
|
||||
# needs only libogg.
|
||||
THEORA_VERSION="1.1.1"
|
||||
THEORA_TARBALL="libtheora-$THEORA_VERSION.tar.bz2"
|
||||
THEORA_URL="https://downloads.xiph.org/releases/theora/$THEORA_TARBALL"
|
||||
THEORA_SHA256="b6ae1ee2fa3d42ac489287d3ec34c5885730b1296f0801ae577a35193d3affbc"
|
||||
|
||||
# OpenAL for the same reason as SDL2, one level down. Debian's libopenal is
|
||||
# openal-soft built with the sndio backend enabled, so it hard-links
|
||||
# libsndio, which itself hard-links libasound -- reintroducing exactly the
|
||||
# mandatory-ALSA dependency the SDL2 source build exists to remove. Upstream
|
||||
# openal-soft dlopens its backends, so building it here leaves the shipped
|
||||
# library with no audio-stack dependency at all.
|
||||
OPENAL_VERSION="1.23.1"
|
||||
OPENAL_TARBALL="openal-soft-$OPENAL_VERSION.tar.gz"
|
||||
OPENAL_URL="https://github.com/kcat/openal-soft/archive/refs/tags/$OPENAL_VERSION.tar.gz"
|
||||
OPENAL_SHA256="dfddf3a1f61059853c625b7bb03de8433b455f2f79f89548cbcbd5edca3d4a4a"
|
||||
|
||||
# The audio codecs are built from source for a third, different reason: SONAME
|
||||
# collision with the host's audio stack.
|
||||
#
|
||||
# OpenAL dlopens ALSA, ALSA's config loads its PulseAudio hook plugin, and that
|
||||
# plugin pulls the HOST's libsndfile into our process. libsndfile links
|
||||
# libogg, libvorbis and libmpg123 -- the same three we bundle. The loader
|
||||
# resolves a SONAME once per process, so the host's libsndfile binds to OUR
|
||||
# copies, and a bullseye libmpg123 has no mpg123_info2 (added in 1.32):
|
||||
#
|
||||
# openal -> libasound -> libasound_module_conf_pulse -> libsndfile (host)
|
||||
# `-> mpg123_info2 -> libmpg123 (ours, bullseye)
|
||||
#
|
||||
# which failed to relocate and left the game with no audio device at all.
|
||||
# Not bundling them instead would make libogg/libvorbis/libmpg123 mandatory
|
||||
# host packages; building them current means our copies satisfy the host's
|
||||
# libsndfile rather than starving it. libvorbisfile ships in the vorbis
|
||||
# tarball.
|
||||
OGG_VERSION="1.3.5"
|
||||
OGG_TARBALL="libogg-$OGG_VERSION.tar.gz"
|
||||
OGG_URL="https://downloads.xiph.org/releases/ogg/$OGG_TARBALL"
|
||||
OGG_SHA256="0eb4b4b9420a0f51db142ba3f9c64b333f826532dc0f48c6410ae51f4799b664"
|
||||
|
||||
VORBIS_VERSION="1.3.7"
|
||||
VORBIS_TARBALL="libvorbis-$VORBIS_VERSION.tar.gz"
|
||||
VORBIS_URL="https://downloads.xiph.org/releases/vorbis/$VORBIS_TARBALL"
|
||||
VORBIS_SHA256="0e982409a9c3fc82ee06e08205b1355e5c6aa4c36bca58146ef399621b0ce5ab"
|
||||
|
||||
MPG123_VERSION="1.32.10"
|
||||
MPG123_TARBALL="mpg123-$MPG123_VERSION.tar.bz2"
|
||||
MPG123_URL="https://www.mpg123.de/download/$MPG123_TARBALL"
|
||||
MPG123_SHA256="87b2c17fe0c979d3ef38eeceff6362b35b28ac8589fbf1854b5be75c9ab6557c"
|
||||
|
||||
# AppImage type-2 runtime: the ~900 KB static-pie ELF that gets prepended to
|
||||
# the squashfs payload. Pinned to a dated tag, never "continuous", so a
|
||||
# rebuild months from now produces the same bytes.
|
||||
APPIMAGE_RUNTIME_TAG="20251108"
|
||||
APPIMAGE_RUNTIME_NAME="runtime-aarch64"
|
||||
APPIMAGE_RUNTIME_URL="https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_TAG/$APPIMAGE_RUNTIME_NAME"
|
||||
APPIMAGE_RUNTIME_SHA256="00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444"
|
||||
|
||||
# Debian bullseye (glibc 2.31) is the compile environment, NOT a statement
|
||||
# about where the artifact runs. glibc is backward compatible but not forward
|
||||
# compatible, so linking against the oldest glibc we support is what lets one
|
||||
# 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}"
|
||||
|
||||
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")"
|
||||
curl -fL --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
|
||||
docker or podman on an aarch64 host.
|
||||
|
||||
Raspberry Pi OS / Debian / Ubuntu: sudo apt install docker.io && sudo usermod -aG docker "$USER"
|
||||
Fedora / Asahi: sudo dnf install podman
|
||||
macOS (Apple Silicon): brew install --cask docker
|
||||
|
||||
Override the runtime with GEN1_CONTAINER_RUNTIME=podman.
|
||||
See docs/linux-arm64-build.md.
|
||||
EOF
|
||||
)"
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
# Offline checks for the aarch64 Linux AppImage build.
|
||||
#
|
||||
# Runs anywhere -- no container, no network, no aarch64 host -- so PR CI can
|
||||
# gate the parts of this build that do not need three minutes of compiling.
|
||||
# The real build is exercised separately by the linux-arm64-build job.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=common.sh
|
||||
. "$SCRIPT_DIR/common.sh"
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
|
||||
}
|
||||
require_command unzip
|
||||
require_command zip
|
||||
|
||||
say "checking shell entry points"
|
||||
bash -n "$ROOT/scripts/build_linux_arm64.sh" "$SCRIPT_DIR"/*.sh
|
||||
help="$(bash "$ROOT/scripts/build_linux_arm64.sh" --help)"
|
||||
printf '%s' "$help" | grep -q -- '--version X.Y.Z' \
|
||||
|| fail "build help does not document --version"
|
||||
printf '%s' "$help" | grep -q 'linux-arm64\.AppImage' \
|
||||
|| fail "build help does not name the artifact it produces"
|
||||
|
||||
say "checking the host-architecture guard"
|
||||
# The guard is what stops someone from kicking off a qemu-emulated build that
|
||||
# takes hours and miscompiles LuaJIT. Prove it fires rather than trusting it.
|
||||
# The guard is what stops someone from kicking off a qemu-emulated build that
|
||||
# takes hours and has miscompiled LuaJIT before. Prove it fires by shadowing
|
||||
# uname, rather than trusting the branch is reachable.
|
||||
fake_bin="$(mktemp -d "${TMPDIR:-/tmp}/gen1recomp-fake-uname.XXXXXX")"
|
||||
printf '#!/bin/sh\necho x86_64\n' > "$fake_bin/uname"
|
||||
chmod +x "$fake_bin/uname"
|
||||
guard_out="$(PATH="$fake_bin:$PATH" \
|
||||
bash "$ROOT/scripts/build_linux_arm64.sh" --version 0.0.0 2>&1 || true)"
|
||||
rm -rf "$fake_bin"
|
||||
printf '%s' "$guard_out" | grep -q 'aarch64 host' \
|
||||
|| fail "build script does not refuse to run on a non-aarch64 host"
|
||||
|
||||
say "checking pinned inputs"
|
||||
# Pins must be real digests, and the AppImage runtime must come from a dated
|
||||
# tag: "continuous" is a moving target and would make rebuilds unreproducible.
|
||||
for pin_name in LOVE_SRC_SHA256 SDL2_SHA256 OPENAL_SHA256 THEORA_SHA256 \
|
||||
OGG_SHA256 VORBIS_SHA256 MPG123_SHA256 APPIMAGE_RUNTIME_SHA256; do
|
||||
pin_value="${!pin_name}"
|
||||
printf '%s' "$pin_value" | grep -Eq '^[0-9a-f]{64}$' \
|
||||
|| fail "$pin_name is not a sha256 digest: $pin_value"
|
||||
done
|
||||
if printf '%s' "$APPIMAGE_RUNTIME_URL" | grep -q '/continuous/'; then
|
||||
fail "the AppImage runtime is pinned to the moving 'continuous' tag"
|
||||
fi
|
||||
printf '%s' "$APPIMAGE_RUNTIME_URL" | grep -q "/$APPIMAGE_RUNTIME_TAG/$APPIMAGE_RUNTIME_NAME\$" \
|
||||
|| fail "APPIMAGE_RUNTIME_URL does not match the pinned tag/asset"
|
||||
printf '%s' "$LOVE_SRC_URL" | grep -q "/$LOVE_VERSION/$LOVE_SRC_TARBALL\$" \
|
||||
|| fail "LOVE_SRC_URL does not match 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" ] \
|
||||
|| fail "BUILDER_BASE_IMAGE disagrees with the Dockerfile"
|
||||
|
||||
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
|
||||
# bundling the engine's own dependencies.
|
||||
EXCLUDE_RE="$(
|
||||
# shellcheck disable=SC1090
|
||||
grep -m1 "^EXCLUDE_RE=" "$SCRIPT_DIR/build_appimage.sh" | sed "s/^EXCLUDE_RE='//; s/'\$//"
|
||||
)"
|
||||
[ -n "$EXCLUDE_RE" ] || fail "could not read EXCLUDE_RE out of build_appimage.sh"
|
||||
|
||||
must_exclude=(libc.so.6 ld-linux-aarch64.so.1 libstdc++.so.6 libgcc_s.so.1
|
||||
libGL.so.1 libEGL.so.1 libgbm.so.1 libdrm.so.2 libX11.so.6
|
||||
libwayland-client.so.0 libpulse.so.0 libasound.so.2
|
||||
libfreetype.so.6 libfontconfig.so.1 libpng16.so.16 libz.so.1)
|
||||
must_bundle=(libSDL2-2.0.so.0 libopenal.so.1 libluajit-5.1.so.2 libmodplug.so.1
|
||||
libmpg123.so.0 libogg.so.0 libvorbis.so.0 libvorbisfile.so.3
|
||||
libtheoradec.so.1 liblove-11.5.so)
|
||||
|
||||
for soname in "${must_exclude[@]}"; do
|
||||
[[ "$soname" =~ $EXCLUDE_RE ]] \
|
||||
|| fail "$soname must be host-provided but the exclude list would bundle it"
|
||||
done
|
||||
for soname in "${must_bundle[@]}"; do
|
||||
if [[ "$soname" =~ $EXCLUDE_RE ]]; then
|
||||
fail "$soname is an engine dependency but the exclude list drops it"
|
||||
fi
|
||||
done
|
||||
|
||||
say "checking AppRun and the fusion contract"
|
||||
# The AppImage must boot straight into the game. If AppRun ever loses --fused,
|
||||
# users get vanilla LÖVE's "no game" screen instead, and nothing else catches
|
||||
# that before someone downloads a release.
|
||||
grep -qF -- '--fused "\$APPDIR/game.love"' "$SCRIPT_DIR/build_appimage.sh" \
|
||||
|| fail "AppRun no longer launches game.love with --fused"
|
||||
grep -qF 'LD_LIBRARY_PATH="\$APPDIR/lib/' "$SCRIPT_DIR/build_appimage.sh" \
|
||||
|| fail "AppRun no longer puts the bundled lib directory on LD_LIBRARY_PATH"
|
||||
grep -qF 'comp gzip -b 131072' "$SCRIPT_DIR/build_appimage.sh" \
|
||||
|| fail "squashfs payload is no longer gzip/128K (older type-2 runtimes cannot read it)"
|
||||
|
||||
say "checking the linked-module assertions"
|
||||
# configure exits 0 when an optional -dev package is missing and just drops the
|
||||
# module, so these assertions are the only thing standing between a missing
|
||||
# build dependency and a release that cannot play sound.
|
||||
for soname in libSDL2-2.0.so.0 libopenal.so.1 libfreetype.so.6 libmodplug.so.1 \
|
||||
libmpg123.so.0 libvorbisfile.so.3 libtheoradec.so.1; do
|
||||
grep -qF "$soname" "$SCRIPT_DIR/build_appimage.sh" \
|
||||
|| fail "build_appimage.sh no longer asserts liblove links $soname"
|
||||
done
|
||||
|
||||
say "checking the dlopen guarantees"
|
||||
# SDL2, OpenAL and libtheora are compiled from source for correctness, not for
|
||||
# a newer version number: Debian's builds hard-link libpulse/libasound/libX11/
|
||||
# libwayland (SDL2), libsndio (OpenAL) and libcairo (libtheora), each of which
|
||||
# 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
|
||||
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 -- '--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" \
|
||||
|| fail "SDL2 is no longer configured to dlopen its video backends"
|
||||
grep -qF 'ALSOFT_DLOPEN=ON' "$SCRIPT_DIR/build_appimage.sh" \
|
||||
|| fail "openal-soft is no longer configured to dlopen its backends"
|
||||
grep -qF -- '--disable-examples' "$SCRIPT_DIR/build_appimage.sh" \
|
||||
|| fail "libtheora is no longer built with --disable-examples (it regains the libcairo link)"
|
||||
|
||||
say "checking the host dependency contract"
|
||||
# The shipped objects may require nothing from the host beyond glibc,
|
||||
# libstdc++ and the font stack. Everything driver-, session- or audio-related
|
||||
# has to be dlopened. This is the invariant a headless CI runner proved was
|
||||
# broken the first time round.
|
||||
HOST_ALLOWED_RE="$(
|
||||
grep -m1 "^HOST_ALLOWED_RE=" "$SCRIPT_DIR/build_appimage.sh" \
|
||||
| sed "s/^HOST_ALLOWED_RE='//; s/'\$//"
|
||||
)"
|
||||
[ -n "$HOST_ALLOWED_RE" ] || fail "could not read HOST_ALLOWED_RE out of build_appimage.sh"
|
||||
for soname in libpulse.so.0 libasound.so.2 libX11.so.6 libwayland-client.so.0 \
|
||||
libGL.so.1 libcairo.so.2 libsndio.so.7.0 libdbus-1.so.3; do
|
||||
if [[ "$soname" =~ $HOST_ALLOWED_RE ]]; then
|
||||
fail "$soname is allowed as a hard host dependency; it must be dlopened"
|
||||
fi
|
||||
done
|
||||
for soname in libc.so.6 libstdc++.so.6 libfreetype.so.6 libz.so.1; do
|
||||
[[ "$soname" =~ $HOST_ALLOWED_RE ]] \
|
||||
|| fail "$soname must be allowed as a host dependency but the contract rejects it"
|
||||
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" \
|
||||
--version 1.2.3 \
|
||||
--dry-run >/dev/null
|
||||
unzip -p "$temp_dir/game.love" src/core/Version.lua \
|
||||
| grep -Eq 'engine[[:space:]]*=[[:space:]]*"1\.2\.3"' \
|
||||
|| fail "shared payload version was not stamped"
|
||||
|
||||
say "Linux arm64 self-test passed"
|
||||
Reference in New Issue
Block a user