Compare commits

...
Author SHA1 Message Date
The412Banner 5ca58f6fbe droiddeck-fex: use DroidDeck's own FEX and x86 root without Steam's
x86 Linux programs on the desktop (an x86_64 AppImage) needed Valve's
FEX (Steam app 3127680) and Steam Linux Runtime 3.0; without them they
exited with status 126. The x86-64 client work built our own FEX
(release fex-2609-r3: openat2 fallback for proot, GL/Vulkan/EGL/ALSA
thunks) installed under /usr/local, run against FEX's Ubuntu 24.04 x86
image.

Look for FEX in /usr/local as well as /usr, and when Steam's runtime is
absent accept a plain x86 root at /opt/droiddeck-fex/rootfs once its
unpack is marked complete. Steam's FEX and runtime still win when
installed.
2026-10-05 15:06:28 -04:00
2 changed files with 43 additions and 5 deletions

No files matched your search

@@ -30,6 +30,11 @@ FEX_APPID = "3127680"
SNIPER_APPID = "1628350"
INSTALL_DIRS = {FEX_APPID: "FEX-Emu", SNIPER_APPID: "SteamLinuxRuntime_sniper"}
MARKER = ".droiddeck-fex"
# Without Steam's FEX and runtime: DroidDeck's own FEX build in /usr/local (the x86-64 client's,
# with GL/Vulkan thunks) and a plain x86 root - FEX's Ubuntu image unpacked - marked complete here.
OWN_ROOTFS = "/opt/droiddeck-fex/rootfs"
OWN_ROOTFS_DONE = ".droiddeck-complete"
SYSTEM_PREFIXES = ("/usr/local", "/usr")
LAYOUT = 1
GUEST_LIBDIRS = (("x86_64", "lib/x86_64-linux-gnu"), ("i386", "lib/i386-linux-gnu"))
PRELOAD_NAMES = ("libblsession.so", "libfakeinput.so")
@@ -142,7 +147,7 @@ def fex_install():
tool = app_dir(FEX_APPID)
if tool:
prefixes += [(os.path.join(tool, sub), True) for sub in ("usr", "", "files/usr", "files")]
prefixes.append(("/usr", False))
prefixes += [(prefix, False) for prefix in SYSTEM_PREFIXES]
for prefix, portable in prefixes:
binary = next((os.path.join(prefix, "bin", name) for name in ("FEX", "FEXInterpreter")
if os.access(os.path.join(prefix, "bin", name), os.X_OK)), None)
@@ -179,6 +184,13 @@ def runtime_platform():
return max(platforms, key=platform_version, default=None)
def own_rootfs():
if os.path.isfile(os.path.join(OWN_ROOTFS, OWN_ROOTFS_DONE)) and \
os.path.isfile(os.path.join(OWN_ROOTFS, "lib/x86_64-linux-gnu/libc.so.6")):
return OWN_ROOTFS
return None
def unescape(raw):
return ESCAPE.sub(lambda match: bytes([int(match.group(1), 8)]), raw)
@@ -308,6 +320,7 @@ def request_install():
def describe(fex, platform):
# platform: Steam's sniper platform folder, or the own root when Steam's runtime is absent.
return {
"fex": fex["bin"] if fex else None,
"server": fex["server"] if fex else None,
@@ -322,7 +335,7 @@ def describe(fex, platform):
def prepare():
with locked():
fex = fex_install()
platform = runtime_platform()
platform = runtime_platform() or own_rootfs()
status = describe(fex, platform)
if not status["ready"]:
missing = []
@@ -341,9 +354,12 @@ def prepare():
"the Steam client was asked to install it" if requested else "open Steam once and it is installed for you"))
write_if_changed(STATUS_FILE, json.dumps(status, indent=2) + "\n")
return None
root = os.path.join(platform, "files")
if materialize(platform):
say("prepared %s as the x86 runtime" % os.path.basename(platform))
if platform == OWN_ROOTFS:
root = OWN_ROOTFS
else:
root = os.path.join(platform, "files")
if materialize(platform):
say("prepared %s as the x86 runtime" % os.path.basename(platform))
install_preloads(root)
write_config(fex, root)
fex["rootfs"] = root
+22
View File
@@ -245,6 +245,28 @@ class PrepareTest(FexTestCase):
self.assertFalse((self.home / ".config/droiddeck/fex-wanted").exists())
self.assertEqual(self.quiet(FEX["prepare"])[1], "")
def test_the_own_build_and_root_stand_in_for_steams(self):
local = self.tmp / "usr-local"
self.write(local / "bin/FEX", elf(183), 0o755)
self.write(local / "bin/FEXServer", elf(183), 0o755)
(local / "lib/fex-emu/HostThunks").mkdir(parents=True)
(local / "share/fex-emu/GuestThunks").mkdir(parents=True)
self.write(local / "share/fex-emu/ThunksDB.json", "{}")
root = self.tmp / "own-rootfs"
self.write(root / "lib/x86_64-linux-gnu/libc.so.6", elf(62))
with mock.patch.dict(G, {"SYSTEM_PREFIXES": (str(local),), "OWN_ROOTFS": str(root),
"steam_running": lambda: False}):
# An unpack that did not finish is not a root.
self.assertIsNone(self.quiet(FEX["prepare"])[0])
self.write(root / ".droiddeck-complete")
fex, said = self.quiet(FEX["prepare"])
self.assertEqual((fex["bin"], fex["rootfs"], fex["portable"]), (str(local / "bin/FEX"), str(root), False))
self.assertNotIn("prepared", said)
status = json.loads((self.home / ".local/share/droiddeck-fex/status.json").read_text())
self.assertEqual((status["ready"], status["runtime"], status["thunks"]), (True, str(root), True))
config = json.loads((self.home / ".local/share/droiddeck-fex/config/Config.json").read_text())
self.assertEqual(config["Config"]["RootFS"], str(root))
def test_status_refreshes_what_the_app_reads_once_steam_installed_fex(self):
with mock.patch.dict(G, {"steam_running": lambda: False}):
self.quiet(FEX["prepare"])