mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
Ship a Linux arm64 (aarch64) AppImage
scripts/build.sh's `linux` target only ever produces x86_64: it unpacks LOVE's official love-11.5-x86_64.AppImage and re-fuses game.love into it. There is no aarch64 equivalent to unpack -- LOVE 11.5 publishes win32, win64, macOS, Android, iOS and exactly one x86_64 AppImage -- so arm64 desktop Linux (Raspberry Pi 4/5, Armbian, arm64 VMs on Apple Silicon) had no artifact at all. Compile LOVE 11.5 from the official linux-src tarball instead, inside a Debian bullseye arm64 container, and assemble the AppImage from scratch. Both pinned inputs (the LOVE source tarball and the AppImage type-2 runtime, on a dated tag rather than `continuous`) are SHA-256 verified on the host, so the container runs with no network access. Bullseye is the compile environment, not a claim about where the artifact runs: glibc is backward but not forward compatible, so linking against the oldest supported glibc is the only thing that makes one artifact work everywhere. The binaries come out needing only glibc 2.29 / GLIBCXX_3.4.21, covering Raspberry Pi OS bullseye through trixie and Ubuntu 20.04 onward. The dependency walker copies in LOVE's own libraries and leaves the driver-coupled, loader-coupled and font-stack libraries to the host. That last category is not cosmetic: Debian's libtheoradec is linked against libcairo, so a host cairo gets loaded into the process, and because the loader resolves one SONAME once per process it then binds to whatever libfreetype we bundled -- bullseye's 2.10.4 has no FT_Get_Transform, which cairo 1.18 needs, and the game died at startup with a symbol lookup error. Excluding the whole font stack makes the process self-consistent. CI gets three path-gated jobs: an offline selftest on ubuntu-latest (pins, the host-arch guard, the exclude list, the AppRun fusion contract), a real build on ubuntu-24.04-arm that asserts the layout, that every bundled object resolves under AppRun's LD_LIBRARY_PATH, and that the glibc floor is still <= 2.31, and a release job that reuses the shared game.love payload. None of it needs secrets or self-hosted hardware, so it runs on fork PRs. Verified end to end on a Raspberry Pi 5 (Debian trixie, Wayland): the launcher boots from the AppImage and renders correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/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 "$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 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,29 @@
|
||||
# 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.
|
||||
# The lib*-dev set is LÖVE's full 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.
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential pkg-config autoconf automake libtool \
|
||||
ca-certificates curl file xz-utils zip unzip squashfs-tools \
|
||||
libsdl2-dev libopenal-dev libogg-dev libvorbis-dev libtheora-dev \
|
||||
libmodplug-dev libmpg123-dev libfreetype6-dev libluajit-5.1-dev \
|
||||
zlib1g-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /work
|
||||
Executable
+236
@@ -0,0 +1,236 @@
|
||||
#!/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:?}"
|
||||
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"
|
||||
|
||||
# ------------------------------------------------------------ compile LÖVE
|
||||
# The prefix is cached because this is the only slow step (~3 min on a Pi 5,
|
||||
# and it is identical for every game version). Keyed by LÖVE version so a
|
||||
# LOVE_VERSION bump cannot silently reuse the old build.
|
||||
PREFIX="$CACHE/love-$LOVE_VERSION-prefix"
|
||||
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 "$PREFIX" "$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.
|
||||
./configure --prefix="$PREFIX" --disable-static >/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.
|
||||
#
|
||||
# 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. Shared with the host's font stack -- the subtle one, and the reason
|
||||
# this list is longer than LÖVE's own AppImage manifest. Bullseye's
|
||||
# libtheoradec is (bizarrely, a Debian packaging artifact) linked against
|
||||
# libcairo, so the HOST's cairo gets loaded into our process. Because the
|
||||
# dynamic loader resolves one SONAME once per process, that host cairo
|
||||
# then binds to whatever libfreetype.so.6 we bundled -- and a bullseye
|
||||
# freetype 2.10.4 has no FT_Get_Transform, which cairo 1.18 needs:
|
||||
#
|
||||
# love -> liblove -> libtheoradec -> libcairo (host, new)
|
||||
# `-> FT_Get_Transform -> libfreetype (ours, old) BOOM
|
||||
#
|
||||
# Bundling a newer freetype only moves the arms race. Excluding the whole
|
||||
# font/compression stack instead makes it self-consistent: cairo,
|
||||
# fontconfig and freetype all come from one host and agree with each
|
||||
# other, while 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' ' ')"
|
||||
|
||||
# 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
+108
@@ -0,0 +1,108 @@
|
||||
#!/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"
|
||||
|
||||
# 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
|
||||
)"
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
#!/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 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 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