Merge pull request #1820 from 1Jamie/app-image-fixes

feat(linux): ship raw x86 AppImage, Flatpak bundle, and dual-env curl
This commit is contained in:
bryanthaboi
2026-08-26 07:40:48 -04:00
committed by GitHub
20 changed files with 819 additions and 54 deletions
+25
View File
@@ -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")
+76
View File
@@ -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")
+4
View File
@@ -43,6 +43,10 @@ eq(Check.fullAssetName("1.4.2", "Android", "arm64"),
"gen1recomp-1.4.2-android.apk", "Android full asset mapping")
eq(Check.fullAssetName("1.4.2", "Linux", "aarch64"),
"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"),
"gen1recomp++-1.4.2-ios.ipa", "iOS package mapping")
eq(Check.fullAssetName("not-a-version", "Android", "arm64"), nil,