mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-28 18:06:09 +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:
+114
-24
@@ -4,18 +4,79 @@ local HostShell = {}
|
||||
|
||||
-- Our AppRun exports LD_LIBRARY_PATH="$APPDIR/lib:..." so every subprocess we
|
||||
-- spawn tries to link against the libraries we're shipping instead of the
|
||||
-- system ones. We want to unset the var so that any system tools can find
|
||||
-- their proper libraries. Only needed when running in an AppImage.
|
||||
-- LD_PRELOAD is Steam's overlay (#1470): its 32-bit half cannot load into a
|
||||
-- 64-bit child, so ld.so prints an error into that child's output.
|
||||
-- system ones. Host tools (curl, zenity) need that unset. A *bundled* AppDir
|
||||
-- curl must keep APPDIR on LD_LIBRARY_PATH so it resolves the bundled
|
||||
-- libssl/libcrypto -- scrubbing it forces host OpenSSL and segfaults across
|
||||
-- distros. LD_PRELOAD is always scrubbed: Steam's overlay (#1470) cannot load
|
||||
-- into a 64-bit child.
|
||||
function HostShell.envPrefix()
|
||||
return HostShell.curlEnvPrefix("host")
|
||||
end
|
||||
|
||||
-- kind "bundled" = AppDir / Flatpak curl (keep LD_LIBRARY_PATH, scrub preload).
|
||||
-- kind "host" = system curl (scrub both).
|
||||
function HostShell.curlEnvPrefix(kind)
|
||||
local unset = ""
|
||||
if os.getenv("APPIMAGE") then unset = unset .. "-u LD_LIBRARY_PATH " end
|
||||
if kind ~= "bundled" and os.getenv("APPIMAGE") then
|
||||
unset = unset .. "-u LD_LIBRARY_PATH "
|
||||
end
|
||||
if os.getenv("LD_PRELOAD") then unset = unset .. "-u LD_PRELOAD " end
|
||||
if unset == "" then return "" end
|
||||
return "env " .. unset
|
||||
end
|
||||
|
||||
local curlResolved = nil -- { path=, kind= } once per Lua state
|
||||
|
||||
local function pathIsExecutable(path)
|
||||
if type(path) ~= "string" or path == "" then return false end
|
||||
local f = io.open(path, "rb")
|
||||
if not f then return false end
|
||||
f:close()
|
||||
-- Best-effort: existence is enough; exec bit is checked by the spawn.
|
||||
return true
|
||||
end
|
||||
|
||||
-- Prefer Flatpak /app/bin/curl, then $APPDIR/{usr/,}bin/curl, else host "curl".
|
||||
function HostShell.resolveCurl()
|
||||
if curlResolved ~= nil then return curlResolved.path, curlResolved.kind end
|
||||
local candidates = {}
|
||||
if os.getenv("FLATPAK_ID") then
|
||||
candidates[#candidates + 1] = { "/app/bin/curl", "bundled" }
|
||||
end
|
||||
local appdir = os.getenv("APPDIR")
|
||||
if type(appdir) == "string" and appdir ~= "" then
|
||||
candidates[#candidates + 1] = { appdir .. "/usr/bin/curl", "bundled" }
|
||||
candidates[#candidates + 1] = { appdir .. "/bin/curl", "bundled" }
|
||||
end
|
||||
for _, c in ipairs(candidates) do
|
||||
if pathIsExecutable(c[1]) then
|
||||
curlResolved = { path = c[1], kind = c[2] }
|
||||
return curlResolved.path, curlResolved.kind
|
||||
end
|
||||
end
|
||||
curlResolved = { path = "curl", kind = "host" }
|
||||
return curlResolved.path, curlResolved.kind
|
||||
end
|
||||
|
||||
-- Quoted curl argv0 for a shell command, plus the matching env prefix.
|
||||
function HostShell.curlInvocation()
|
||||
local path, kind = HostShell.resolveCurl()
|
||||
return HostShell.curlEnvPrefix(kind) .. HostShell.quote(path), kind
|
||||
end
|
||||
|
||||
-- Diagnostics for support / About screens.
|
||||
function HostShell.curlDiagnostics()
|
||||
local path, kind = HostShell.resolveCurl()
|
||||
return {
|
||||
path = path,
|
||||
kind = kind,
|
||||
appimage = os.getenv("APPIMAGE") ~= nil,
|
||||
appdir = os.getenv("APPDIR"),
|
||||
flatpakId = os.getenv("FLATPAK_ID"),
|
||||
haveCurl = HostShell.haveCurl(),
|
||||
}
|
||||
end
|
||||
|
||||
-- Windows: every host tool we shell out to (curl for the update and mod-index
|
||||
-- fetches, the PowerShell ROM picker, the update downloader's `start /b`) is
|
||||
-- spawned through io.popen / os.execute, which run it under cmd.exe. A
|
||||
@@ -126,12 +187,18 @@ local function withPopenLock(fn)
|
||||
if not okAtomic then fn() end
|
||||
end
|
||||
|
||||
-- Wraps io.popen with the AppImage env fix applied and lua errors swallowed
|
||||
function HostShell.popen(command, mode)
|
||||
-- Wraps io.popen with the AppImage env fix applied and lua errors swallowed.
|
||||
-- opts.envPrefix overrides the default HostShell.envPrefix() (pass "" to skip,
|
||||
-- or curlEnvPrefix(kind) when spawning a resolved curl binary).
|
||||
function HostShell.popen(command, mode, opts)
|
||||
HostShell.releasePointerGrab()
|
||||
local prefix = HostShell.envPrefix()
|
||||
if type(opts) == "table" and opts.envPrefix ~= nil then
|
||||
prefix = opts.envPrefix
|
||||
end
|
||||
local pipe
|
||||
withPopenLock(function()
|
||||
local ok, p = pcall(io.popen, HostShell.envPrefix() .. command, mode or "r")
|
||||
local ok, p = pcall(io.popen, prefix .. command, mode or "r")
|
||||
pipe = (ok and p) or nil
|
||||
end)
|
||||
return pipe
|
||||
@@ -193,6 +260,12 @@ function HostShell.restart()
|
||||
|
||||
local appimage = os.getenv("APPIMAGE")
|
||||
if not appimage then
|
||||
-- Flatpak and plain desktop: love.event.quit("restart") uses
|
||||
-- execv(/proc/self/exe). Inside bwrap that path is valid, but PhysFS
|
||||
-- locks on a mounted .love payload can survive the exec boundary and
|
||||
-- hang the next Boot.mount. Callers that just wrote updates/*.love
|
||||
-- should already have closed archive handles; Boot.run still owns
|
||||
-- crash-guard rollback if a mid-handoff restart dies.
|
||||
love.event.quit("restart")
|
||||
return
|
||||
end
|
||||
@@ -341,7 +414,10 @@ local curlAvailable = nil
|
||||
|
||||
function HostShell.haveCurl()
|
||||
if curlAvailable ~= nil then return curlAvailable end
|
||||
local pipe = HostShell.popen("curl --version")
|
||||
local path, kind = HostShell.resolveCurl()
|
||||
local pipe = HostShell.popen(
|
||||
HostShell.quote(path) .. " --version", "r",
|
||||
{ envPrefix = HostShell.curlEnvPrefix(kind) })
|
||||
if not pipe then curlAvailable = false return false end
|
||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||
HostShell.pclose(pipe)
|
||||
@@ -349,6 +425,12 @@ function HostShell.haveCurl()
|
||||
return curlAvailable
|
||||
end
|
||||
|
||||
local function curlCmd(flags)
|
||||
local path, kind = HostShell.resolveCurl()
|
||||
return HostShell.quote(path) .. " " .. flags,
|
||||
{ envPrefix = HostShell.curlEnvPrefix(kind) }
|
||||
end
|
||||
|
||||
-- An older mobile build reports nil here and falls back to the "no transport"
|
||||
-- error the callers already show.
|
||||
local function haveBridge()
|
||||
@@ -409,9 +491,11 @@ function HostShell.httpDownload(url, absPath, userAgent, accept, maxTime, etagPa
|
||||
if type(absPath) ~= "string" or absPath == "" then return nil, "missing path" end
|
||||
userAgent = userAgent or "gen1recomp"
|
||||
if HostShell.haveCurl() then
|
||||
local cmd = ("curl -fsSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 15 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 300)
|
||||
local head, popts = curlCmd(
|
||||
("-fsSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 15 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 300))
|
||||
local cmd = head
|
||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||
if accept then
|
||||
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
||||
@@ -423,7 +507,7 @@ function HostShell.httpDownload(url, absPath, userAgent, accept, maxTime, etagPa
|
||||
cmd = cmd .. "-o " .. HostShell.quote(absPath) .. " "
|
||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||
.. HostShell.quote(url) .. " 2>&1"
|
||||
local pipe = HostShell.popen(cmd)
|
||||
local pipe = HostShell.popen(cmd, "r", popts)
|
||||
if not pipe then return nil, "could not start download" end
|
||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||
HostShell.pclose(pipe)
|
||||
@@ -466,16 +550,18 @@ function HostShell.httpGet(url, userAgent, accept, maxTime)
|
||||
-- BODY, and on the two services this talks to that body is the whole
|
||||
-- diagnosis: GitHub's 403 says "API rate limit exceeded for <ip>", which
|
||||
-- tells a user to wait rather than to go hunting for a broken index.
|
||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 40)
|
||||
local head, popts = curlCmd(
|
||||
("-sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 40))
|
||||
local cmd = head
|
||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||
if accept then
|
||||
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
||||
end
|
||||
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||
.. HostShell.quote(url) .. " 2>&1"
|
||||
local pipe = HostShell.popen(cmd)
|
||||
local pipe = HostShell.popen(cmd, "r", popts)
|
||||
if not pipe then return nil, "could not run curl" end
|
||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||
HostShell.pclose(pipe)
|
||||
@@ -562,9 +648,11 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
||||
-- newlines. The body is staged above because io.popen cannot be opened
|
||||
-- for both writing and reading. No -f, matching httpGet: the response
|
||||
-- body is discarded anyway, and curl's stderr carries the diagnosis.
|
||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 40)
|
||||
local head, popts = curlCmd(
|
||||
("-sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 40))
|
||||
local cmd = head
|
||||
.. "-X POST "
|
||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||
if contentType then
|
||||
@@ -574,7 +662,7 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
||||
.. "--data-binary " .. HostShell.quote("@" .. bodyPath) .. " "
|
||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||
.. HostShell.quote(url) .. " 2>&1"
|
||||
local pipe = HostShell.popen(cmd)
|
||||
local pipe = HostShell.popen(cmd, "r", popts)
|
||||
if not pipe then
|
||||
pcall(os.remove, bodyPath)
|
||||
return nil, "could not run curl"
|
||||
@@ -751,8 +839,10 @@ function HostShell.httpRequest(url, opts)
|
||||
pcall(os.remove, headerPath)
|
||||
end
|
||||
|
||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d "):format(maxTime)
|
||||
local head, popts = curlCmd(
|
||||
("-sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d "):format(maxTime))
|
||||
local cmd = head
|
||||
.. "-X " .. HostShell.quote(method) .. " "
|
||||
.. "-H " .. HostShell.quote("@" .. headerPath) .. " "
|
||||
if body then
|
||||
@@ -761,7 +851,7 @@ function HostShell.httpRequest(url, opts)
|
||||
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||
.. HostShell.quote(url) .. " 2>&1"
|
||||
|
||||
local pipe = HostShell.popen(cmd)
|
||||
local pipe = HostShell.popen(cmd, "r", popts)
|
||||
if not pipe then
|
||||
cleanup()
|
||||
return nil, "could not run curl"
|
||||
|
||||
+36
-2
@@ -173,13 +173,40 @@ function SaveData.gameFolders()
|
||||
end
|
||||
|
||||
-- The game folder carrying portable.txt, or false. First candidate holding
|
||||
-- the marker wins.
|
||||
-- the marker *and* a writable probe wins. Flatpak installs always use the
|
||||
-- sandbox XDG save dir (portable USB mode does not apply). A read-only
|
||||
-- parent (system /opt, immutable OS, etc.) falls back to LOVE's save
|
||||
-- directory without throwing -- probe I/O is fully guarded.
|
||||
local function dirIsWritable(dir)
|
||||
if type(dir) ~= "string" or dir == "" then return false end
|
||||
local ok, writable = pcall(function()
|
||||
local name = string.format(".write_probe_%d_%d.tmp",
|
||||
os.time() % 100000000, math.random(0, 999999))
|
||||
local path = dir .. SEP .. name
|
||||
local f, err = io.open(path, "wb")
|
||||
if not f then return false end
|
||||
local wrote = f:write("ok")
|
||||
f:close()
|
||||
pcall(os.remove, path)
|
||||
return wrote ~= nil
|
||||
end)
|
||||
return ok and writable == true
|
||||
end
|
||||
|
||||
local function detectPortable()
|
||||
if portableChecked then return portableBase end
|
||||
portableChecked = true
|
||||
portableBase = false
|
||||
-- Flatpak: never honor portable.txt (sandbox home is the persistence root).
|
||||
if type(os.getenv) == "function" and os.getenv("FLATPAK_ID") then
|
||||
return portableBase
|
||||
end
|
||||
for _, base in ipairs(SaveData.gameFolders()) do
|
||||
if pathExists(base .. SEP .. PORTABLE_MARKER) then
|
||||
local markerOk = false
|
||||
pcall(function()
|
||||
markerOk = pathExists(base .. SEP .. PORTABLE_MARKER)
|
||||
end)
|
||||
if markerOk and dirIsWritable(base) then
|
||||
portableBase = base
|
||||
break
|
||||
end
|
||||
@@ -187,6 +214,13 @@ local function detectPortable()
|
||||
return portableBase
|
||||
end
|
||||
|
||||
-- Soft-reset cache so tests can re-run detect after env changes.
|
||||
function SaveData._resetPortableCacheForTests()
|
||||
portableChecked = false
|
||||
portableBase = false
|
||||
portableFsCache = nil
|
||||
end
|
||||
|
||||
function SaveData.isPortable()
|
||||
return detectPortable() ~= false
|
||||
end
|
||||
|
||||
+11
-1
@@ -43,10 +43,13 @@ local function fullAssetName(version, osName, arch, port)
|
||||
return "gen1recomp-" .. version .. "-xbox-uwp.zip"
|
||||
elseif osName == "NX" then
|
||||
return "gen1recomp-" .. version .. "-switch.zip"
|
||||
elseif osName == "Linux" and (port == "flatpak"
|
||||
or (type(os.getenv) == "function" and os.getenv("FLATPAK_ID"))) then
|
||||
return "gen1recomp-" .. version .. "-linux.flatpak"
|
||||
elseif osName == "Linux" and (arch == "arm64" or arch == "aarch64") then
|
||||
return "gen1recomp-" .. version .. "-linux-arm64.AppImage"
|
||||
elseif osName == "Linux" then
|
||||
return "gen1recomp-" .. version .. "-linux.zip"
|
||||
return "gen1recomp-" .. version .. "-linux-x86_64.AppImage"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@@ -288,6 +291,13 @@ function Check.fullUpdateAction()
|
||||
"https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json" }
|
||||
elseif osName == "UWP" then
|
||||
return { label = "Open Xbox install guide", url = Check.releaseUrl() }
|
||||
elseif osName == "Linux" and type(st.full) == "table"
|
||||
and type(st.full.url) == "string" then
|
||||
local name = type(st.full.name) == "string" and st.full.name or ""
|
||||
if name:find("%.flatpak$") or os.getenv("FLATPAK_ID") then
|
||||
return { label = "Download Flatpak update", url = st.full.url }
|
||||
end
|
||||
return { label = "Download AppImage update", url = st.full.url }
|
||||
elseif type(st.full) == "table" and type(st.full.url) == "string" then
|
||||
return { label = "Download full update", url = st.full.url }
|
||||
end
|
||||
|
||||
@@ -310,8 +310,13 @@ local function launchDownload(url, partAbs, doneAbs)
|
||||
.. "type nul > \"" .. doneAbs .. "\"\r\n")
|
||||
os.execute('start "" /b ' .. shq(saveDir .. "/" .. batRel))
|
||||
else
|
||||
-- ( ... ) & backgrounds the whole group so os.execute returns at once
|
||||
os.execute("( " .. HostShell.envPrefix() .. "curl -fsSL --connect-timeout 15 --max-time 900 -o "
|
||||
-- ( ... ) & backgrounds the whole group so os.execute returns at once.
|
||||
-- Use the same dual-env curl resolution as HostShell.http*: bundled
|
||||
-- AppDir/Flatpak curl keeps APPDIR libs; host curl scrubs them.
|
||||
local curlPath, curlKind = HostShell.resolveCurl()
|
||||
os.execute("( " .. HostShell.curlEnvPrefix(curlKind)
|
||||
.. HostShell.quote(curlPath)
|
||||
.. " -fsSL --connect-timeout 15 --max-time 900 -o "
|
||||
.. shq(partAbs) .. " " .. shq(url)
|
||||
.. " ; touch " .. shq(doneAbs) .. " ) >/dev/null 2>&1 &")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user