feat(update): gate payloads by native host family

This commit is contained in:
Codex Agent
2026-08-12 17:23:25 -05:00
parent c3136bf8f7
commit 0aab11b690
5 changed files with 122 additions and 16 deletions
+3
View File
@@ -8,6 +8,9 @@ local Version = {
-- "-dev" placeholder; CI stamps the real X.Y.Z into
-- the packed game.love only, never the working tree.
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.
-- Bump only when a payload needs a newer native
-- binary (e.g. a LOVE version bump); an older shell
+48 -13
View File
@@ -80,7 +80,8 @@ local function purgeBundledModules()
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
-- 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
return nil, "payload has no usable Version table"
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
-- 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.
-- 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
-- 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
-- already ship), or superseded by the chosen one (not newer than chosen).
-- A payload newer than the chosen one but unrunnable here (minShell too
-- high) is kept: a future shell upgrade may be able to run it.
function Boot.select(candidates, bundledEngine, bundledShell)
-- A newer incompatible payload is kept: a matching host or future shell
-- may be able to run it.
function Boot.select(candidates, bundledEngine, bundledShell, bundledPayloadHost)
local chosen
for _, c in ipairs(candidates) do
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 not chosen or Semver.compare(c.engine, chosen.engine) > 0 then
chosen = c
@@ -132,9 +159,15 @@ function Boot.select(candidates, bundledEngine, bundledShell)
local toDelete = {}
for _, c in ipairs(candidates) do
if not (chosen and c.name == chosen.name) then
local stale = Semver.compare(c.engine, bundledEngine) <= 0
if chosen and Semver.compare(c.engine, chosen.engine) <= 0 then
stale = true
local stale = false
-- Never clean up another host family's payloads. A shared save directory
-- 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
if stale then toDelete[#toDelete + 1] = c.name end
end
@@ -223,6 +256,7 @@ local function runInner(args)
name = entry,
engine = info.engine,
minShell = info.minShell,
payloadHost = info.payloadHost,
}
end
end
@@ -230,7 +264,8 @@ local function runInner(args)
end
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
love.filesystem.remove(PAYLOAD_DIR .. "/" .. victim)
+3 -2
View File
@@ -153,8 +153,9 @@ local function gatePasses(rel)
local info = Boot.probePayload(rel)
if not info then return true end
local shell = (Version and Version.shell) or 1
if info.minShell and info.minShell > shell then return false end
return true
local payloadHost = (Version and Version.payloadHost) or "love"
if Boot.canHost then return Boot.canHost(info, shell, payloadHost) end
return not (info.minShell and info.minShell > shell)
end
-- ---------------------------------------------------------------------------