mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 20:20:19 +02:00
feat(update): gate payloads by native host family
This commit is contained in:
+12
-1
@@ -26,7 +26,7 @@ JSON parsing, and sha256 verification run on a background `love.thread`
|
|||||||
|
|
||||||
## Version.lua fields
|
## Version.lua fields
|
||||||
|
|
||||||
`src/core/Version.lua` carries three fields the updater reads directly (the
|
`src/core/Version.lua` carries four fields the updater reads directly (the
|
||||||
existing `modApi`, `linkProtocol`, `saveFormat`, and `cache` fields are
|
existing `modApi`, `linkProtocol`, `saveFormat`, and `cache` fields are
|
||||||
untouched):
|
untouched):
|
||||||
|
|
||||||
@@ -37,6 +37,11 @@ untouched):
|
|||||||
as a valid payload to chainload).
|
as a valid payload to chainload).
|
||||||
- `shell` - the native-shell contract this build's fused executable
|
- `shell` - the native-shell contract this build's fused executable
|
||||||
implements.
|
implements.
|
||||||
|
- `payloadHost` - the native host family an in-place payload targets. Ordinary
|
||||||
|
LÖVE packages use `"love"`. A specialized native package uses a distinct,
|
||||||
|
stable identifier and accepts only payloads carrying that same identifier.
|
||||||
|
A missing field defaults to `"love"`, preserving compatibility with payloads
|
||||||
|
released before this field existed.
|
||||||
- `minShell` - the lowest shell contract required to *run* this payload.
|
- `minShell` - the lowest shell contract required to *run* this payload.
|
||||||
|
|
||||||
Bump `minShell` only when a payload needs something the currently-shipped
|
Bump `minShell` only when a payload needs something the currently-shipped
|
||||||
@@ -49,6 +54,12 @@ rather than deleting it, in case a future shell upgrade can run it, and
|
|||||||
installer instead. Do not bump `minShell` for an ordinary Lua/data release;
|
installer instead. Do not bump `minShell` for an ordinary Lua/data release;
|
||||||
that is exactly the case the updater exists to avoid a reinstall for.
|
that is exactly the case the updater exists to avoid a reinstall for.
|
||||||
|
|
||||||
|
Change `payloadHost` only when the packaged Lua depends on a different native
|
||||||
|
host family. This is separate from `minShell`: the host name answers *which*
|
||||||
|
native integration the payload targets, while the shell number answers *which
|
||||||
|
revision* of that integration it requires. A mismatched-host payload is never
|
||||||
|
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
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ local Version = {
|
|||||||
-- "-dev" placeholder; CI stamps the real X.Y.Z into
|
-- "-dev" placeholder; CI stamps the real X.Y.Z into
|
||||||
-- the packed game.love only, never the working tree.
|
-- the packed game.love only, never the working tree.
|
||||||
shell = 1, -- native-shell contract this build implements
|
shell = 1, -- native-shell contract this build implements
|
||||||
|
payloadHost = "love", -- native host family for in-place Lua payloads.
|
||||||
|
-- A payload must name the same family; this prevents
|
||||||
|
-- mounting code packaged for a different native host.
|
||||||
minShell = 1, -- lowest shell contract that can RUN this payload.
|
minShell = 1, -- lowest shell contract that can RUN this payload.
|
||||||
-- Bump only when a payload needs a newer native
|
-- Bump only when a payload needs a newer native
|
||||||
-- binary (e.g. a LOVE version bump); an older shell
|
-- binary (e.g. a LOVE version bump); an older shell
|
||||||
|
|||||||
+48
-13
@@ -80,7 +80,8 @@ local function purgeBundledModules()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Boot.probePayload(rel) -> { engine = string, minShell = number } | nil, err
|
-- Boot.probePayload(rel)
|
||||||
|
-- -> { engine = string, minShell = number, payloadHost = string } | nil, err
|
||||||
--
|
--
|
||||||
-- Mount the archive at rel (a save-directory-relative path) on an isolated
|
-- Mount the archive at rel (a save-directory-relative path) on an isolated
|
||||||
-- mountpoint, read its src/core/Version.lua by executing the source with
|
-- mountpoint, read its src/core/Version.lua by executing the source with
|
||||||
@@ -104,24 +105,50 @@ function Boot.probePayload(rel)
|
|||||||
if type(v) ~= "table" or type(v.engine) ~= "string" then
|
if type(v) ~= "table" or type(v.engine) ~= "string" then
|
||||||
return nil, "payload has no usable Version table"
|
return nil, "payload has no usable Version table"
|
||||||
end
|
end
|
||||||
return { engine = v.engine, minShell = tonumber(v.minShell) or 1 }
|
return {
|
||||||
|
engine = v.engine,
|
||||||
|
minShell = tonumber(v.minShell) or 1,
|
||||||
|
payloadHost = type(v.payloadHost) == "string" and v.payloadHost or "love",
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Boot.select(candidates, bundledEngine, bundledShell) -> chosen | nil, toDelete
|
-- Pure host gate shared by boot selection and the download worker. Missing
|
||||||
|
-- payloadHost fields mean "love" so payloads made before this contract remain
|
||||||
|
-- compatible with ordinary LOVE packages.
|
||||||
|
function Boot.canHost(info, bundledShell, bundledPayloadHost)
|
||||||
|
if type(info) ~= "table" then return false end
|
||||||
|
local payloadHost = type(info.payloadHost) == "string"
|
||||||
|
and info.payloadHost or "love"
|
||||||
|
local host = type(bundledPayloadHost) == "string"
|
||||||
|
and bundledPayloadHost or "love"
|
||||||
|
return payloadHost == host and (tonumber(info.minShell) or 1)
|
||||||
|
<= (tonumber(bundledShell) or 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function samePayloadHost(info, bundledPayloadHost)
|
||||||
|
local payloadHost = type(info.payloadHost) == "string"
|
||||||
|
and info.payloadHost or "love"
|
||||||
|
local host = type(bundledPayloadHost) == "string"
|
||||||
|
and bundledPayloadHost or "love"
|
||||||
|
return payloadHost == host
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Boot.select(candidates, bundledEngine, bundledShell, bundledPayloadHost)
|
||||||
|
-- -> chosen | nil, toDelete
|
||||||
--
|
--
|
||||||
-- Pure (no love.*): decide which payload to run and which to delete.
|
-- Pure (no love.*): decide which payload to run and which to delete.
|
||||||
-- candidates is a list of { name = , engine = , minShell = }.
|
-- candidates is a list of { name = , engine = , minShell = , payloadHost = }.
|
||||||
-- * chosen: the highest engine that is STRICTLY newer than bundledEngine and
|
-- * chosen: the highest engine that is STRICTLY newer than bundledEngine and
|
||||||
-- whose minShell <= bundledShell (a payload the running shell can host).
|
-- whose payloadHost matches and minShell <= bundledShell.
|
||||||
-- * toDelete: stale payloads -- engine <= bundled (old or the same as what we
|
-- * toDelete: stale payloads -- engine <= bundled (old or the same as what we
|
||||||
-- already ship), or superseded by the chosen one (not newer than chosen).
|
-- already ship), or superseded by the chosen one (not newer than chosen).
|
||||||
-- A payload newer than the chosen one but unrunnable here (minShell too
|
-- A newer incompatible payload is kept: a matching host or future shell
|
||||||
-- high) is kept: a future shell upgrade may be able to run it.
|
-- may be able to run it.
|
||||||
function Boot.select(candidates, bundledEngine, bundledShell)
|
function Boot.select(candidates, bundledEngine, bundledShell, bundledPayloadHost)
|
||||||
local chosen
|
local chosen
|
||||||
for _, c in ipairs(candidates) do
|
for _, c in ipairs(candidates) do
|
||||||
local newer = Semver.compare(c.engine, bundledEngine) > 0
|
local newer = Semver.compare(c.engine, bundledEngine) > 0
|
||||||
local runnable = (c.minShell or 1) <= bundledShell
|
local runnable = Boot.canHost(c, bundledShell, bundledPayloadHost)
|
||||||
if newer and runnable then
|
if newer and runnable then
|
||||||
if not chosen or Semver.compare(c.engine, chosen.engine) > 0 then
|
if not chosen or Semver.compare(c.engine, chosen.engine) > 0 then
|
||||||
chosen = c
|
chosen = c
|
||||||
@@ -132,9 +159,15 @@ function Boot.select(candidates, bundledEngine, bundledShell)
|
|||||||
local toDelete = {}
|
local toDelete = {}
|
||||||
for _, c in ipairs(candidates) do
|
for _, c in ipairs(candidates) do
|
||||||
if not (chosen and c.name == chosen.name) then
|
if not (chosen and c.name == chosen.name) then
|
||||||
local stale = Semver.compare(c.engine, bundledEngine) <= 0
|
local stale = false
|
||||||
if chosen and Semver.compare(c.engine, chosen.engine) <= 0 then
|
-- Never clean up another host family's payloads. A shared save directory
|
||||||
stale = true
|
-- may be opened by multiple native packages, and only the matching host
|
||||||
|
-- can decide whether one of its own archives is stale.
|
||||||
|
if samePayloadHost(c, bundledPayloadHost) then
|
||||||
|
stale = Semver.compare(c.engine, bundledEngine) <= 0
|
||||||
|
if chosen and Semver.compare(c.engine, chosen.engine) <= 0 then
|
||||||
|
stale = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if stale then toDelete[#toDelete + 1] = c.name end
|
if stale then toDelete[#toDelete + 1] = c.name end
|
||||||
end
|
end
|
||||||
@@ -223,6 +256,7 @@ local function runInner(args)
|
|||||||
name = entry,
|
name = entry,
|
||||||
engine = info.engine,
|
engine = info.engine,
|
||||||
minShell = info.minShell,
|
minShell = info.minShell,
|
||||||
|
payloadHost = info.payloadHost,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -230,7 +264,8 @@ local function runInner(args)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local Version = require("src.core.Version")
|
local Version = require("src.core.Version")
|
||||||
local chosen, toDelete = Boot.select(candidates, Version.engine, Version.shell)
|
local chosen, toDelete = Boot.select(candidates, Version.engine,
|
||||||
|
Version.shell, Version.payloadHost)
|
||||||
|
|
||||||
for _, victim in ipairs(toDelete) do
|
for _, victim in ipairs(toDelete) do
|
||||||
love.filesystem.remove(PAYLOAD_DIR .. "/" .. victim)
|
love.filesystem.remove(PAYLOAD_DIR .. "/" .. victim)
|
||||||
|
|||||||
@@ -153,8 +153,9 @@ local function gatePasses(rel)
|
|||||||
local info = Boot.probePayload(rel)
|
local info = Boot.probePayload(rel)
|
||||||
if not info then return true end
|
if not info then return true end
|
||||||
local shell = (Version and Version.shell) or 1
|
local shell = (Version and Version.shell) or 1
|
||||||
if info.minShell and info.minShell > shell then return false end
|
local payloadHost = (Version and Version.payloadHost) or "love"
|
||||||
return true
|
if Boot.canHost then return Boot.canHost(info, shell, payloadHost) end
|
||||||
|
return not (info.minShell and info.minShell > shell)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -104,6 +104,34 @@ local function nameSet(list)
|
|||||||
return s
|
return s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Host-family compatibility is independent from the numeric shell revision.
|
||||||
|
-- Missing fields preserve the historical ordinary-LOVE defaults.
|
||||||
|
do
|
||||||
|
check(Boot.canHost({}, 1, "love"),
|
||||||
|
"canHost: legacy payload defaults to ordinary LOVE host and shell 1")
|
||||||
|
check(Boot.canHost({ payloadHost = "love", minShell = 2 }, 2, "love"),
|
||||||
|
"canHost: matching host and sufficient shell pass")
|
||||||
|
check(not Boot.canHost({ payloadHost = "love", minShell = 2 }, 1, "love"),
|
||||||
|
"canHost: newer shell requirement fails")
|
||||||
|
check(not Boot.canHost({ payloadHost = "special", minShell = 1 }, 9, "love"),
|
||||||
|
"canHost: a high shell revision cannot override a host mismatch")
|
||||||
|
check(Boot.canHost({ payloadHost = "special", minShell = 3 }, 3, "special"),
|
||||||
|
"canHost: a specialized host accepts its own payload")
|
||||||
|
check(not Boot.canHost(nil, 1, "love"),
|
||||||
|
"canHost: malformed payload metadata fails closed")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Even an older mismatched-host payload is not ours to clean up.
|
||||||
|
do
|
||||||
|
local candidates = {
|
||||||
|
{ name = "other-old.love", engine = "0.5.0", minShell = 1,
|
||||||
|
payloadHost = "special" },
|
||||||
|
}
|
||||||
|
local chosen, del = Boot.select(candidates, "1.0.0", 1, "love")
|
||||||
|
eq(chosen, nil, "select: old payload for another host is not runnable")
|
||||||
|
eq(#del, 0, "select: old payload for another host is not deleted")
|
||||||
|
end
|
||||||
|
|
||||||
-- empty candidate list: nothing to run, nothing to delete
|
-- empty candidate list: nothing to run, nothing to delete
|
||||||
do
|
do
|
||||||
local chosen, del = Boot.select({}, "1.0.0", 1)
|
local chosen, del = Boot.select({}, "1.0.0", 1)
|
||||||
@@ -127,6 +155,34 @@ do
|
|||||||
check(not d["b.love"], "select: the chosen payload is never deleted")
|
check(not d["b.love"], "select: the chosen payload is never deleted")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- A newer payload for another native host is neither selected nor deleted.
|
||||||
|
-- It may be valid for another full package sharing this save directory.
|
||||||
|
do
|
||||||
|
local candidates = {
|
||||||
|
{ name = "other.love", engine = "2.0.0", minShell = 1,
|
||||||
|
payloadHost = "special" },
|
||||||
|
{ name = "ours.love", engine = "1.5.0", minShell = 1,
|
||||||
|
payloadHost = "love" },
|
||||||
|
}
|
||||||
|
local chosen, del = Boot.select(candidates, "1.0.0", 1, "love")
|
||||||
|
eq(chosen, "ours.love", "select: chooses the matching host payload")
|
||||||
|
eq(#del, 0, "select: incompatible newer host payload is retained")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The same candidate set selects the specialized payload when the bundled
|
||||||
|
-- package identifies as that host family.
|
||||||
|
do
|
||||||
|
local candidates = {
|
||||||
|
{ name = "ordinary.love", engine = "2.1.0", minShell = 1,
|
||||||
|
payloadHost = "love" },
|
||||||
|
{ name = "special.love", engine = "2.0.0", minShell = 1,
|
||||||
|
payloadHost = "special" },
|
||||||
|
}
|
||||||
|
local chosen, del = Boot.select(candidates, "1.0.0", 1, "special")
|
||||||
|
eq(chosen, "special.love", "select: specialized package chooses its host payload")
|
||||||
|
eq(#del, 0, "select: newer ordinary payload remains available to its host")
|
||||||
|
end
|
||||||
|
|
||||||
-- skips payloads whose minShell is above the bundled shell, and KEEPS an
|
-- skips payloads whose minShell is above the bundled shell, and KEEPS an
|
||||||
-- otherwise-newer one for a future shell upgrade instead of deleting it
|
-- otherwise-newer one for a future shell upgrade instead of deleting it
|
||||||
do
|
do
|
||||||
|
|||||||
Reference in New Issue
Block a user