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"
|
mkdir -p "$outdir"
|
||||||
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
|
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
|
||||||
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.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
|
# arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
|
||||||
# its own runner because LÖVE publishes no aarch64 binary and the
|
# its own runner because LÖVE publishes no aarch64 binary and the
|
||||||
# AppImage has to be compiled natively; ships as a runnable
|
# 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"
|
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; }
|
[ -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"
|
cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||||
chmod +x "$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)"
|
apk="$(find dist/android/release -name '*.apk' | head -1)"
|
||||||
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/release"; exit 1; }
|
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/release"; exit 1; }
|
||||||
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
||||||
@@ -656,8 +664,9 @@ jobs:
|
|||||||
release_files=(
|
release_files=(
|
||||||
"dist/release/gen1recomp-${v}-macos.zip"
|
"dist/release/gen1recomp-${v}-macos.zip"
|
||||||
"dist/release/gen1recomp-${v}-windows.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-arm64.AppImage"
|
||||||
|
"dist/release/gen1recomp-${v}-linux.flatpak"
|
||||||
"dist/release/gen1recomp-${v}-android.apk"
|
"dist/release/gen1recomp-${v}-android.apk"
|
||||||
"dist/release/gen1recomp++-${v}-ios.ipa"
|
"dist/release/gen1recomp++-${v}-ios.ipa"
|
||||||
"dist/release/gen1recomp-${v}-switch.zip"
|
"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 |
|
| `--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
|
Releases ship raw AppImages (no zip wrapper) plus an optional Flatpak bundle:
|
||||||
`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:
|
- `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
|
```sh
|
||||||
chmod +x gen1recomp-*-linux-arm64.AppImage
|
chmod +x gen1recomp-*-linux-arm64.AppImage
|
||||||
./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
|
engine — and SDL2, OpenAL and the codecs — from source inside a Debian
|
||||||
bullseye arm64 container. It needs only glibc 2.29+, libstdc++, freetype and
|
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
|
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
|
# Linux arm64 (aarch64) AppImage
|
||||||
|
|
||||||
Releases ship `gen1recomp-<version>-linux-arm64.AppImage` alongside the
|
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
|
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
|
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
|
> The Anbernic RG34XXSP has its own PortMaster-style pack
|
||||||
> (`gen1recomp-*-rg34xxsp-stockos64-mod.zip`, see
|
> (`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
|
## Release assets
|
||||||
|
|
||||||
Each tagged release `vX.Y.Z` carries the existing per-platform archives
|
Each tagged release `vX.Y.Z` carries the existing per-platform archives
|
||||||
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`, `-linux.zip`,
|
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`,
|
||||||
`-linux-arm64.AppImage`, `-android.apk`, `-ios.ipa`, `-switch.zip`, Xbox and
|
`-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:
|
PortMaster archives) plus two assets the updater itself consumes:
|
||||||
|
|
||||||
- `gen1recomp-X.Y.Z.love` - the payload, matched by the exact pattern
|
- `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
|
# Output: dist/mac/gen1recomp-macos.zip
|
||||||
# dist/win/gen1recomp-win64.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
|
# dist/android/debug/*.apk (full gradle output stays under
|
||||||
# mobile/android/app/build/outputs/apk/embedNoRecord/)
|
# mobile/android/app/build/outputs/apk/embedNoRecord/)
|
||||||
# dist/ios/<Config>-<sdk>/gen1recomp++.app (full xcodebuild output stays
|
# dist/ios/<Config>-<sdk>/gen1recomp++.app (full xcodebuild output stays
|
||||||
@@ -457,15 +457,31 @@ EOF
|
|||||||
# file-manager thumbnailers show for the file itself.
|
# file-manager thumbnailers show for the file itself.
|
||||||
[ -f "$ICON_SRC" ] || fail "missing icon source: $ICON_SRC"
|
[ -f "$ICON_SRC" ] || fail "missing icon source: $ICON_SRC"
|
||||||
rm -f "$appdir/love.svg" "$appdir/love.png" "$appdir/.DirIcon"
|
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"
|
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" \
|
grep -q '^FUSE_PATH="\$APPDIR/game.love"$' "$appdir/AppRun" \
|
||||||
|| fail "failed to enable FUSE_PATH in AppRun (upstream AppRun changed?)"
|
|| 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\
|
local wayland_hook='if [ -n "$WAYLAND_DISPLAY" ] && [ -z "$SDL_VIDEODRIVER" ]; then export SDL_VIDEODRIVER=x11; fi\
|
||||||
exec "$APPDIR/bin/love"|' "$appdir/AppRun"
|
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
|
# Match the upstream image's compression (gzip, 128K blocks) so the
|
||||||
# bundled runtime can read it.
|
# bundled runtime can read it.
|
||||||
@@ -474,16 +490,20 @@ exec "$APPDIR/bin/love"|' "$appdir/AppRun"
|
|||||||
mksquashfs "$appdir" "$sfs_out" \
|
mksquashfs "$appdir" "$sfs_out" \
|
||||||
-comp gzip -b 131072 -noappend -all-root -no-xattrs -quiet >/dev/null
|
-comp gzip -b 131072 -noappend -all-root -no-xattrs -quiet >/dev/null
|
||||||
|
|
||||||
local out_bin="$WORK/$APP_NAME-x86_64.AppImage"
|
mkdir -p "$DIST/linux"
|
||||||
rm -f "$out_bin"
|
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"
|
head -c "$sfs_offset" "$love_appimage" > "$out_bin"
|
||||||
cat "$sfs_out" >> "$out_bin"
|
cat "$sfs_out" >> "$out_bin"
|
||||||
chmod +x "$out_bin"
|
chmod +x "$out_bin"
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
local zip_out="$DIST/linux/$APP_NAME-linux.zip"
|
printf '%s %s\n' "$(sha256sum "$out_bin" | awk '{print $1}')" "$(basename "$out_bin")" \
|
||||||
rm -f "$zip_out"
|
> "$out_bin.sha256"
|
||||||
(cd "$WORK" && zip -q -9 -j "$zip_out" "$(basename "$out_bin")")
|
else
|
||||||
say "Linux build: $zip_out"
|
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
|
# --------------------------------------------------------------- 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
|
-- 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
|
-- 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
|
-- system ones. Host tools (curl, zenity) need that unset. A *bundled* AppDir
|
||||||
-- their proper libraries. Only needed when running in an AppImage.
|
-- curl must keep APPDIR on LD_LIBRARY_PATH so it resolves the bundled
|
||||||
-- LD_PRELOAD is Steam's overlay (#1470): its 32-bit half cannot load into a
|
-- libssl/libcrypto -- scrubbing it forces host OpenSSL and segfaults across
|
||||||
-- 64-bit child, so ld.so prints an error into that child's output.
|
-- distros. LD_PRELOAD is always scrubbed: Steam's overlay (#1470) cannot load
|
||||||
|
-- into a 64-bit child.
|
||||||
function HostShell.envPrefix()
|
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 = ""
|
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 os.getenv("LD_PRELOAD") then unset = unset .. "-u LD_PRELOAD " end
|
||||||
if unset == "" then return "" end
|
if unset == "" then return "" end
|
||||||
return "env " .. unset
|
return "env " .. unset
|
||||||
end
|
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
|
-- 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
|
-- 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
|
-- 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
|
if not okAtomic then fn() end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Wraps io.popen with the AppImage env fix applied and lua errors swallowed
|
-- Wraps io.popen with the AppImage env fix applied and lua errors swallowed.
|
||||||
function HostShell.popen(command, mode)
|
-- 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()
|
HostShell.releasePointerGrab()
|
||||||
|
local prefix = HostShell.envPrefix()
|
||||||
|
if type(opts) == "table" and opts.envPrefix ~= nil then
|
||||||
|
prefix = opts.envPrefix
|
||||||
|
end
|
||||||
local pipe
|
local pipe
|
||||||
withPopenLock(function()
|
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
|
pipe = (ok and p) or nil
|
||||||
end)
|
end)
|
||||||
return pipe
|
return pipe
|
||||||
@@ -193,6 +260,12 @@ function HostShell.restart()
|
|||||||
|
|
||||||
local appimage = os.getenv("APPIMAGE")
|
local appimage = os.getenv("APPIMAGE")
|
||||||
if not appimage then
|
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")
|
love.event.quit("restart")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -341,7 +414,10 @@ local curlAvailable = nil
|
|||||||
|
|
||||||
function HostShell.haveCurl()
|
function HostShell.haveCurl()
|
||||||
if curlAvailable ~= nil then return curlAvailable end
|
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
|
if not pipe then curlAvailable = false return false end
|
||||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||||
HostShell.pclose(pipe)
|
HostShell.pclose(pipe)
|
||||||
@@ -349,6 +425,12 @@ function HostShell.haveCurl()
|
|||||||
return curlAvailable
|
return curlAvailable
|
||||||
end
|
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"
|
-- An older mobile build reports nil here and falls back to the "no transport"
|
||||||
-- error the callers already show.
|
-- error the callers already show.
|
||||||
local function haveBridge()
|
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
|
if type(absPath) ~= "string" or absPath == "" then return nil, "missing path" end
|
||||||
userAgent = userAgent or "gen1recomp"
|
userAgent = userAgent or "gen1recomp"
|
||||||
if HostShell.haveCurl() then
|
if HostShell.haveCurl() then
|
||||||
local cmd = ("curl -fsSL --proto =http,https --proto-redir =http,https "
|
local head, popts = curlCmd(
|
||||||
.. "--connect-timeout 15 --max-time %d ")
|
("-fsSL --proto =http,https --proto-redir =http,https "
|
||||||
:format(tonumber(maxTime) or 300)
|
.. "--connect-timeout 15 --max-time %d ")
|
||||||
|
:format(tonumber(maxTime) or 300))
|
||||||
|
local cmd = head
|
||||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||||
if accept then
|
if accept then
|
||||||
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
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) .. " "
|
cmd = cmd .. "-o " .. HostShell.quote(absPath) .. " "
|
||||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||||
.. HostShell.quote(url) .. " 2>&1"
|
.. 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
|
if not pipe then return nil, "could not start download" end
|
||||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||||
HostShell.pclose(pipe)
|
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
|
-- 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
|
-- 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.
|
-- 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 "
|
local head, popts = curlCmd(
|
||||||
.. "--connect-timeout 10 --max-time %d ")
|
("-sSL --proto =http,https --proto-redir =http,https "
|
||||||
:format(tonumber(maxTime) or 40)
|
.. "--connect-timeout 10 --max-time %d ")
|
||||||
|
:format(tonumber(maxTime) or 40))
|
||||||
|
local cmd = head
|
||||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||||
if accept then
|
if accept then
|
||||||
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
||||||
end
|
end
|
||||||
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||||
.. HostShell.quote(url) .. " 2>&1"
|
.. 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
|
if not pipe then return nil, "could not run curl" end
|
||||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||||
HostShell.pclose(pipe)
|
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
|
-- newlines. The body is staged above because io.popen cannot be opened
|
||||||
-- for both writing and reading. No -f, matching httpGet: the response
|
-- for both writing and reading. No -f, matching httpGet: the response
|
||||||
-- body is discarded anyway, and curl's stderr carries the diagnosis.
|
-- body is discarded anyway, and curl's stderr carries the diagnosis.
|
||||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
local head, popts = curlCmd(
|
||||||
.. "--connect-timeout 10 --max-time %d ")
|
("-sSL --proto =http,https --proto-redir =http,https "
|
||||||
:format(tonumber(maxTime) or 40)
|
.. "--connect-timeout 10 --max-time %d ")
|
||||||
|
:format(tonumber(maxTime) or 40))
|
||||||
|
local cmd = head
|
||||||
.. "-X POST "
|
.. "-X POST "
|
||||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||||
if contentType then
|
if contentType then
|
||||||
@@ -574,7 +662,7 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
|||||||
.. "--data-binary " .. HostShell.quote("@" .. bodyPath) .. " "
|
.. "--data-binary " .. HostShell.quote("@" .. bodyPath) .. " "
|
||||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||||
.. HostShell.quote(url) .. " 2>&1"
|
.. HostShell.quote(url) .. " 2>&1"
|
||||||
local pipe = HostShell.popen(cmd)
|
local pipe = HostShell.popen(cmd, "r", popts)
|
||||||
if not pipe then
|
if not pipe then
|
||||||
pcall(os.remove, bodyPath)
|
pcall(os.remove, bodyPath)
|
||||||
return nil, "could not run curl"
|
return nil, "could not run curl"
|
||||||
@@ -751,8 +839,10 @@ function HostShell.httpRequest(url, opts)
|
|||||||
pcall(os.remove, headerPath)
|
pcall(os.remove, headerPath)
|
||||||
end
|
end
|
||||||
|
|
||||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
local head, popts = curlCmd(
|
||||||
.. "--connect-timeout 10 --max-time %d "):format(maxTime)
|
("-sSL --proto =http,https --proto-redir =http,https "
|
||||||
|
.. "--connect-timeout 10 --max-time %d "):format(maxTime))
|
||||||
|
local cmd = head
|
||||||
.. "-X " .. HostShell.quote(method) .. " "
|
.. "-X " .. HostShell.quote(method) .. " "
|
||||||
.. "-H " .. HostShell.quote("@" .. headerPath) .. " "
|
.. "-H " .. HostShell.quote("@" .. headerPath) .. " "
|
||||||
if body then
|
if body then
|
||||||
@@ -761,7 +851,7 @@ function HostShell.httpRequest(url, opts)
|
|||||||
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||||
.. HostShell.quote(url) .. " 2>&1"
|
.. HostShell.quote(url) .. " 2>&1"
|
||||||
|
|
||||||
local pipe = HostShell.popen(cmd)
|
local pipe = HostShell.popen(cmd, "r", popts)
|
||||||
if not pipe then
|
if not pipe then
|
||||||
cleanup()
|
cleanup()
|
||||||
return nil, "could not run curl"
|
return nil, "could not run curl"
|
||||||
|
|||||||
+36
-2
@@ -173,13 +173,40 @@ function SaveData.gameFolders()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- The game folder carrying portable.txt, or false. First candidate holding
|
-- 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()
|
local function detectPortable()
|
||||||
if portableChecked then return portableBase end
|
if portableChecked then return portableBase end
|
||||||
portableChecked = true
|
portableChecked = true
|
||||||
portableBase = false
|
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
|
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
|
portableBase = base
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -187,6 +214,13 @@ local function detectPortable()
|
|||||||
return portableBase
|
return portableBase
|
||||||
end
|
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()
|
function SaveData.isPortable()
|
||||||
return detectPortable() ~= false
|
return detectPortable() ~= false
|
||||||
end
|
end
|
||||||
|
|||||||
+11
-1
@@ -43,10 +43,13 @@ local function fullAssetName(version, osName, arch, port)
|
|||||||
return "gen1recomp-" .. version .. "-xbox-uwp.zip"
|
return "gen1recomp-" .. version .. "-xbox-uwp.zip"
|
||||||
elseif osName == "NX" then
|
elseif osName == "NX" then
|
||||||
return "gen1recomp-" .. version .. "-switch.zip"
|
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
|
elseif osName == "Linux" and (arch == "arm64" or arch == "aarch64") then
|
||||||
return "gen1recomp-" .. version .. "-linux-arm64.AppImage"
|
return "gen1recomp-" .. version .. "-linux-arm64.AppImage"
|
||||||
elseif osName == "Linux" then
|
elseif osName == "Linux" then
|
||||||
return "gen1recomp-" .. version .. "-linux.zip"
|
return "gen1recomp-" .. version .. "-linux-x86_64.AppImage"
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -288,6 +291,13 @@ function Check.fullUpdateAction()
|
|||||||
"https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json" }
|
"https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json" }
|
||||||
elseif osName == "UWP" then
|
elseif osName == "UWP" then
|
||||||
return { label = "Open Xbox install guide", url = Check.releaseUrl() }
|
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
|
elseif type(st.full) == "table" and type(st.full.url) == "string" then
|
||||||
return { label = "Download full update", url = st.full.url }
|
return { label = "Download full update", url = st.full.url }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -310,8 +310,13 @@ local function launchDownload(url, partAbs, doneAbs)
|
|||||||
.. "type nul > \"" .. doneAbs .. "\"\r\n")
|
.. "type nul > \"" .. doneAbs .. "\"\r\n")
|
||||||
os.execute('start "" /b ' .. shq(saveDir .. "/" .. batRel))
|
os.execute('start "" /b ' .. shq(saveDir .. "/" .. batRel))
|
||||||
else
|
else
|
||||||
-- ( ... ) & backgrounds the whole group so os.execute returns at once
|
-- ( ... ) & backgrounds the whole group so os.execute returns at once.
|
||||||
os.execute("( " .. HostShell.envPrefix() .. "curl -fsSL --connect-timeout 15 --max-time 900 -o "
|
-- 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)
|
.. shq(partAbs) .. " " .. shq(url)
|
||||||
.. " ; touch " .. shq(doneAbs) .. " ) >/dev/null 2>&1 &")
|
.. " ; touch " .. shq(doneAbs) .. " ) >/dev/null 2>&1 &")
|
||||||
end
|
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")
|
"gen1recomp-1.4.2-android.apk", "Android full asset mapping")
|
||||||
eq(Check.fullAssetName("1.4.2", "Linux", "aarch64"),
|
eq(Check.fullAssetName("1.4.2", "Linux", "aarch64"),
|
||||||
"gen1recomp-1.4.2-linux-arm64.AppImage", "Linux ARM package mapping")
|
"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"),
|
eq(Check.fullAssetName("1.4.2", "iOS", "arm64"),
|
||||||
"gen1recomp++-1.4.2-ios.ipa", "iOS package mapping")
|
"gen1recomp++-1.4.2-ios.ipa", "iOS package mapping")
|
||||||
eq(Check.fullAssetName("not-a-version", "Android", "arm64"), nil,
|
eq(Check.fullAssetName("not-a-version", "Android", "arm64"), nil,
|
||||||
|
|||||||
Reference in New Issue
Block a user