mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
* Run programs, scripts and AppImages from the desktop Windows programs open with the Proton Steam has chosen, and a Steam game's own launch program is handed to Steam. x86-64 Linux programs and game scripts run under FEX, and an exec the kernel refuses goes to droiddeck-open through libblsession's binfmt.c. Programs on noexec shared storage pass KIO's checks; a folder of Linux programs there, and sharun AppImages, are added by the app once and run from the copy. Executable scripts run without KIO's prompt, mpv plays on the software desktop, FEXServer's socket moves under XDG_RUNTIME_DIR, and Proton runtimes no longer show up as games. * Switch to Desktop from Big Picture without Deck mode Big Picture's Switch to Desktop asks the client's SteamOS manager, which the client only has when started as SteamOS (Deck mode). Without it the request failed and the screen stayed on "Switching to Desktop..." for good. The live compat helper now sees the request on its way to the client and hands it to steamos-session-select, which opens the KDE desktop as Deck mode already did. * Run x86 programs from shared storage where they lie FEX asks for the guest's PROT_EXEC when it maps a program or library, which the kernel refuses on noexec shared storage; FEX never runs those pages itself, so libblsession maps them without it in FEX's processes. An x86 game, its libraries and its start script now run in place instead of being copied in; only native code on noexec storage is still copied. A Steam library's games are never copied, and a Linux game's launch program opened from the desktop goes to Steam like a Windows one.
166 lines
7.7 KiB
Python
166 lines
7.7 KiB
Python
import importlib.machinery
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import re
|
|
import runpy
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
BIN = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin"
|
|
|
|
|
|
def load(name):
|
|
loader = importlib.machinery.SourceFileLoader(name.replace("-", "_"), str(BIN / name))
|
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
|
module = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
library = load("droiddeck-steam-library")
|
|
desktop = load("droiddeck-desktop-games")
|
|
COMPAT = runpy.run_path(str(BIN / "steam-compatibility"))
|
|
|
|
|
|
def entry(index, path, label=""):
|
|
return '\t"%d"\n\t{\n\t\t"path"\t\t"%s"\n\t\t"label"\t\t"%s"\n\t}\n' % (index, path, label)
|
|
|
|
|
|
def manifest(steamapps, appid, name="Game", installdir="Game", flags="4"):
|
|
steamapps.mkdir(parents=True, exist_ok=True)
|
|
(steamapps / ("appmanifest_%s.acf" % appid)).write_text(
|
|
'"AppState"\n{\n\t"appid"\t\t"%s"\n\t"name"\t\t"%s"\n\t"StateFlags"\t\t"%s"\n\t"installdir"\t\t"%s"\n}\n'
|
|
% (appid, name, flags, installdir))
|
|
|
|
|
|
class LibraryListTest(unittest.TestCase):
|
|
def setUp(self):
|
|
temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temp.cleanup)
|
|
self.root = Path(temp.name)
|
|
self.steam = self.root / "Steam"
|
|
self.vdf = self.steam / "steamapps/libraryfolders.vdf"
|
|
self.vdf.parent.mkdir(parents=True)
|
|
self.games = self.root / "Games"
|
|
self.card = self.root / "card"
|
|
self.pc = self.games / "PC"
|
|
self.old = self.games / "Old"
|
|
self.user = self.root / "user-library"
|
|
for folder in (self.pc, self.old, self.user, self.card):
|
|
(folder / "steamapps").mkdir(parents=True)
|
|
manifest(self.pc / "steamapps", 10)
|
|
self.vdf.write_text('"libraryfolders"\n{\n' + entry(0, self.steam) + entry(1, self.old, "Old")
|
|
+ entry(2, self.user, "Mine") + entry(3, self.card, "SD Card") + "}\n")
|
|
self.listing = self.root / "steam-libraries.json"
|
|
for target in (patch.object(library, "GAMES_DIR", str(self.games) + "/"),
|
|
patch.object(library, "LIBRARIES", ((str(self.card), "SD Card"),)),
|
|
patch("builtins.print")):
|
|
target.start()
|
|
self.addCleanup(target.stop)
|
|
|
|
def run_main(self, listing=True):
|
|
with patch.dict(os.environ), patch("sys.argv", ["droiddeck-steam-library", str(self.steam)]):
|
|
os.environ.pop("BL_STEAM_LIBRARIES", None)
|
|
if listing:
|
|
os.environ["BL_STEAM_LIBRARIES"] = str(self.listing)
|
|
library.main()
|
|
return self.vdf.read_text()
|
|
|
|
def paths(self, text):
|
|
return [Path(p) for p in re.findall(r'"path"\s+"([^"]*)"', text)]
|
|
|
|
def test_listed_games_folder_is_registered_and_the_rest_are_forgotten(self):
|
|
self.listing.write_text(json.dumps([{"path": str(self.pc), "label": "PC (PNY USB drive)"}]))
|
|
text = self.run_main()
|
|
self.assertEqual([self.steam, self.user, self.pc], self.paths(text))
|
|
self.assertIn('"label"\t\t"PC (PNY USB drive)"', text)
|
|
self.assertIn('"10"\t\t"0"', text)
|
|
self.assertIn('"label"\t\t"PC (PNY USB drive)"', (self.pc / "libraryfolder.vdf").read_text())
|
|
|
|
def test_absent_listed_folder_is_forgotten_and_comes_back(self):
|
|
self.listing.write_text(json.dumps([{"path": str(self.pc), "label": "PC"}, {"path": str(self.card), "label": "SD Card"}]))
|
|
self.run_main()
|
|
moved = self.root / "unplugged"
|
|
self.pc.rename(moved)
|
|
self.assertNotIn(self.pc, self.paths(self.run_main()))
|
|
moved.rename(self.pc)
|
|
self.assertIn(self.pc, self.paths(self.run_main()))
|
|
self.assertIn(self.card, self.paths(self.vdf.read_text()))
|
|
|
|
def test_without_a_list_only_the_card_is_managed(self):
|
|
text = self.run_main(listing=False)
|
|
self.assertEqual([self.steam, self.old, self.user, self.card], self.paths(text))
|
|
|
|
def test_unreadable_list_keeps_games_folder_entries(self):
|
|
self.listing.write_text("{not json")
|
|
text = self.run_main()
|
|
self.assertEqual([self.steam, self.old, self.user, self.card], self.paths(text))
|
|
|
|
def test_labels_cannot_break_the_vdf(self):
|
|
self.listing.write_text(json.dumps([{"path": str(self.pc), "label": 'P"C\\'}]))
|
|
text = self.run_main()
|
|
self.assertIn('"label"\t\t"P C "', text)
|
|
|
|
|
|
class CompatLibrariesTest(unittest.TestCase):
|
|
def setUp(self):
|
|
temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temp.cleanup)
|
|
self.root = Path(temp.name)
|
|
self.steam = self.root / "Steam"
|
|
self.pc = self.root / "Games/My PC"
|
|
(self.steam / "steamapps").mkdir(parents=True)
|
|
(self.steam / "steamapps/libraryfolders.vdf").write_text(
|
|
'"libraryfolders"\n{\n' + entry(0, self.steam) + entry(1, self.pc, "PC") + "}\n")
|
|
|
|
def test_titles_and_tools_in_a_registered_library_are_found(self):
|
|
manifest(self.steam / "steamapps", 20)
|
|
manifest(self.pc / "steamapps", 30)
|
|
manifest(self.pc / "steamapps", 3127680, installdir="FEX")
|
|
self.assertEqual(["20", "30"], COMPAT["installed_apps"](str(self.steam)))
|
|
self.assertEqual(["1628350"], COMPAT["fex_missing"](str(self.steam)))
|
|
|
|
def test_newest_proton_across_libraries_wins(self):
|
|
for root, build in ((self.steam, "100"), (self.pc, "200")):
|
|
depot = root / "steamapps/common/Proton Experimental (ARM64)"
|
|
(depot / "files/bin-arm64").mkdir(parents=True)
|
|
(depot / "version").write_text(build + " x\n")
|
|
self.assertEqual(str(self.pc / "steamapps/common/Proton Experimental (ARM64)"), COMPAT["find_source"](str(self.steam)))
|
|
|
|
def test_missing_list_still_reads_the_client_library(self):
|
|
(self.steam / "steamapps/libraryfolders.vdf").unlink()
|
|
self.assertEqual(str(self.steam), COMPAT["libraries"](str(self.steam))[0])
|
|
|
|
|
|
class DesktopGamesTest(unittest.TestCase):
|
|
def test_games_in_registered_libraries_get_entries(self):
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
root = Path(temp)
|
|
steam, pc = root / "Steam", root / "Games/PC"
|
|
manifest(steam / "steamapps", 40, name="Internal")
|
|
manifest(pc / "steamapps", 50, name="Outside")
|
|
(steam / "steamapps/libraryfolders.vdf").write_text('"libraryfolders"\n{\n' + entry(0, steam) + entry(1, pc) + "}\n")
|
|
with patch.object(desktop, "STEAM_ROOT", str(steam)), patch.object(desktop, "APPS_DIR", str(root / "apps")), \
|
|
patch.object(desktop, "LIBRARIES", ((str(steam), "Steam Games"), (str(root / "card"), "SD Games"))):
|
|
self.assertEqual({40: "Internal", 50: "Outside"}, desktop.write_entries())
|
|
|
|
def test_runtimes_and_compatibility_tools_get_none(self):
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
root = Path(temp)
|
|
steam = root / "Steam"
|
|
manifest(steam / "steamapps", 40, name="Game")
|
|
manifest(steam / "steamapps", 1628350, name="Steam Linux Runtime 3.0 (sniper)", installdir="SteamLinuxRuntime_sniper")
|
|
manifest(steam / "steamapps", 5000, name="Some Proton", installdir="Some Proton")
|
|
(steam / "steamapps/common/Some Proton").mkdir(parents=True)
|
|
(steam / "steamapps/common/Some Proton/toolmanifest.vdf").write_text('"manifest"\n{\n}\n')
|
|
with patch.object(desktop, "STEAM_ROOT", str(steam)), patch.object(desktop, "APPS_DIR", str(root / "apps")), \
|
|
patch.object(desktop, "LIBRARIES", ((str(steam), "Steam Games"),)):
|
|
self.assertEqual({40: "Game"}, desktop.write_entries())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|