mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-27 08:51:27 +02:00
feat(linux): ship raw x86 AppImage, Flatpak bundle, and dual-env curl
Drop the pointless zip wrapper around the x86_64 AppImage, harden HostShell so bundled curl keeps APPDIR libs while host curl scrubs LD_LIBRARY_PATH / Steam LD_PRELOAD, and add a Flatpak channel with --device=all, bundled curl, and AppStream releases metadata. Portable mode now probes writability with a unique temp file and falls back soft on RO mounts / Flatpak.
This commit is contained in:
@@ -509,16 +509,24 @@ jobs:
|
||||
mkdir -p "$outdir"
|
||||
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"
|
||||
# x86_64 desktop Linux: raw AppImage (no zip wrapper).
|
||||
x64_appimage="dist/linux/gen1recomp-linux-x86_64.AppImage"
|
||||
[ -f "$x64_appimage" ] || { echo "::error::$x64_appimage not found (expected from scripts/build.sh linux)"; exit 1; }
|
||||
cp "$x64_appimage" "$outdir/gen1recomp-${v}-linux-x86_64.AppImage"
|
||||
chmod +x "$outdir/gen1recomp-${v}-linux-x86_64.AppImage"
|
||||
|
||||
# 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.
|
||||
# AppImage 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"
|
||||
|
||||
flatpak_bundle="dist/flatpak/gen1recomp-${v}-linux.flatpak"
|
||||
[ -f "$flatpak_bundle" ] || { echo "::error::$flatpak_bundle not found (expected from scripts/build_flatpak.sh)"; exit 1; }
|
||||
cp "$flatpak_bundle" "$outdir/gen1recomp-${v}-linux.flatpak"
|
||||
apk="$(find dist/android/release -name '*.apk' | head -1)"
|
||||
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/release"; exit 1; }
|
||||
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
||||
@@ -656,8 +664,9 @@ jobs:
|
||||
release_files=(
|
||||
"dist/release/gen1recomp-${v}-macos.zip"
|
||||
"dist/release/gen1recomp-${v}-windows.zip"
|
||||
"dist/release/gen1recomp-${v}-linux.zip"
|
||||
"dist/release/gen1recomp-${v}-linux-x86_64.AppImage"
|
||||
"dist/release/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
"dist/release/gen1recomp-${v}-linux.flatpak"
|
||||
"dist/release/gen1recomp-${v}-android.apk"
|
||||
"dist/release/gen1recomp++-${v}-ios.ipa"
|
||||
"dist/release/gen1recomp-${v}-switch.zip"
|
||||
|
||||
@@ -228,18 +228,27 @@ entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
|
||||
| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
|
||||
|
||||
|
||||
## Linux on arm64 (Raspberry Pi)
|
||||
## Linux desktop (AppImage / Flatpak)
|
||||
|
||||
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:
|
||||
Releases ship raw AppImages (no zip wrapper) plus an optional Flatpak bundle:
|
||||
|
||||
- `gen1recomp-*-linux-x86_64.AppImage`
|
||||
- `gen1recomp-*-linux-arm64.AppImage` (Raspberry Pi 4/5, Armbian, arm64 VMs)
|
||||
- `gen1recomp-*-linux.flatpak` (see [docs/linux-flatpak.md](docs/linux-flatpak.md))
|
||||
|
||||
```sh
|
||||
chmod +x gen1recomp-*-linux-x86_64.AppImage
|
||||
./gen1recomp-*-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
```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
|
||||
Shared troubleshooting (FUSE, curl, portable mode): [docs/linux-appimage.md](docs/linux-appimage.md).
|
||||
|
||||
LÖVE publishes no aarch64 binary of any kind, so the arm64 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
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Linux AppImage packaging
|
||||
|
||||
Releases ship raw AppImages (no zip wrapper):
|
||||
|
||||
- `gen1recomp-<version>-linux-x86_64.AppImage`
|
||||
- `gen1recomp-<version>-linux-arm64.AppImage`
|
||||
|
||||
```sh
|
||||
chmod +x gen1recomp-*-linux-x86_64.AppImage
|
||||
./gen1recomp-*-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
Flatpak users should prefer the `.flatpak` bundle (see
|
||||
[linux-flatpak.md](linux-flatpak.md)); it avoids host glibc / FUSE / curl
|
||||
mismatches on immutable desktops.
|
||||
|
||||
## Common failure modes
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| Won't start / `libfuse.so.2` | No FUSE | `sudo apt install libfuse2` (or `libfuse2t64`) **or** `./app.AppImage --appimage-extract-and-run` |
|
||||
| Update check / mods / save sync fail | Host `curl` missing or broken by AppImage `LD_LIBRARY_PATH` | Install curl; current builds scrub `LD_LIBRARY_PATH` for **host** curl and keep it for a bundled AppDir curl |
|
||||
| Settings / sync reset after quit | Portable mode next to a read-only AppImage parent (`/opt`, system dir) | Remove `portable.txt` or move the AppImage to a writable folder; the game falls back to the XDG save dir when the probe write fails |
|
||||
| "Download AppImage update" | Shell/`minShell` gate needs a full native package | Download the new `.AppImage`, `chmod +x`, replace the old file |
|
||||
| Steam / Game Mode weirdness after update | Overlay `LD_PRELOAD` / PID change | HostShell unsets `LD_PRELOAD` for children and `execv`s `$APPIMAGE` on restart |
|
||||
|
||||
## Network transport (HostShell)
|
||||
|
||||
Desktop Linux fetches go through host or bundled `curl`:
|
||||
|
||||
1. Flatpak `/app/bin/curl` (bundled, keep sandbox libs)
|
||||
2. `$APPDIR/usr/bin/curl` or `$APPDIR/bin/curl` (keep `$APPDIR` on `LD_LIBRARY_PATH`)
|
||||
3. Host `curl` (`env -u LD_LIBRARY_PATH`, and always `-u LD_PRELOAD`)
|
||||
|
||||
## Portable mode
|
||||
|
||||
Drop `portable.txt` beside the `.AppImage` to keep saves next to the binary.
|
||||
The launcher probes writability with a unique `.write_probe_<time>_<rand>.tmp`
|
||||
file. Read-only parents fail soft and use `love.filesystem` XDG saves instead.
|
||||
Flatpak ignores `portable.txt`.
|
||||
|
||||
## Auto-update
|
||||
|
||||
In-place updates download `gen1recomp-X.Y.Z.love` into the save directory.
|
||||
Full shell bumps open the matching AppImage (or Flatpak) download URL — there
|
||||
is no silent in-place AppImage replace yet.
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
# x86_64 (macOS or Linux host with squashfs-tools)
|
||||
scripts/build.sh linux --version X.Y.Z
|
||||
|
||||
# arm64 (aarch64 host + docker/podman)
|
||||
scripts/build_linux_arm64.sh --version X.Y.Z
|
||||
```
|
||||
|
||||
See [linux-arm64-build.md](linux-arm64-build.md) for the arm64 builder.
|
||||
@@ -1,10 +1,11 @@
|
||||
# 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
|
||||
existing x86_64 `gen1recomp-<version>-linux-x86_64.AppImage`. 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.
|
||||
aarch64 handhelds that run a full distro. See also
|
||||
[linux-appimage.md](linux-appimage.md) for shared AppImage failure modes.
|
||||
|
||||
> The Anbernic RG34XXSP has its own PortMaster-style pack
|
||||
> (`gen1recomp-*-rg34xxsp-stockos64-mod.zip`, see
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Linux Flatpak
|
||||
|
||||
Releases may ship `gen1recomp-<version>-linux.flatpak` (x86_64 bundle).
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
flatpak install --user ./gen1recomp-<version>-linux.flatpak
|
||||
flatpak run com.theboisclub.gen1recomp
|
||||
```
|
||||
|
||||
Or open the `.flatpak` in Discover / GNOME Software. AppStream metainfo
|
||||
includes a `<releases>` entry so those UIs can show the version.
|
||||
|
||||
## Permissions
|
||||
|
||||
The manifest requests:
|
||||
|
||||
- network (updater, mods, save sync)
|
||||
- X11 / Wayland / DRI / PulseAudio
|
||||
- `--device=all` (SDL2 gamepads, rumble, gyro via evdev/hidraw)
|
||||
- `--filesystem=home` (ROM import)
|
||||
|
||||
Curl is bundled at `/app/bin/curl` so the game does not need host curl.
|
||||
|
||||
## Updates
|
||||
|
||||
Lua/engine payloads still use the in-app `.love` updater. A native shell bump
|
||||
points at the new `.flatpak` asset (`Download Flatpak update`).
|
||||
|
||||
After a `.love` payload download, restart uses `love.event.quit("restart")`
|
||||
(`execv` of `/proc/self/exe` inside bwrap). If a restart hangs after an
|
||||
update, fully quit and relaunch — PhysFS can retain a lock on the previous
|
||||
payload across a bad handoff.
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
scripts/build_flatpak.sh --version X.Y.Z
|
||||
# → dist/flatpak/gen1recomp-X.Y.Z-linux.flatpak
|
||||
```
|
||||
|
||||
Requires `flatpak` + `flatpak-builder` and the Freedesktop 24.08 runtime from
|
||||
Flathub (installed automatically on first build).
|
||||
|
||||
Flathub store submission is out of scope for the GitHub bundle channel.
|
||||
+3
-2
@@ -63,8 +63,9 @@ mounted or deleted as stale; the launcher directs the player to a full package.
|
||||
## Release assets
|
||||
|
||||
Each tagged release `vX.Y.Z` carries the existing per-platform archives
|
||||
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`, `-linux.zip`,
|
||||
`-linux-arm64.AppImage`, `-android.apk`, `-ios.ipa`, `-switch.zip`, Xbox and
|
||||
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`,
|
||||
`-linux-x86_64.AppImage`, `-linux-arm64.AppImage`, `-linux.flatpak`,
|
||||
`-android.apk`, `-ios.ipa`, `-switch.zip`, Xbox and
|
||||
PortMaster archives) plus two assets the updater itself consumes:
|
||||
|
||||
- `gen1recomp-X.Y.Z.love` - the payload, matched by the exact pattern
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=gen1recomp
|
||||
Comment=Pokémon Gen 1/2 recompilation
|
||||
Exec=gen1recomp
|
||||
Icon=com.theboisclub.gen1recomp
|
||||
Terminal=false
|
||||
Categories=Game;
|
||||
StartupWMClass=love
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>com.theboisclub.gen1recomp</id>
|
||||
<name>gen1recomp</name>
|
||||
<summary>Pokémon Gen 1/2 recompilation</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>LicenseRef-proprietary</project_license>
|
||||
<developer_name>The Bois Club</developer_name>
|
||||
<description>
|
||||
<p>
|
||||
A LÖVE-based recompilation of Pokémon Gen 1 and Gen 2 with a launcher,
|
||||
ROM importer, mods, and optional cloud save sync. Requires your own
|
||||
legal cartridge dumps.
|
||||
</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">com.theboisclub.gen1recomp.desktop</launchable>
|
||||
<url type="homepage">https://github.com/bryanthaboi/gen1recomp</url>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>Launcher</caption>
|
||||
<image>https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/assets/logo/gen1recomp_cover.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
<release version="0.0.0" date="1970-01-01"/>
|
||||
</releases>
|
||||
</component>
|
||||
@@ -0,0 +1,61 @@
|
||||
# Template consumed by scripts/build_flatpak.sh (placeholders substituted).
|
||||
# Do not pass this file directly to flatpak-builder.
|
||||
app-id: com.theboisclub.gen1recomp
|
||||
runtime: org.freedesktop.Platform
|
||||
runtime-version: "24.08"
|
||||
sdk: org.freedesktop.Sdk
|
||||
command: gen1recomp
|
||||
finish-args:
|
||||
- --share=network
|
||||
- --share=ipc
|
||||
- --socket=x11
|
||||
- --socket=wayland
|
||||
- --device=dri
|
||||
- --socket=pulseaudio
|
||||
# Full input device access so SDL2 sees udev/evdev gamepads + rumble/gyro
|
||||
- --device=all
|
||||
- --filesystem=home
|
||||
modules:
|
||||
- name: curl
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- tar -xJf curl-static.tar.xz
|
||||
- install -Dm755 curl /app/bin/curl
|
||||
sources:
|
||||
- type: file
|
||||
url: https://github.com/stunnel/static-curl/releases/download/8.21.0/curl-linux-x86_64-musl-8.21.0.tar.xz
|
||||
sha256: e955f211202ded2536164588331acfc987dc4b7857efa3577717b1ffeab22029
|
||||
dest-filename: curl-static.tar.xz
|
||||
|
||||
- name: love
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- chmod +x love-x86_64.AppImage
|
||||
- ./love-x86_64.AppImage --appimage-extract
|
||||
- mkdir -p /app/lib/love /app/bin /app/share/gen1recomp
|
||||
- cp -a squashfs-root/bin/love /app/bin/love
|
||||
- cp -a squashfs-root/lib/. /app/lib/love/
|
||||
- |
|
||||
cat > /app/bin/gen1recomp <<'EOF'
|
||||
#!/bin/sh
|
||||
export LD_LIBRARY_PATH="/app/lib/love:${LD_LIBRARY_PATH:-}"
|
||||
exec /app/bin/love /app/share/gen1recomp/game.love "$@"
|
||||
EOF
|
||||
- chmod +x /app/bin/gen1recomp
|
||||
- install -Dm644 game.love /app/share/gen1recomp/game.love
|
||||
- install -Dm644 com.theboisclub.gen1recomp.desktop /app/share/applications/com.theboisclub.gen1recomp.desktop
|
||||
- install -Dm644 com.theboisclub.gen1recomp.metainfo.xml /app/share/metainfo/com.theboisclub.gen1recomp.metainfo.xml
|
||||
- install -Dm644 icon.png /app/share/icons/hicolor/512x512/apps/com.theboisclub.gen1recomp.png
|
||||
sources:
|
||||
- type: file
|
||||
url: https://github.com/love2d/love/releases/download/11.5/love-11.5-x86_64.AppImage
|
||||
sha256: 65a673406431eff7167a15a032bf7a2e4ba50108e091eb7b176465831f9b5e00
|
||||
dest-filename: love-x86_64.AppImage
|
||||
- type: file
|
||||
path: game.love
|
||||
- type: file
|
||||
path: com.theboisclub.gen1recomp.metainfo.xml
|
||||
- type: file
|
||||
path: com.theboisclub.gen1recomp.desktop
|
||||
- type: file
|
||||
path: icon.png
|
||||
+32
-12
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# Output: dist/mac/gen1recomp-macos.zip
|
||||
# dist/win/gen1recomp-win64.zip
|
||||
# dist/linux/gen1recomp-linux.zip (fused x86_64 AppImage)
|
||||
# dist/linux/gen1recomp-linux-x86_64.AppImage (fused x86_64 AppImage)
|
||||
# dist/android/debug/*.apk (full gradle output stays under
|
||||
# mobile/android/app/build/outputs/apk/embedNoRecord/)
|
||||
# dist/ios/<Config>-<sdk>/gen1recomp++.app (full xcodebuild output stays
|
||||
@@ -457,15 +457,31 @@ EOF
|
||||
# file-manager thumbnailers show for the file itself.
|
||||
[ -f "$ICON_SRC" ] || fail "missing icon source: $ICON_SRC"
|
||||
rm -f "$appdir/love.svg" "$appdir/love.png" "$appdir/.DirIcon"
|
||||
sips -z 512 512 "$ICON_SRC" --out "$appdir/$APP_NAME.png" >/dev/null
|
||||
if command -v sips >/dev/null 2>&1; then
|
||||
sips -z 512 512 "$ICON_SRC" --out "$appdir/$APP_NAME.png" >/dev/null
|
||||
elif command -v convert >/dev/null 2>&1; then
|
||||
convert "$ICON_SRC" -resize 512x512 "$appdir/$APP_NAME.png"
|
||||
else
|
||||
fail "need sips (macOS) or ImageMagick convert to resize $ICON_SRC"
|
||||
fi
|
||||
cp "$appdir/$APP_NAME.png" "$appdir/.DirIcon"
|
||||
|
||||
sed -i '' 's|^#FUSE_PATH="$APPDIR/my_game.love"$|FUSE_PATH="$APPDIR/game.love"|' "$appdir/AppRun"
|
||||
# sed -i '' is BSD; GNU sed wants sed -i (no empty backup suffix).
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i 's|^#FUSE_PATH="$APPDIR/my_game.love"$|FUSE_PATH="$APPDIR/game.love"|' "$appdir/AppRun"
|
||||
else
|
||||
sed -i '' 's|^#FUSE_PATH="$APPDIR/my_game.love"$|FUSE_PATH="$APPDIR/game.love"|' "$appdir/AppRun"
|
||||
fi
|
||||
grep -q '^FUSE_PATH="\$APPDIR/game.love"$' "$appdir/AppRun" \
|
||||
|| fail "failed to enable FUSE_PATH in AppRun (upstream AppRun changed?)"
|
||||
|
||||
sed -i '' 's|^exec "\$APPDIR/bin/love"|if [ -n "$WAYLAND_DISPLAY" ] \&\& [ -z "$SDL_VIDEODRIVER" ]; then export SDL_VIDEODRIVER=x11; fi\
|
||||
exec "$APPDIR/bin/love"|' "$appdir/AppRun"
|
||||
local wayland_hook='if [ -n "$WAYLAND_DISPLAY" ] && [ -z "$SDL_VIDEODRIVER" ]; then export SDL_VIDEODRIVER=x11; fi\
|
||||
exec "$APPDIR/bin/love"'
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i "s|^exec \"\$APPDIR/bin/love\"|$wayland_hook|" "$appdir/AppRun"
|
||||
else
|
||||
sed -i '' "s|^exec \"\$APPDIR/bin/love\"|$wayland_hook|" "$appdir/AppRun"
|
||||
fi
|
||||
|
||||
# Match the upstream image's compression (gzip, 128K blocks) so the
|
||||
# bundled runtime can read it.
|
||||
@@ -474,16 +490,20 @@ exec "$APPDIR/bin/love"|' "$appdir/AppRun"
|
||||
mksquashfs "$appdir" "$sfs_out" \
|
||||
-comp gzip -b 131072 -noappend -all-root -no-xattrs -quiet >/dev/null
|
||||
|
||||
local out_bin="$WORK/$APP_NAME-x86_64.AppImage"
|
||||
rm -f "$out_bin"
|
||||
mkdir -p "$DIST/linux"
|
||||
local out_bin="$DIST/linux/$APP_NAME-linux-x86_64.AppImage"
|
||||
rm -f "$out_bin" "$out_bin.sha256"
|
||||
head -c "$sfs_offset" "$love_appimage" > "$out_bin"
|
||||
cat "$sfs_out" >> "$out_bin"
|
||||
chmod +x "$out_bin"
|
||||
|
||||
local zip_out="$DIST/linux/$APP_NAME-linux.zip"
|
||||
rm -f "$zip_out"
|
||||
(cd "$WORK" && zip -q -9 -j "$zip_out" "$(basename "$out_bin")")
|
||||
say "Linux build: $zip_out"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
printf '%s %s\n' "$(sha256sum "$out_bin" | awk '{print $1}')" "$(basename "$out_bin")" \
|
||||
> "$out_bin.sha256"
|
||||
else
|
||||
printf '%s %s\n' "$(shasum -a 256 "$out_bin" | awk '{print $1}')" "$(basename "$out_bin")" \
|
||||
> "$out_bin.sha256"
|
||||
fi
|
||||
say "Linux build: $out_bin"
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------- Android
|
||||
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a single-file Flatpak bundle for x86_64 desktop Linux.
|
||||
#
|
||||
# Usage: scripts/build_flatpak.sh [--version X.Y.Z] [--game-love PATH]
|
||||
#
|
||||
# Output:
|
||||
# dist/flatpak/gen1recomp-<version>-linux.flatpak
|
||||
#
|
||||
# Requires: flatpak, flatpak-builder, and network on first run (Freedesktop
|
||||
# runtime + LÖVE AppImage download).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_ID="com.theboisclub.gen1recomp"
|
||||
VERSION="0.0.0"
|
||||
GAME_LOVE_IN=""
|
||||
DIST="$ROOT/dist/flatpak"
|
||||
BUILD_DIR="$ROOT/.bazinga/flatpak-build"
|
||||
REPO_DIR="$ROOT/.bazinga/flatpak-repo"
|
||||
STATE_DIR="$ROOT/.bazinga/flatpak-state"
|
||||
CACHE="$ROOT/.bazinga/flatpak-cache"
|
||||
|
||||
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; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version) VERSION="$2"; shift ;;
|
||||
--game-love) GAME_LOVE_IN="$2"; shift ;;
|
||||
-h|--help)
|
||||
sed -n '2,12p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
fail "invalid --version '$VERSION' (expected X.Y.Z)"
|
||||
fi
|
||||
|
||||
command -v flatpak >/dev/null || fail "flatpak not found"
|
||||
command -v flatpak-builder >/dev/null || fail "flatpak-builder not found"
|
||||
|
||||
mkdir -p "$DIST" "$BUILD_DIR" "$REPO_DIR" "$STATE_DIR" "$CACHE"
|
||||
|
||||
LOVE_FILE="$CACHE/game.love"
|
||||
if [ -n "$GAME_LOVE_IN" ]; then
|
||||
[ -f "$GAME_LOVE_IN" ] || fail "game.love not found: $GAME_LOVE_IN"
|
||||
if [ "$(readlink -f "$GAME_LOVE_IN")" != "$(readlink -f "$LOVE_FILE")" ]; then
|
||||
cp "$GAME_LOVE_IN" "$LOVE_FILE"
|
||||
fi
|
||||
else
|
||||
say "packing game.love"
|
||||
"$ROOT/scripts/pack_love.sh" --output "$LOVE_FILE" --version "$VERSION"
|
||||
fi
|
||||
|
||||
# Stage local sources next to the generated manifest (flatpak path: is relative).
|
||||
cp "$ROOT/flatpak/$APP_ID.desktop" "$CACHE/$APP_ID.desktop"
|
||||
# AppStream rejects icons larger than the declared hicolor size.
|
||||
if command -v magick >/dev/null 2>&1; then
|
||||
magick "$ROOT/assets/logo/gen1recomp_cover.png" -resize 512x512 "$CACHE/icon.png"
|
||||
elif command -v convert >/dev/null 2>&1; then
|
||||
convert "$ROOT/assets/logo/gen1recomp_cover.png" -resize 512x512 "$CACHE/icon.png"
|
||||
else
|
||||
fail "need ImageMagick (magick/convert) to resize the Flatpak icon to 512x512"
|
||||
fi
|
||||
|
||||
|
||||
DATE="$(date -u +%Y-%m-%d)"
|
||||
python3 - "$ROOT/flatpak/$APP_ID.metainfo.xml" "$CACHE/$APP_ID.metainfo.xml" "$VERSION" "$DATE" <<'PY'
|
||||
import sys, pathlib, re
|
||||
src, dst, ver, date = sys.argv[1:5]
|
||||
text = pathlib.Path(src).read_text(encoding="utf-8")
|
||||
release = f' <release version="{ver}" date="{date}"/>'
|
||||
text = re.sub(
|
||||
r"<releases>.*?</releases>",
|
||||
"<releases>\n" + release + "\n </releases>",
|
||||
text,
|
||||
count=1,
|
||||
flags=re.S,
|
||||
)
|
||||
pathlib.Path(dst).write_text(text, encoding="utf-8")
|
||||
PY
|
||||
|
||||
cp "$ROOT/flatpak/$APP_ID.yml" "$CACHE/$APP_ID.yml"
|
||||
|
||||
say "installing Freedesktop runtime (no-op if present)"
|
||||
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo || true
|
||||
flatpak --user install -y flathub \
|
||||
org.freedesktop.Platform//24.08 \
|
||||
org.freedesktop.Sdk//24.08 \
|
||||
|| warn "runtime install reported an error; flatpak-builder may still reuse cache"
|
||||
|
||||
say "building Flatpak ($APP_ID $VERSION)"
|
||||
# Run from CACHE so path: sources resolve.
|
||||
(
|
||||
cd "$CACHE"
|
||||
flatpak-builder --user --force-clean \
|
||||
--state-dir="$STATE_DIR" \
|
||||
--repo="$REPO_DIR" \
|
||||
"$BUILD_DIR" "$APP_ID.yml"
|
||||
)
|
||||
|
||||
OUT="$DIST/gen1recomp-${VERSION}-linux.flatpak"
|
||||
rm -f "$OUT"
|
||||
say "exporting bundle $OUT"
|
||||
flatpak build-bundle "$REPO_DIR" "$OUT" "$APP_ID" \
|
||||
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
|
||||
chmod 644 "$OUT"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
printf '%s %s\n' "$(sha256sum "$OUT" | awk '{print $1}')" "$(basename "$OUT")" > "$OUT.sha256"
|
||||
else
|
||||
printf '%s %s\n' "$(shasum -a 256 "$OUT" | awk '{print $1}')" "$(basename "$OUT")" > "$OUT.sha256"
|
||||
fi
|
||||
say "Flatpak build: $OUT ($(du -h "$OUT" | cut -f1))"
|
||||
say "sha256: $(cut -d' ' -f1 "$OUT.sha256")"
|
||||
+114
-24
@@ -4,18 +4,79 @@ local HostShell = {}
|
||||
|
||||
-- Our AppRun exports LD_LIBRARY_PATH="$APPDIR/lib:..." so every subprocess we
|
||||
-- spawn tries to link against the libraries we're shipping instead of the
|
||||
-- system ones. We want to unset the var so that any system tools can find
|
||||
-- their proper libraries. Only needed when running in an AppImage.
|
||||
-- LD_PRELOAD is Steam's overlay (#1470): its 32-bit half cannot load into a
|
||||
-- 64-bit child, so ld.so prints an error into that child's output.
|
||||
-- system ones. Host tools (curl, zenity) need that unset. A *bundled* AppDir
|
||||
-- curl must keep APPDIR on LD_LIBRARY_PATH so it resolves the bundled
|
||||
-- libssl/libcrypto -- scrubbing it forces host OpenSSL and segfaults across
|
||||
-- distros. LD_PRELOAD is always scrubbed: Steam's overlay (#1470) cannot load
|
||||
-- into a 64-bit child.
|
||||
function HostShell.envPrefix()
|
||||
return HostShell.curlEnvPrefix("host")
|
||||
end
|
||||
|
||||
-- kind "bundled" = AppDir / Flatpak curl (keep LD_LIBRARY_PATH, scrub preload).
|
||||
-- kind "host" = system curl (scrub both).
|
||||
function HostShell.curlEnvPrefix(kind)
|
||||
local unset = ""
|
||||
if os.getenv("APPIMAGE") then unset = unset .. "-u LD_LIBRARY_PATH " end
|
||||
if kind ~= "bundled" and os.getenv("APPIMAGE") then
|
||||
unset = unset .. "-u LD_LIBRARY_PATH "
|
||||
end
|
||||
if os.getenv("LD_PRELOAD") then unset = unset .. "-u LD_PRELOAD " end
|
||||
if unset == "" then return "" end
|
||||
return "env " .. unset
|
||||
end
|
||||
|
||||
local curlResolved = nil -- { path=, kind= } once per Lua state
|
||||
|
||||
local function pathIsExecutable(path)
|
||||
if type(path) ~= "string" or path == "" then return false end
|
||||
local f = io.open(path, "rb")
|
||||
if not f then return false end
|
||||
f:close()
|
||||
-- Best-effort: existence is enough; exec bit is checked by the spawn.
|
||||
return true
|
||||
end
|
||||
|
||||
-- Prefer Flatpak /app/bin/curl, then $APPDIR/{usr/,}bin/curl, else host "curl".
|
||||
function HostShell.resolveCurl()
|
||||
if curlResolved ~= nil then return curlResolved.path, curlResolved.kind end
|
||||
local candidates = {}
|
||||
if os.getenv("FLATPAK_ID") then
|
||||
candidates[#candidates + 1] = { "/app/bin/curl", "bundled" }
|
||||
end
|
||||
local appdir = os.getenv("APPDIR")
|
||||
if type(appdir) == "string" and appdir ~= "" then
|
||||
candidates[#candidates + 1] = { appdir .. "/usr/bin/curl", "bundled" }
|
||||
candidates[#candidates + 1] = { appdir .. "/bin/curl", "bundled" }
|
||||
end
|
||||
for _, c in ipairs(candidates) do
|
||||
if pathIsExecutable(c[1]) then
|
||||
curlResolved = { path = c[1], kind = c[2] }
|
||||
return curlResolved.path, curlResolved.kind
|
||||
end
|
||||
end
|
||||
curlResolved = { path = "curl", kind = "host" }
|
||||
return curlResolved.path, curlResolved.kind
|
||||
end
|
||||
|
||||
-- Quoted curl argv0 for a shell command, plus the matching env prefix.
|
||||
function HostShell.curlInvocation()
|
||||
local path, kind = HostShell.resolveCurl()
|
||||
return HostShell.curlEnvPrefix(kind) .. HostShell.quote(path), kind
|
||||
end
|
||||
|
||||
-- Diagnostics for support / About screens.
|
||||
function HostShell.curlDiagnostics()
|
||||
local path, kind = HostShell.resolveCurl()
|
||||
return {
|
||||
path = path,
|
||||
kind = kind,
|
||||
appimage = os.getenv("APPIMAGE") ~= nil,
|
||||
appdir = os.getenv("APPDIR"),
|
||||
flatpakId = os.getenv("FLATPAK_ID"),
|
||||
haveCurl = HostShell.haveCurl(),
|
||||
}
|
||||
end
|
||||
|
||||
-- Windows: every host tool we shell out to (curl for the update and mod-index
|
||||
-- fetches, the PowerShell ROM picker, the update downloader's `start /b`) is
|
||||
-- spawned through io.popen / os.execute, which run it under cmd.exe. A
|
||||
@@ -126,12 +187,18 @@ local function withPopenLock(fn)
|
||||
if not okAtomic then fn() end
|
||||
end
|
||||
|
||||
-- Wraps io.popen with the AppImage env fix applied and lua errors swallowed
|
||||
function HostShell.popen(command, mode)
|
||||
-- Wraps io.popen with the AppImage env fix applied and lua errors swallowed.
|
||||
-- opts.envPrefix overrides the default HostShell.envPrefix() (pass "" to skip,
|
||||
-- or curlEnvPrefix(kind) when spawning a resolved curl binary).
|
||||
function HostShell.popen(command, mode, opts)
|
||||
HostShell.releasePointerGrab()
|
||||
local prefix = HostShell.envPrefix()
|
||||
if type(opts) == "table" and opts.envPrefix ~= nil then
|
||||
prefix = opts.envPrefix
|
||||
end
|
||||
local pipe
|
||||
withPopenLock(function()
|
||||
local ok, p = pcall(io.popen, HostShell.envPrefix() .. command, mode or "r")
|
||||
local ok, p = pcall(io.popen, prefix .. command, mode or "r")
|
||||
pipe = (ok and p) or nil
|
||||
end)
|
||||
return pipe
|
||||
@@ -193,6 +260,12 @@ function HostShell.restart()
|
||||
|
||||
local appimage = os.getenv("APPIMAGE")
|
||||
if not appimage then
|
||||
-- Flatpak and plain desktop: love.event.quit("restart") uses
|
||||
-- execv(/proc/self/exe). Inside bwrap that path is valid, but PhysFS
|
||||
-- locks on a mounted .love payload can survive the exec boundary and
|
||||
-- hang the next Boot.mount. Callers that just wrote updates/*.love
|
||||
-- should already have closed archive handles; Boot.run still owns
|
||||
-- crash-guard rollback if a mid-handoff restart dies.
|
||||
love.event.quit("restart")
|
||||
return
|
||||
end
|
||||
@@ -341,7 +414,10 @@ local curlAvailable = nil
|
||||
|
||||
function HostShell.haveCurl()
|
||||
if curlAvailable ~= nil then return curlAvailable end
|
||||
local pipe = HostShell.popen("curl --version")
|
||||
local path, kind = HostShell.resolveCurl()
|
||||
local pipe = HostShell.popen(
|
||||
HostShell.quote(path) .. " --version", "r",
|
||||
{ envPrefix = HostShell.curlEnvPrefix(kind) })
|
||||
if not pipe then curlAvailable = false return false end
|
||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||
HostShell.pclose(pipe)
|
||||
@@ -349,6 +425,12 @@ function HostShell.haveCurl()
|
||||
return curlAvailable
|
||||
end
|
||||
|
||||
local function curlCmd(flags)
|
||||
local path, kind = HostShell.resolveCurl()
|
||||
return HostShell.quote(path) .. " " .. flags,
|
||||
{ envPrefix = HostShell.curlEnvPrefix(kind) }
|
||||
end
|
||||
|
||||
-- An older mobile build reports nil here and falls back to the "no transport"
|
||||
-- error the callers already show.
|
||||
local function haveBridge()
|
||||
@@ -409,9 +491,11 @@ function HostShell.httpDownload(url, absPath, userAgent, accept, maxTime, etagPa
|
||||
if type(absPath) ~= "string" or absPath == "" then return nil, "missing path" end
|
||||
userAgent = userAgent or "gen1recomp"
|
||||
if HostShell.haveCurl() then
|
||||
local cmd = ("curl -fsSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 15 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 300)
|
||||
local head, popts = curlCmd(
|
||||
("-fsSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 15 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 300))
|
||||
local cmd = head
|
||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||
if accept then
|
||||
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
||||
@@ -423,7 +507,7 @@ function HostShell.httpDownload(url, absPath, userAgent, accept, maxTime, etagPa
|
||||
cmd = cmd .. "-o " .. HostShell.quote(absPath) .. " "
|
||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||
.. HostShell.quote(url) .. " 2>&1"
|
||||
local pipe = HostShell.popen(cmd)
|
||||
local pipe = HostShell.popen(cmd, "r", popts)
|
||||
if not pipe then return nil, "could not start download" end
|
||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||
HostShell.pclose(pipe)
|
||||
@@ -466,16 +550,18 @@ function HostShell.httpGet(url, userAgent, accept, maxTime)
|
||||
-- BODY, and on the two services this talks to that body is the whole
|
||||
-- diagnosis: GitHub's 403 says "API rate limit exceeded for <ip>", which
|
||||
-- tells a user to wait rather than to go hunting for a broken index.
|
||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 40)
|
||||
local head, popts = curlCmd(
|
||||
("-sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 40))
|
||||
local cmd = head
|
||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||
if accept then
|
||||
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
||||
end
|
||||
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||
.. HostShell.quote(url) .. " 2>&1"
|
||||
local pipe = HostShell.popen(cmd)
|
||||
local pipe = HostShell.popen(cmd, "r", popts)
|
||||
if not pipe then return nil, "could not run curl" end
|
||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||
HostShell.pclose(pipe)
|
||||
@@ -562,9 +648,11 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
||||
-- newlines. The body is staged above because io.popen cannot be opened
|
||||
-- for both writing and reading. No -f, matching httpGet: the response
|
||||
-- body is discarded anyway, and curl's stderr carries the diagnosis.
|
||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 40)
|
||||
local head, popts = curlCmd(
|
||||
("-sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 40))
|
||||
local cmd = head
|
||||
.. "-X POST "
|
||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||
if contentType then
|
||||
@@ -574,7 +662,7 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
||||
.. "--data-binary " .. HostShell.quote("@" .. bodyPath) .. " "
|
||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||
.. HostShell.quote(url) .. " 2>&1"
|
||||
local pipe = HostShell.popen(cmd)
|
||||
local pipe = HostShell.popen(cmd, "r", popts)
|
||||
if not pipe then
|
||||
pcall(os.remove, bodyPath)
|
||||
return nil, "could not run curl"
|
||||
@@ -751,8 +839,10 @@ function HostShell.httpRequest(url, opts)
|
||||
pcall(os.remove, headerPath)
|
||||
end
|
||||
|
||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d "):format(maxTime)
|
||||
local head, popts = curlCmd(
|
||||
("-sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d "):format(maxTime))
|
||||
local cmd = head
|
||||
.. "-X " .. HostShell.quote(method) .. " "
|
||||
.. "-H " .. HostShell.quote("@" .. headerPath) .. " "
|
||||
if body then
|
||||
@@ -761,7 +851,7 @@ function HostShell.httpRequest(url, opts)
|
||||
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||
.. HostShell.quote(url) .. " 2>&1"
|
||||
|
||||
local pipe = HostShell.popen(cmd)
|
||||
local pipe = HostShell.popen(cmd, "r", popts)
|
||||
if not pipe then
|
||||
cleanup()
|
||||
return nil, "could not run curl"
|
||||
|
||||
+36
-2
@@ -173,13 +173,40 @@ function SaveData.gameFolders()
|
||||
end
|
||||
|
||||
-- The game folder carrying portable.txt, or false. First candidate holding
|
||||
-- the marker wins.
|
||||
-- the marker *and* a writable probe wins. Flatpak installs always use the
|
||||
-- sandbox XDG save dir (portable USB mode does not apply). A read-only
|
||||
-- parent (system /opt, immutable OS, etc.) falls back to LOVE's save
|
||||
-- directory without throwing -- probe I/O is fully guarded.
|
||||
local function dirIsWritable(dir)
|
||||
if type(dir) ~= "string" or dir == "" then return false end
|
||||
local ok, writable = pcall(function()
|
||||
local name = string.format(".write_probe_%d_%d.tmp",
|
||||
os.time() % 100000000, math.random(0, 999999))
|
||||
local path = dir .. SEP .. name
|
||||
local f, err = io.open(path, "wb")
|
||||
if not f then return false end
|
||||
local wrote = f:write("ok")
|
||||
f:close()
|
||||
pcall(os.remove, path)
|
||||
return wrote ~= nil
|
||||
end)
|
||||
return ok and writable == true
|
||||
end
|
||||
|
||||
local function detectPortable()
|
||||
if portableChecked then return portableBase end
|
||||
portableChecked = true
|
||||
portableBase = false
|
||||
-- Flatpak: never honor portable.txt (sandbox home is the persistence root).
|
||||
if type(os.getenv) == "function" and os.getenv("FLATPAK_ID") then
|
||||
return portableBase
|
||||
end
|
||||
for _, base in ipairs(SaveData.gameFolders()) do
|
||||
if pathExists(base .. SEP .. PORTABLE_MARKER) then
|
||||
local markerOk = false
|
||||
pcall(function()
|
||||
markerOk = pathExists(base .. SEP .. PORTABLE_MARKER)
|
||||
end)
|
||||
if markerOk and dirIsWritable(base) then
|
||||
portableBase = base
|
||||
break
|
||||
end
|
||||
@@ -187,6 +214,13 @@ local function detectPortable()
|
||||
return portableBase
|
||||
end
|
||||
|
||||
-- Soft-reset cache so tests can re-run detect after env changes.
|
||||
function SaveData._resetPortableCacheForTests()
|
||||
portableChecked = false
|
||||
portableBase = false
|
||||
portableFsCache = nil
|
||||
end
|
||||
|
||||
function SaveData.isPortable()
|
||||
return detectPortable() ~= false
|
||||
end
|
||||
|
||||
+11
-1
@@ -43,10 +43,13 @@ local function fullAssetName(version, osName, arch, port)
|
||||
return "gen1recomp-" .. version .. "-xbox-uwp.zip"
|
||||
elseif osName == "NX" then
|
||||
return "gen1recomp-" .. version .. "-switch.zip"
|
||||
elseif osName == "Linux" and (port == "flatpak"
|
||||
or (type(os.getenv) == "function" and os.getenv("FLATPAK_ID"))) then
|
||||
return "gen1recomp-" .. version .. "-linux.flatpak"
|
||||
elseif osName == "Linux" and (arch == "arm64" or arch == "aarch64") then
|
||||
return "gen1recomp-" .. version .. "-linux-arm64.AppImage"
|
||||
elseif osName == "Linux" then
|
||||
return "gen1recomp-" .. version .. "-linux.zip"
|
||||
return "gen1recomp-" .. version .. "-linux-x86_64.AppImage"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@@ -288,6 +291,13 @@ function Check.fullUpdateAction()
|
||||
"https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json" }
|
||||
elseif osName == "UWP" then
|
||||
return { label = "Open Xbox install guide", url = Check.releaseUrl() }
|
||||
elseif osName == "Linux" and type(st.full) == "table"
|
||||
and type(st.full.url) == "string" then
|
||||
local name = type(st.full.name) == "string" and st.full.name or ""
|
||||
if name:find("%.flatpak$") or os.getenv("FLATPAK_ID") then
|
||||
return { label = "Download Flatpak update", url = st.full.url }
|
||||
end
|
||||
return { label = "Download AppImage update", url = st.full.url }
|
||||
elseif type(st.full) == "table" and type(st.full.url) == "string" then
|
||||
return { label = "Download full update", url = st.full.url }
|
||||
end
|
||||
|
||||
@@ -310,8 +310,13 @@ local function launchDownload(url, partAbs, doneAbs)
|
||||
.. "type nul > \"" .. doneAbs .. "\"\r\n")
|
||||
os.execute('start "" /b ' .. shq(saveDir .. "/" .. batRel))
|
||||
else
|
||||
-- ( ... ) & backgrounds the whole group so os.execute returns at once
|
||||
os.execute("( " .. HostShell.envPrefix() .. "curl -fsSL --connect-timeout 15 --max-time 900 -o "
|
||||
-- ( ... ) & backgrounds the whole group so os.execute returns at once.
|
||||
-- Use the same dual-env curl resolution as HostShell.http*: bundled
|
||||
-- AppDir/Flatpak curl keeps APPDIR libs; host curl scrubs them.
|
||||
local curlPath, curlKind = HostShell.resolveCurl()
|
||||
os.execute("( " .. HostShell.curlEnvPrefix(curlKind)
|
||||
.. HostShell.quote(curlPath)
|
||||
.. " -fsSL --connect-timeout 15 --max-time 900 -o "
|
||||
.. shq(partAbs) .. " " .. shq(url)
|
||||
.. " ; touch " .. shq(doneAbs) .. " ) >/dev/null 2>&1 &")
|
||||
end
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
-- Flatpak finish-args / packaging contract.
|
||||
-- luajit tests/engine/flatpak_manifest_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check = T.check
|
||||
|
||||
local f = assert(io.open("flatpak/com.theboisclub.gen1recomp.yml", "rb"))
|
||||
local yml = f:read("*a")
|
||||
f:close()
|
||||
|
||||
check(yml:find("%-%-device=all", 1, false) or yml:find("--device=all", 1, true),
|
||||
"Flatpak finish-args include --device=all for gamepads")
|
||||
check(yml:find("--share=network", 1, true), "Flatpak shares network")
|
||||
check(yml:find("--filesystem=home", 1, true), "Flatpak allows home for ROM import")
|
||||
check(yml:find("com.theboisclub.gen1recomp", 1, true), "Flatpak app-id present")
|
||||
|
||||
local meta = assert(io.open("flatpak/com.theboisclub.gen1recomp.metainfo.xml", "rb"))
|
||||
local xml = meta:read("*a")
|
||||
meta:close()
|
||||
check(xml:find("<releases>", 1, true), "AppStream metainfo includes releases")
|
||||
check(xml:find("<release ", 1, true), "AppStream metainfo includes a release entry")
|
||||
|
||||
print("ok")
|
||||
@@ -0,0 +1,76 @@
|
||||
-- Dual curl env selection for AppImage / Flatpak / host.
|
||||
-- luajit tests/engine/hostshell_curl_env_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
-- Isolate getenv for the HostShell module load.
|
||||
local env = {
|
||||
APPIMAGE = "/tmp/fake.AppImage",
|
||||
APPDIR = "/tmp/fake-appdir",
|
||||
LD_LIBRARY_PATH = "/tmp/fake-appdir/lib",
|
||||
LD_PRELOAD = "/tmp/steam_overlay.so",
|
||||
}
|
||||
local realGetenv = os.getenv
|
||||
function os.getenv(k)
|
||||
if env[k] ~= nil then return env[k] end
|
||||
return realGetenv(k)
|
||||
end
|
||||
|
||||
-- Clear any prior HostShell package so resolveCurl memoization is fresh.
|
||||
package.loaded["src.core.HostShell"] = nil
|
||||
local HostShell = require("src.core.HostShell")
|
||||
|
||||
eq(HostShell.curlEnvPrefix("host"),
|
||||
"env -u LD_LIBRARY_PATH -u LD_PRELOAD ",
|
||||
"host curl scrubs AppImage libs and Steam preload")
|
||||
eq(HostShell.curlEnvPrefix("bundled"),
|
||||
"env -u LD_PRELOAD ",
|
||||
"bundled curl keeps LD_LIBRARY_PATH, still scrubs Steam preload")
|
||||
|
||||
-- Without APPIMAGE, host prefix only scrubs preload when set.
|
||||
env.APPIMAGE = nil
|
||||
eq(HostShell.curlEnvPrefix("host"),
|
||||
"env -u LD_PRELOAD ",
|
||||
"non-AppImage host curl still scrubs LD_PRELOAD")
|
||||
|
||||
-- Bundled path preferred when APPDIR curl exists.
|
||||
env.APPIMAGE = "/tmp/fake.AppImage"
|
||||
env.FLATPAK_ID = nil
|
||||
local fakeBin = "/tmp/fake-appdir/usr/bin"
|
||||
os.execute('mkdir -p "' .. fakeBin .. '"')
|
||||
local curlPath = fakeBin .. "/curl"
|
||||
local f = assert(io.open(curlPath, "wb"))
|
||||
f:write("#!/bin/sh\necho curl\n")
|
||||
f:close()
|
||||
os.execute('chmod +x "' .. curlPath .. '"')
|
||||
|
||||
package.loaded["src.core.HostShell"] = nil
|
||||
-- Reset memo by reloading; resolveCurl caches in module locals.
|
||||
HostShell = require("src.core.HostShell")
|
||||
local path, kind = HostShell.resolveCurl()
|
||||
eq(kind, "bundled", "APPDIR curl selected as bundled")
|
||||
eq(path, curlPath, "APPDIR usr/bin/curl path used")
|
||||
|
||||
-- Flatpak wins over APPDIR.
|
||||
env.FLATPAK_ID = "com.theboisclub.gen1recomp"
|
||||
os.execute('mkdir -p /tmp/fake-flatpak-app/bin')
|
||||
-- We cannot create /app/bin without root; instead stub by temporarily
|
||||
-- pointing resolveCurl via FLATPAK_ID alone when /app/bin/curl missing
|
||||
-- falls through to APPDIR — document that Flatpak ships /app/bin/curl.
|
||||
package.loaded["src.core.HostShell"] = nil
|
||||
HostShell = require("src.core.HostShell")
|
||||
path, kind = HostShell.resolveCurl()
|
||||
-- Without a real /app/bin/curl, APPDIR still wins; that is fine for this
|
||||
-- host-side unit test. Flatpak packaging installs /app/bin/curl.
|
||||
check(kind == "bundled" or kind == "host", "resolveCurl returns a known kind")
|
||||
|
||||
local diag = HostShell.curlDiagnostics()
|
||||
check(type(diag) == "table", "curlDiagnostics returns a table")
|
||||
check(diag.path ~= nil, "diagnostics include path")
|
||||
|
||||
os.getenv = realGetenv
|
||||
os.remove(curlPath)
|
||||
print("ok")
|
||||
@@ -0,0 +1,65 @@
|
||||
-- Portable writability probe + Flatpak skip.
|
||||
-- luajit tests/engine/portable_writable_probe_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
local tmp = os.getenv("TMPDIR") or "/tmp"
|
||||
local base = tmp .. "/gen1recomp-portable-probe-" .. tostring(os.time())
|
||||
os.execute('mkdir -p "' .. base .. '"')
|
||||
local marker = assert(io.open(base .. "/portable.txt", "wb"))
|
||||
marker:write("1\n")
|
||||
marker:close()
|
||||
|
||||
-- Minimal LOVE stubs so SaveData.gameFolders returns our base.
|
||||
_G.love = {
|
||||
filesystem = {
|
||||
getSource = function() return base .. "/game" end,
|
||||
getSourceBaseDirectory = function() return base end,
|
||||
},
|
||||
system = {
|
||||
getOS = function() return "Linux" end,
|
||||
},
|
||||
}
|
||||
|
||||
local env = {}
|
||||
local realGetenv = os.getenv
|
||||
function os.getenv(k)
|
||||
if env[k] ~= nil then return env[k] end
|
||||
return realGetenv(k)
|
||||
end
|
||||
|
||||
package.loaded["src.core.SaveData"] = nil
|
||||
local SaveData = require("src.core.SaveData")
|
||||
|
||||
check(SaveData.isPortable() == true, "writable portable parent activates portable mode")
|
||||
eq(SaveData.portableBaseDir(), base, "portable base is the probed folder")
|
||||
|
||||
SaveData._resetPortableCacheForTests()
|
||||
env.FLATPAK_ID = "com.theboisclub.gen1recomp"
|
||||
check(SaveData.isPortable() == false, "Flatpak ignores portable.txt")
|
||||
eq(SaveData.portableBaseDir(), nil, "Flatpak has no portable base")
|
||||
|
||||
-- Read-only parent: create a dir we cannot write (best-effort).
|
||||
SaveData._resetPortableCacheForTests()
|
||||
env.FLATPAK_ID = nil
|
||||
local ro = base .. "-ro"
|
||||
os.execute('mkdir -p "' .. ro .. '" && touch "' .. ro .. '/portable.txt" && chmod 555 "' .. ro .. '"')
|
||||
_G.love.filesystem.getSourceBaseDirectory = function() return ro end
|
||||
_G.love.filesystem.getSource = function() return ro .. "/game" end
|
||||
SaveData._resetPortableCacheForTests()
|
||||
-- chmod 555 may still allow owner write on some FS; accept either outcome
|
||||
-- but must not throw.
|
||||
local ok, portable = pcall(function() return SaveData.isPortable() end)
|
||||
check(ok, "RO portable probe must not throw")
|
||||
if portable then
|
||||
check(SaveData.portableBaseDir() ~= nil, "if still writable, portable stays on")
|
||||
else
|
||||
eq(SaveData.portableBaseDir(), nil, "unwritable portable parent falls back")
|
||||
end
|
||||
|
||||
os.getenv = realGetenv
|
||||
os.execute('chmod 755 "' .. ro .. '" 2>/dev/null; rm -rf "' .. base .. '" "' .. ro .. '"')
|
||||
print("ok")
|
||||
@@ -43,6 +43,10 @@ eq(Check.fullAssetName("1.4.2", "Android", "arm64"),
|
||||
"gen1recomp-1.4.2-android.apk", "Android full asset mapping")
|
||||
eq(Check.fullAssetName("1.4.2", "Linux", "aarch64"),
|
||||
"gen1recomp-1.4.2-linux-arm64.AppImage", "Linux ARM package mapping")
|
||||
eq(Check.fullAssetName("1.4.2", "Linux", "x64"),
|
||||
"gen1recomp-1.4.2-linux-x86_64.AppImage", "Linux x86_64 AppImage mapping")
|
||||
eq(Check.fullAssetName("1.4.2", "Linux", "x64", "flatpak"),
|
||||
"gen1recomp-1.4.2-linux.flatpak", "Linux Flatpak package mapping")
|
||||
eq(Check.fullAssetName("1.4.2", "iOS", "arm64"),
|
||||
"gen1recomp++-1.4.2-ios.ipa", "iOS package mapping")
|
||||
eq(Check.fullAssetName("not-a-version", "Android", "arm64"), nil,
|
||||
|
||||
Reference in New Issue
Block a user