mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 21:35:42 -04:00
Fix self-update on the PortMaster SBC build
The SBC and RG34XXSP ports hand LÖVE a source *directory*
(`love <dir>`), so `love.filesystem.isFused()` is false there and
`Boot.run` bailed on its first line. The launcher still offered
"Update vX.Y.Z" and "Restart to update", so an update downloaded,
verified, and then was silently ignored forever.
- Boot.canUpdateInPlace() replaces the fused-only gate: a packaged
build updates whether it is fused (AppImage, Flatpak game.love) or
unpacked, and only a dev / source checkout (engine "0.0.0-dev") is
excluded. Fails closed when the host cannot be established.
Prelaunch.updateAllowed now delegates to it, so the boot gate and the
`--update` gate cannot disagree.
- Check.hostPort() reads the release-target marker, with
POKEPORT_HANDHELD as a legacy fallback (the SBC launcher has always
exported it, so packs predating this change still identify
themselves). The marker was read from the environment but never
exported, so a full-package fallback on a handheld resolved to a
desktop AppImage that cannot run there.
- A handheld now fetches its own package ("Download port update",
reusing the worker's cross-platform download_full) instead of
offering a URL it has no browser to open, and reports "Update package
ready" once it is in the save directory for a manual re-extract.
- Launchers export POKEPORT_PORTMASTER / POKEPORT_RG34XXSP.
An in-place update only ever mounts a payload over the running source,
so relaxing the gate is less invasive than the fused path it joins.
A runtime bump still needs a full package, which the minShell gate
already reports as needs_full.
Tests: tests/engine/update_boot_host_gate.lua (new, 10 checks) pins the
gate; update_check_tests.lua pins both port asset names and hostPort().
scripts/test.sh --quick: all tiers passed; scripts/lint.sh: 0 errors.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -341,6 +341,9 @@ apply_cpu_governors() {
|
||||
export POKEPORT_HANDHELD=1
|
||||
export HANDHELD=1
|
||||
export PORTMASTER=1
|
||||
# Release-target marker for the self-updater: a full-package fallback here must
|
||||
# offer gen1recomp-<v>-sbc-portmaster.zip, never a desktop AppImage.
|
||||
export POKEPORT_PORTMASTER=1
|
||||
export NINTENDO_LAYOUT=1
|
||||
export XDG_DATA_HOME="$CONFDIR"
|
||||
export XDG_CONFIG_HOME="$CONFDIR"
|
||||
|
||||
@@ -240,6 +240,9 @@ cd "$GAMEDIR" || exit 1
|
||||
|
||||
export XDG_DATA_HOME="$CONFDIR"
|
||||
export XDG_CONFIG_HOME="$CONFDIR"
|
||||
# Release-target marker for the self-updater: a full-package fallback here must
|
||||
# offer gen1recomp-<v>-rg34xxsp-stockos64-mod.zip, never a desktop package.
|
||||
export POKEPORT_RG34XXSP=1
|
||||
export LD_LIBRARY_PATH="$GAMEDIR/libs.aarch64:${LD_LIBRARY_PATH:-}"
|
||||
export SDL_GAMECONTROLLERCONFIG="${sdl_controllerconfig:-}"
|
||||
# Mali / H700: prefer GLES where available
|
||||
|
||||
@@ -40,6 +40,32 @@ Only the canonical 1 MiB US carts import:
|
||||
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
||||
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
||||
|
||||
## Updating
|
||||
|
||||
The port updates itself from the launcher. When a newer release exists the
|
||||
footer chip reads **Update vX.Y.Z**: press it to download the new version, then
|
||||
press **Restart to update** to relaunch and start using it. Nothing inside the
|
||||
port folder is rewritten, so an update that fails or is interrupted still boots
|
||||
the version you already had.
|
||||
|
||||
An update replaces the game's code and data. It cannot replace the LÖVE runtime
|
||||
the port ships, so a release that needs a newer runtime is applied differently:
|
||||
the chip then reads **Download port update** and saves
|
||||
`gen1recomp-<version>-rg34xxsp-stockos64-mod.zip` into
|
||||
|
||||
```
|
||||
Roms/PORTS/gen1recomp/conf/love/pokemon-love2d/updates/
|
||||
```
|
||||
|
||||
That case is finished on a computer — unzip the file and copy the
|
||||
`Gen1recomp.sh` and `gen1recomp/` it contains over the copies on the SD card,
|
||||
exactly as in [Install](#install) above. Your saves sit beside the game
|
||||
(`portable.txt`) and are untouched by either kind of update.
|
||||
|
||||
The launcher exports `POKEPORT_RG34XXSP=1`, which is how the updater knows this
|
||||
is the RG34XXSP port and offers the matching port package rather than a desktop
|
||||
AppImage.
|
||||
|
||||
## Launcher controls
|
||||
|
||||
| Input | Action |
|
||||
|
||||
@@ -21,7 +21,7 @@ mismatches on immutable desktops.
|
||||
| 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 |
|
||||
| "Download AppImage update" | Shell/`minShell` gate needs a full native package | Download the new `.AppImage`, `chmod +x`, replace the old file. Handheld ports show **Download port update** and fetch their own package instead — see [linux-arm-sbc.md](linux-arm-sbc.md) |
|
||||
| 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)
|
||||
@@ -45,6 +45,13 @@ 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.
|
||||
|
||||
PortMaster-style ports (`linux-arm-sbc`, `rg34xxsp`) are unpacked Linux builds
|
||||
too, so they apply the same `.love` payloads in place, but they have no browser
|
||||
in which to open a download URL. On a handheld the full-package prompt fetches
|
||||
the port's own zip into the save directory for the player to re-extract; the
|
||||
port is identified by `POKEPORT_PORTMASTER` / `POKEPORT_RG34XXSP`. See
|
||||
[updater.md](updater.md) for the whole contract.
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
|
||||
@@ -17,6 +17,32 @@ Canonical US cart SHA-1 values:
|
||||
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
||||
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
||||
|
||||
## Updating
|
||||
|
||||
The port updates itself from the launcher. When a newer release exists the
|
||||
footer chip reads **Update vX.Y.Z**: press it to download the new version, then
|
||||
press **Restart to update** to relaunch and start using it. Nothing inside the
|
||||
port folder is rewritten, so an update that fails or is interrupted still boots
|
||||
the version you already had.
|
||||
|
||||
An update replaces the game's code and data. It cannot replace the LÖVE runtime
|
||||
the port ships, so a release that needs a newer runtime is applied differently:
|
||||
the chip then reads **Download port update** and saves
|
||||
`gen1recomp-<version>-sbc-portmaster.zip` into
|
||||
|
||||
```
|
||||
gen1recomp-sbc/conf/love/pokemon-love2d/updates/
|
||||
```
|
||||
|
||||
That case is finished on a computer — unzip the file and copy the
|
||||
`gen1recomp-sbc.sh` and `gen1recomp-sbc/` it contains over the copies on the SD
|
||||
card, exactly as in [Install](#install) above. Your saves sit beside the game
|
||||
(`portable.txt`) and are untouched by either kind of update.
|
||||
|
||||
The launcher exports `POKEPORT_PORTMASTER=1`, which is how the updater knows
|
||||
this is the PortMaster port and offers the port package rather than a desktop
|
||||
AppImage.
|
||||
|
||||
## Controls
|
||||
|
||||
| Input | Action |
|
||||
|
||||
+43
-18
@@ -1,15 +1,24 @@
|
||||
# Updater
|
||||
|
||||
A fused build (`love.filesystem.isFused()` true) ships a bundled `game.love`
|
||||
baked into the executable, but that bundled copy is only ever the *fallback*.
|
||||
On every launch, before anything else runs, `Boot.run` (`src/update/Boot.lua`)
|
||||
looks in the save directory's `updates/` folder for a downloaded
|
||||
A packaged build carries a fixed engine plus the game source it starts from,
|
||||
and that source is only ever the *fallback*. It ships in one of two shapes:
|
||||
**fused** (a `game.love` baked into the executable, so LÖVE reports
|
||||
`love.filesystem.isFused()` true) or **unpacked** (a source directory the
|
||||
launcher hands to the runtime, `love <dir>`, which is what every
|
||||
PortMaster-style port does and which reports `isFused()` false). On every
|
||||
launch, before anything else runs, `Boot.run` (`src/update/Boot.lua`) looks in
|
||||
the save directory's `updates/` folder for a downloaded
|
||||
`gen1recomp-X.Y.Z.love` payload that is both strictly newer than the bundled
|
||||
engine version and runnable on this shell. If one qualifies, it is mounted
|
||||
over `/` (so its files win over the fused source for every subsequent
|
||||
over `/` (so its files win over the bundled source for every subsequent
|
||||
`require`) and chainloaded in place: the payload's `main.lua` and `love.load`
|
||||
run as if they had shipped in the executable. A dev/source checkout is never
|
||||
fused, so `Boot.run` no-ops there and the working tree always runs itself.
|
||||
run as if they had shipped in the executable. Nothing on disk is rewritten
|
||||
either way -- an update is a mount layered over the source -- so the handoff is
|
||||
sound for a fused archive and for an unpacked directory alike.
|
||||
`Boot.canUpdateInPlace` decides whether a build may update at all: a fused
|
||||
build always may, an unpacked build may when its `engine` is a released
|
||||
`X.Y.Z`, and a dev/source checkout never may (its engine is the
|
||||
`"0.0.0-dev"` placeholder, so the working tree always runs itself).
|
||||
|
||||
The pieces are deliberately layered so the risky part is small. `Boot.select`
|
||||
is a pure function (no `love.*` calls) that, given probed candidates and the
|
||||
@@ -86,9 +95,10 @@ every launch, including offline launches.
|
||||
Under the save directory (identity `pokemon-love2d`):
|
||||
|
||||
```
|
||||
updates/gen1recomp-<X.Y.Z>.love downloaded payload(s)
|
||||
updates/pending.txt crash-guard marker
|
||||
updates/full-update.json persistent native-package requirement
|
||||
updates/gen1recomp-<X.Y.Z>.love downloaded payload(s)
|
||||
updates/gen1recomp-<X.Y.Z>-sbc-portmaster.zip downloaded port package (ports)
|
||||
updates/pending.txt crash-guard marker
|
||||
updates/full-update.json persistent native-package requirement
|
||||
```
|
||||
|
||||
`pending.txt` holds the filename of the payload currently being chainloaded.
|
||||
@@ -101,7 +111,7 @@ bundled game, in that case.
|
||||
|
||||
## Update flow
|
||||
|
||||
1. **Boot** (every launch, fused builds only): crash-guard check, enumerate
|
||||
1. **Boot** (every launch, packaged builds only): crash-guard check, enumerate
|
||||
and probe every `updates/*.love`, pick the highest engine that is
|
||||
strictly newer than the bundled one and whose `minShell` this shell
|
||||
satisfies, delete stale payloads, chainload the winner (or run the
|
||||
@@ -138,9 +148,16 @@ bundled game, in that case.
|
||||
version-code, and signing-certificate compatibility. A legacy APK without
|
||||
the installer bridge links its full package for one manual bootstrap
|
||||
update, including when its downloaded payload already reports the latest
|
||||
engine version. iOS links the sideload repository for a re-sideload; Xbox,
|
||||
desktop, and PortMaster builds link their correctly named full package.
|
||||
Switch keeps its native OTA flow.
|
||||
engine version. iOS links the sideload repository for a re-sideload; Xbox
|
||||
and desktop builds link their correctly named full package. A
|
||||
PortMaster-style port has no browser to open a download in, so it fetches
|
||||
its own package (`-sbc-portmaster.zip`, `-rg34xxsp-stockos64-mod.zip`) into
|
||||
the save directory through the same verified download, and the player
|
||||
re-extracts it over the port folder — see
|
||||
[linux-arm-sbc.md](linux-arm-sbc.md). Switch keeps its native OTA flow.
|
||||
Which package a port offers comes from the launcher's environment
|
||||
(`POKEPORT_PORTMASTER`, `POKEPORT_RG34XXSP`, with `POKEPORT_HANDHELD`
|
||||
honoured as a legacy fallback), which is what `Check.hostPort()` reads.
|
||||
|
||||
## Known limitations
|
||||
|
||||
@@ -171,10 +188,18 @@ bundled game, in that case.
|
||||
release job must use the original long-lived Android signing key; a new key
|
||||
causes Android to reject an in-place update and requires a one-time manual
|
||||
reinstall. See [mobile/ANDROID.md](../mobile/ANDROID.md).
|
||||
- **Dev/source runs never self-update.** `Boot.run` returns immediately when
|
||||
`love.filesystem.isFused()` is false, and a working tree's `engine` is the
|
||||
`"0.0.0-dev"` placeholder that always reports up to date, so a source
|
||||
checkout is always "the game" itself; updating it means pulling the repo.
|
||||
- **Dev/source runs never self-update.** `Boot.canUpdateInPlace` is false for a
|
||||
working tree -- its `engine` is the `"0.0.0-dev"` placeholder, which also
|
||||
always reports up to date -- so `Boot.run` returns immediately and
|
||||
`Prelaunch`'s `--update` shortcut refuses to act. A source checkout is always
|
||||
"the game" itself; updating it means pulling the repo.
|
||||
- **An unpacked port updates the Lua, not the runtime.** The PortMaster SBC and
|
||||
RG34XXSP packages ship a fixed `love.aarch64` runtime beside a `lovegame/`
|
||||
source directory. A payload layers new Lua and data over that source, so a
|
||||
LÖVE version bump, a new required system binary, or a `love.run` change still
|
||||
needs a full port package: the `minShell` gate reports `needs_full`, the chip
|
||||
fetches the port package into the save directory, and the player re-extracts
|
||||
it over the port folder.
|
||||
- **Nintendo Switch does not use this LÖVE self-updater.** On NX,
|
||||
`Platform.networkValidated()` is `false`, so `Boot.run` / `Check` never
|
||||
download `.love` payloads. In-console OTA uses the **native OTA launcher**
|
||||
|
||||
+10
-2
@@ -29,8 +29,16 @@ local function updateAllowed(opts, fs)
|
||||
and not Platform.networkValidated() then
|
||||
return false
|
||||
end
|
||||
if not (fs and fs.isFused and fs.isFused()) then return false end
|
||||
return true
|
||||
-- Fused and unpacked packaged builds both update; a dev checkout does not.
|
||||
-- Boot owns the second half of that decision (the isDev check above is the
|
||||
-- first) so the boot gate and this one cannot disagree.
|
||||
if fs and fs.isFused and fs.isFused() then return true end
|
||||
local okb, Boot = pcall(require, "src.update.Boot")
|
||||
if okb and type(Boot) == "table"
|
||||
and type(Boot.canUpdateInPlace) == "function" then
|
||||
return Boot.canUpdateInPlace() and true or false
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function syncSupported()
|
||||
|
||||
+36
-8
@@ -1,11 +1,21 @@
|
||||
-- The boot shell: the heart of the self-updater. A fused build, before it
|
||||
-- runs the game bundled inside it, looks in its save directory for a newer
|
||||
-- payload (a downloaded gen1recomp-X.Y.Z.love), and if one is present and
|
||||
-- runnable, mounts it over the bundled source and chainloads it -- so the
|
||||
-- binary shipped once can keep updating the Lua it runs without a reinstall.
|
||||
-- The boot shell: the heart of the self-updater. A build that carries a fixed
|
||||
-- engine, before it runs the game source it ships, looks in its save directory
|
||||
-- for a newer payload (a downloaded gen1recomp-X.Y.Z.love), and if one is
|
||||
-- present and runnable, mounts it over the bundled source and chainloads it --
|
||||
-- so the binary shipped once can keep updating the Lua it runs without a
|
||||
-- reinstall.
|
||||
--
|
||||
-- Only a fused build self-updates. A dev / source checkout IS the game, so
|
||||
-- Boot.run is a no-op there.
|
||||
-- Two shapes of packaged build carry a fixed engine, and both self-update:
|
||||
-- * fused -- the game is an archive inside the executable, so LÖVE reports
|
||||
-- isFused() true (an AppImage, or the Flatpak's game.love file).
|
||||
-- * unpacked -- the executable is handed a *directory* of game source
|
||||
-- (`love <dir>`, which every PortMaster-style port does). LÖVE reports
|
||||
-- isFused() false there, but the handoff below is still sound: the payload
|
||||
-- is prepend-mounted over the running source and the source on disk is
|
||||
-- never rewritten.
|
||||
-- What must not self-update is a dev / source checkout: it IS the game, its
|
||||
-- Version.engine is the "0.0.0-dev" placeholder, and its next launch already
|
||||
-- runs whatever is on disk. Boot.run is a no-op there. See docs/updater.md.
|
||||
--
|
||||
-- Three pieces, deliberately layered so the risky part is small and the
|
||||
-- decision part is testable:
|
||||
@@ -275,6 +285,24 @@ local function runInner(args)
|
||||
return chainload(chosen, args)
|
||||
end
|
||||
|
||||
-- Boot.canUpdateInPlace() -> boolean
|
||||
--
|
||||
-- May this build hand off to a downloaded payload? True for a packaged build
|
||||
-- (fused or unpacked -- see the header), false for a dev / source checkout, and
|
||||
-- false whenever that cannot be established: the gate must never open by
|
||||
-- accident. Boot.run and Prelaunch.updateAllowed both ask this one function,
|
||||
-- so the boot gate and the --update gate cannot disagree.
|
||||
function Boot.canUpdateInPlace()
|
||||
local fs = love and love.filesystem
|
||||
if not fs then return false end
|
||||
if fs.isFused and fs.isFused() then return true end
|
||||
local ok, Version = pcall(require, "src.core.Version")
|
||||
if not ok or type(Version) ~= "table" or type(Version.isDev) ~= "function" then
|
||||
return false
|
||||
end
|
||||
return not Version.isDev()
|
||||
end
|
||||
|
||||
-- Boot.run(args) -> boolean
|
||||
--
|
||||
-- The first line of love.load. True means a payload was mounted and
|
||||
@@ -282,7 +310,7 @@ end
|
||||
-- bundled game as normal.
|
||||
function Boot.run(args)
|
||||
-- Dev / source checkouts never self-update.
|
||||
if not (love.filesystem.isFused and love.filesystem.isFused()) then
|
||||
if not Boot.canUpdateInPlace() then
|
||||
return false
|
||||
end
|
||||
-- Switch (and any host without validated network): never probe payloads.
|
||||
|
||||
+28
-1
@@ -151,6 +151,20 @@ function Check.releaseUrl()
|
||||
return "https://github.com/" .. Check.REPO .. "/releases/latest"
|
||||
end
|
||||
|
||||
-- The release target's "port" tag, or nil on a desktop package. The handheld
|
||||
-- launchers export the marker (build-linux-arm-sbc.sh sets
|
||||
-- POKEPORT_PORTMASTER, build-rg34xxsp.sh sets POKEPORT_RG34XXSP).
|
||||
-- POKEPORT_HANDHELD is honoured as a fallback because the SBC launcher has
|
||||
-- always exported it, so packs built before the explicit marker existed still
|
||||
-- identify themselves as the PortMaster port instead of a desktop Linux build.
|
||||
function Check.hostPort()
|
||||
if type(os.getenv) ~= "function" then return nil end
|
||||
if os.getenv("POKEPORT_PORTMASTER") then return "portmaster" end
|
||||
if os.getenv("POKEPORT_RG34XXSP") then return "rg34xxsp" end
|
||||
if os.getenv("POKEPORT_HANDHELD") == "1" then return "portmaster" end
|
||||
return nil
|
||||
end
|
||||
|
||||
local worker -- the love.thread, once started
|
||||
local cmdCh, stateCh -- the two channels
|
||||
local workerReady -- nil = untried, true = running, false = unavailable
|
||||
@@ -160,7 +174,7 @@ local cache = { status = "idle" } -- newest snapshot from the worker
|
||||
local function target()
|
||||
local osName = love and love.system and love.system.getOS and love.system.getOS() or nil
|
||||
local arch = jit and jit.arch or nil
|
||||
local port = os.getenv("POKEPORT_PORTMASTER")
|
||||
local port = Check.hostPort()
|
||||
return { os = osName, arch = arch, port = port }
|
||||
end
|
||||
|
||||
@@ -297,6 +311,19 @@ function Check.fullUpdateAction()
|
||||
if name:find("%.flatpak$") or os.getenv("FLATPAK_ID") then
|
||||
return { label = "Download Flatpak update", url = st.full.url }
|
||||
end
|
||||
if Check.hostPort() then
|
||||
-- A handheld has no browser to open a download in, so fetch the port
|
||||
-- package into the save directory instead of offering an AppImage that
|
||||
-- could not run here. The player then re-extracts it over the port
|
||||
-- folder (see docs/linux-arm-sbc.md).
|
||||
if st.status == "full_ready" then
|
||||
return { label = "Update package ready", url = Check.releaseUrl() }
|
||||
end
|
||||
if ensureWorker() then
|
||||
return { label = "Download port update", kind = "download" }
|
||||
end
|
||||
return { label = "Open releases", url = Check.releaseUrl() }
|
||||
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 }
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
-- The self-updater's host gate: which builds may hand off to a downloaded
|
||||
-- payload. A packaged build updates in place -- fused (AppImage, Flatpak's
|
||||
-- game.love) or unpacked (every PortMaster-style port, which runs
|
||||
-- `love <dir>` and so reports isFused() false). A dev / source checkout must
|
||||
-- not, and an unknown host must fail closed.
|
||||
--
|
||||
-- This gate is the difference between the PortMaster SBC port applying an
|
||||
-- update and silently ignoring it after the launcher already said "Restart to
|
||||
-- update", so the unpacked case is pinned here.
|
||||
-- luajit tests/engine/update_boot_host_gate.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
local Boot = require("src.update.Boot")
|
||||
local love_stub = require("tests.love_stub")
|
||||
|
||||
local VERSION_KEY = "src.core.Version"
|
||||
|
||||
-- A packaged build stamps a real engine (X.Y.Z); a working tree keeps the
|
||||
-- "0.0.0-dev" placeholder, which is what Version.isDev() reports.
|
||||
local function withVersion(isDev, fn)
|
||||
local saved = package.loaded[VERSION_KEY]
|
||||
package.loaded[VERSION_KEY] = { isDev = function() return isDev end }
|
||||
local ok, err = pcall(fn)
|
||||
package.loaded[VERSION_KEY] = saved
|
||||
if not ok then error(err, 0) end
|
||||
end
|
||||
|
||||
-- Stand in for the running LÖVE process: nil (no love at all) or a filesystem
|
||||
-- that reports the given fused state.
|
||||
local function withLove(fs, fn)
|
||||
local saved = _G.love
|
||||
_G.love = fs and { filesystem = fs } or nil
|
||||
local ok, err = pcall(fn)
|
||||
_G.love = saved
|
||||
if not ok then error(err, 0) end
|
||||
end
|
||||
|
||||
local function fsWith(isFused)
|
||||
if isFused == nil then return {} end
|
||||
return { isFused = function() return isFused end }
|
||||
end
|
||||
|
||||
-- no love at all (headless host): never update
|
||||
withLove(nil, function()
|
||||
eq(Boot.canUpdateInPlace(), false, "no love.filesystem -> no in-place update")
|
||||
end)
|
||||
|
||||
-- fused wins outright: a binary carrying its own game archive is a packaged
|
||||
-- build whatever the engine string says
|
||||
withLove(fsWith(true), function()
|
||||
withVersion(false, function()
|
||||
eq(Boot.canUpdateInPlace(), true, "fused packaged build updates in place")
|
||||
end)
|
||||
withVersion(true, function()
|
||||
eq(Boot.canUpdateInPlace(), true, "fused build updates even with a dev engine string")
|
||||
end)
|
||||
end)
|
||||
|
||||
-- the fix: an unpacked packaged build (PortMaster SBC / RG34XXSP) is not fused,
|
||||
-- but it carries a released engine and its source on disk is never rewritten,
|
||||
-- so a downloaded payload can be mounted over it
|
||||
withLove(fsWith(false), function()
|
||||
withVersion(false, function()
|
||||
eq(Boot.canUpdateInPlace(), true,
|
||||
"unpacked released build (love <dir>) updates in place")
|
||||
end)
|
||||
withVersion(true, function()
|
||||
eq(Boot.canUpdateInPlace(), false,
|
||||
"unpacked dev checkout (engine 0.0.0-dev) never updates")
|
||||
end)
|
||||
end)
|
||||
|
||||
-- a filesystem that cannot answer the fused question at all still updates when
|
||||
-- the engine proves this is a packaged build
|
||||
withLove(fsWith(nil), function()
|
||||
withVersion(false, function()
|
||||
eq(Boot.canUpdateInPlace(), true,
|
||||
"released build without isFused() still updates")
|
||||
end)
|
||||
end)
|
||||
|
||||
-- fail closed: an unreadable / malformed Version must not open the gate
|
||||
withLove(fsWith(false), function()
|
||||
local saved = package.loaded[VERSION_KEY]
|
||||
package.loaded[VERSION_KEY] = "not a module"
|
||||
eq(Boot.canUpdateInPlace(), false, "non-table Version -> no in-place update")
|
||||
package.loaded[VERSION_KEY] = { engine = "1.4.2" }
|
||||
eq(Boot.canUpdateInPlace(), false, "Version without isDev -> no in-place update")
|
||||
package.loaded[VERSION_KEY] = nil
|
||||
eq(Boot.canUpdateInPlace(), false, "unloadable Version -> no in-place update")
|
||||
package.loaded[VERSION_KEY] = saved
|
||||
end)
|
||||
|
||||
-- Boot.run is the first line of love.load: in a dev checkout it must return
|
||||
-- false immediately, leaving the bundled game to boot as normal
|
||||
withLove(love_stub.filesystem, function()
|
||||
withVersion(true, function()
|
||||
eq(Boot.run(nil), false, "Boot.run is a no-op in a dev checkout")
|
||||
end)
|
||||
end)
|
||||
|
||||
T.finish("update_boot_host_gate")
|
||||
@@ -47,11 +47,40 @@ 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")
|
||||
-- Handheld ports run aarch64 Linux, so the port tag must win over the arch
|
||||
-- mapping: a desktop AppImage cannot be installed on a PortMaster device.
|
||||
eq(Check.fullAssetName("1.4.2", "Linux", "aarch64", "portmaster"),
|
||||
"gen1recomp-1.4.2-sbc-portmaster.zip", "PortMaster SBC package mapping")
|
||||
eq(Check.fullAssetName("1.4.2", "Linux", "aarch64", "rg34xxsp"),
|
||||
"gen1recomp-1.4.2-rg34xxsp-stockos64-mod.zip", "RG34XXSP 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,
|
||||
"invalid full-package version rejected")
|
||||
|
||||
-- The port tag comes from the launcher's environment. POKEPORT_HANDHELD is the
|
||||
-- marker the SBC launcher has always exported, so a pack built before the
|
||||
-- explicit POKEPORT_PORTMASTER existed is still identified as the PortMaster
|
||||
-- port rather than a desktop Linux build.
|
||||
do
|
||||
local realGetenv = os.getenv
|
||||
local function portWith(vars)
|
||||
os.getenv = function(name) return vars[name] end
|
||||
local port = Check.hostPort()
|
||||
os.getenv = realGetenv
|
||||
return port
|
||||
end
|
||||
eq(portWith({ POKEPORT_PORTMASTER = "1" }), "portmaster",
|
||||
"PortMaster launcher marker identifies the SBC port")
|
||||
eq(portWith({ POKEPORT_RG34XXSP = "1" }), "rg34xxsp",
|
||||
"RG34XXSP launcher marker identifies that port")
|
||||
eq(portWith({ POKEPORT_HANDHELD = "1" }), "portmaster",
|
||||
"legacy handheld marker identifies the SBC port")
|
||||
eq(portWith({ PORTMASTER = "1" }), nil,
|
||||
"PORTMASTER alone is not the release-target marker")
|
||||
eq(portWith({}), nil, "a desktop Linux build has no port tag")
|
||||
end
|
||||
|
||||
-- A legacy Android shell needs one manual package update even when its
|
||||
-- downloaded payload already reports the latest engine version.
|
||||
eq(Check.androidNeedsInstallerBootstrap("Android", false), true,
|
||||
|
||||
Reference in New Issue
Block a user