fix(game-env): repair foreign-architecture system DLLs in ARM64 prefixes (#2318)

Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>
This commit is contained in:
MaxsTechReviewandKurt Himebauch authored and GitHub committed 2026-11-10 06:24:03 -05:00
1 parent 1ec2b696b7
commit 13c8edb3d5
6 files changed
+579 -24

No files matched your search

@@ -43,6 +43,7 @@ object SessionFiles {
"usr/local/bin/droiddeck-game-gpu" to "usr/local/bin/droiddeck-game-gpu",
"usr/local/bin/droiddeck-diagnostic-inventory" to "usr/local/bin/droiddeck-diagnostic-inventory",
"usr/local/bin/droiddeck-wincomponents" to "usr/local/bin/droiddeck-wincomponents",
"usr/local/bin/droiddeck-wine-pe" to "usr/local/bin/droiddeck-wine-pe",
"usr/local/bin/droiddeck-msi-install" to "usr/local/bin/droiddeck-msi-install",
"usr/local/bin/droiddeck-esync" to "usr/local/bin/droiddeck-esync",
"usr/local/bin/droiddeck-steam-compat" to "usr/local/bin/droiddeck-steam-compat",
@@ -333,12 +333,12 @@ def engine_fixes(exe, args):
return env, extra, notes
def win_components(compat, app_id):
def win_components(compat, app_id, proton=None, env=None):
if not compat:
return ""
try:
module = runpy.run_path(str(Path(__file__).resolve().parent / "droiddeck-wincomponents"))
names, notes = module["apply"](compat.rstrip("/"), app_id)
names, notes = module["apply"](compat.rstrip("/"), app_id, proton=proton, env=env or os.environ)
for note in notes:
print("droiddeck-game-env: " + note, file=sys.stderr)
return module["overrides_value"](names)
@@ -859,6 +859,60 @@ WINE_DLL_DIRS = ("/lib/wine/i386-windows", "/lib/wine/x86_64-windows", "/lib/win
"/lib/wine/fakedlls", "/lib64/wine", "/lib64/wine/fakedlls", "/lib64/wine/x86_64-windows")
FOREIGN_SUFFIX = ".foreign-arch"
PE = runpy.run_path(str(Path(__file__).resolve().parent / "droiddeck-wine-pe"))
pe_machine = PE["pe_machine"]
def repair_foreign_system_dlls(prefix, proton, env=None):
dist = Path(proton).parent / "files/lib/wine"
windows = Path(prefix) / "pfx/drive_c/windows"
machines_by_folder = PE["proton_pe_machines"](proton, env or {})
if machines_by_folder is None:
return []
repaired = []
for folder, (libdir, machines) in machines_by_folder.items():
builtins = dist / libdir
try:
builtin_names = {name.lower(): name for name in os.listdir(builtins)}
except OSError:
continue
try:
names = sorted(os.listdir(windows / folder))
except OSError:
continue
for name in names:
path = windows / folder / name
builtin_name = builtin_names.get(name.lower())
source = builtins / builtin_name if builtin_name else builtins / name
if path.is_symlink() or not name.lower().endswith(".dll") or not source.is_file() or not path.is_file():
continue
machine = pe_machine(path)
if machine is None or PE["machine_compatible"](path, machines, machine):
continue
link = windows / folder / (name + ".bl-new")
backup = windows / folder / (name + FOREIGN_SUFFIX)
serial = 1
while os.path.lexists(backup):
backup = windows / folder / (name + FOREIGN_SUFFIX + ".%d" % serial)
serial += 1
try:
os.symlink(os.path.relpath(source, path.parent), link)
os.replace(path, backup)
try:
os.replace(link, path)
except OSError:
if not os.path.lexists(path) and os.path.lexists(backup):
os.replace(backup, path)
raise
except OSError:
if link.is_symlink():
link.unlink()
continue
repaired.append("%s/%s (machine 0x%x)" % (folder, name, machine))
return repaired
def is_wine_dll_link(path):
try:
return os.path.islink(path) and os.path.dirname(os.readlink(path)).endswith(WINE_DLL_DIRS)
@@ -1156,6 +1210,42 @@ def main():
env = game_gpu(os.environ.copy())
mic_enabled = env.get("BL_MIC_ENABLED")
debug_dir = env.get("BL_DEBUG_DIR", "")
prefix_path = env.get("STEAM_COMPAT_DATA_PATH", "").rstrip("/")
app_id = game_id(env)
components = ""
waiting = bool(app_id and len(command) > 1 and command[1] == "waitforexitandrun")
config = {}
launch_config = None
launch_config_valid = True
effective_env = env
if waiting:
path = Path.home() / ".config/droiddeck/game-environment.json"
try:
with path.open(encoding="utf-8") as source:
config = json.load(source)
launch_config = config
except FileNotFoundError:
launch_config = {"version": 1}
except (OSError, ValueError, TypeError, AttributeError):
launch_config_valid = False
if launch_config_valid:
try:
effective_env = apply_config(env, launch_config, app_id)
except (OSError, ValueError, TypeError, AttributeError):
launch_config_valid = False
if not launch_config_valid:
print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr)
if waiting:
components = win_components(prefix_path, app_id, command[0], effective_env)
if prefix_path and command:
try:
fixed = repair_foreign_system_dlls(prefix_path, command[0], effective_env)
except OSError as error:
fixed = []
print("droiddeck-game-env: prefix system DLL check skipped (%s)" % error, file=sys.stderr)
if fixed:
print("droiddeck-game-env: replaced foreign-architecture system DLLs with this Proton's own: %s" % ", ".join(fixed),
file=sys.stderr)
scripts = mark_install_scripts(command, env)
if scripts and install_scripts_done(scripts, env):
print("droiddeck-game-env: install scripts already run in this prefix; evaluator skipped", file=sys.stderr)
@@ -1164,8 +1254,6 @@ def main():
digest = scripts_digest(scripts) if scripts else None
except OSError:
digest = None
app_id = game_id(env)
waiting = bool(app_id and len(command) > 1 and command[1] == "waitforexitandrun")
diagnostic_app_id = app_id if waiting and os.environ.get("DROIDDECK_ENHANCED_DIAGNOSTICS") == "1" else None
dirt_prefix = None
if app_id:
@@ -1183,7 +1271,6 @@ def main():
for note in notes:
print("droiddeck-game-env: " + note, file=sys.stderr)
command += extra
components = win_components(env.get("STEAM_COMPAT_DATA_PATH", ""), app_id)
if components:
own = fixes.get("WINEDLLOVERRIDES")
fixes["WINEDLLOVERRIDES"] = components + ";" + own if own else components
@@ -1191,22 +1278,21 @@ def main():
if dirt_prefix:
own = fixes.get("WINEDLLOVERRIDES")
fixes["WINEDLLOVERRIDES"] = own + ";ws2_32=n,b" if own else "ws2_32=n,b"
path = Path.home() / ".config/droiddeck/game-environment.json"
config = {}
try:
with path.open(encoding="utf-8") as source:
config = json.load(source)
if waiting:
if waiting:
if launch_config_valid:
if "vblank_mode" in config.get("shared", {}):
fixes.pop("vblank_mode", None)
env = apply_config(env, config, app_id, fixes)
else:
env = apply_config(env, launch_config, app_id, fixes)
else:
path = Path.home() / ".config/droiddeck/game-environment.json"
try:
with path.open(encoding="utf-8") as source:
config = json.load(source)
env = apply_config(env, sync_config(config, app_id), app_id)
except FileNotFoundError:
if waiting:
env = apply_config(env, {"version": 1}, app_id, fixes)
except (OSError, ValueError, TypeError, AttributeError):
print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr)
except FileNotFoundError:
pass
except (OSError, ValueError, TypeError, AttributeError):
print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr)
if waiting:
before_debug = env
env = apply_game_debug_logs(env, config, debug_dir)
@@ -2,11 +2,17 @@
import json
import os
import re
import runpy
import shutil
import sys
import time
from pathlib import Path
PE = runpy.run_path(str(Path(__file__).resolve().parent / "droiddeck-wine-pe"))
PE_MACHINES = PE["PE_MACHINES"]
pe_machine = PE["pe_machine"]
machine_compatible = PE["machine_compatible"]
STORE = Path("/opt/droiddeck/wincomponents")
SELECTION = Path.home() / ".config/droiddeck/wincomponents.json"
PENDING = Path.home() / ".config/droiddeck/wincomponents-pending.json"
@@ -473,6 +479,15 @@ def save_state(compat, state):
os.replace(temp, compat / STATE)
def foreign_component(rel, path, machines):
parts = Path(rel).parts
folder = parts[2] if len(parts) > 2 and parts[:2] == ("drive_c", "windows") else None
if not machines or folder not in PE_MACHINES or not rel.lower().endswith(".dll"):
return False
machine = pe_machine(path)
return machine is not None and not machine_compatible(path, machines[folder][1], machine)
def pending_for(app_id, pending=PENDING):
try:
with open(pending, encoding="utf-8") as source:
@@ -514,7 +529,8 @@ def wait_for_pending(app_id, pending=PENDING, store=STORE, timeout=PENDING_WAIT,
return "wincomp: %s waited %dms for %s" % (app_id, waited, ",".join(asked))
def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING, wait=wait_for_pending):
def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING, wait=wait_for_pending,
proton=None, env=None):
compat = Path(compat_path)
pfx = compat / "pfx"
waited = wait(app_id, pending, store) if wait else None
@@ -527,6 +543,9 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING
installed = []
values = []
held = set()
skipped = set()
placed_stems = set()
machines = PE["proton_pe_machines"](proton, env) if proton else None
saved_overrides = state.get("overrides") if isinstance(state.get("overrides"), dict) else {}
saved_requires = state.get("requires") if isinstance(state.get("requires"), dict) else {}
recorded = [c for c in state.get("components", []) if isinstance(c, str) and valid_id(c)]
@@ -566,9 +585,17 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING
kept_requires[cid] = list(requires)
values += [(cid, value) for value in registry]
for rel, src in files:
if place(src, pfx / rel, None if rel in state["files"] else compat / BACKUP / rel):
foreign = foreign_component(rel, src, machines)
if foreign:
skipped.add(Path(rel).stem.lower())
changed = False
else:
changed = place(src, pfx / rel, None if rel in state["files"] else compat / BACKUP / rel)
placed_stems.add(Path(rel).stem.lower())
if changed:
copied += 1
keep[rel] = cid
if not foreign:
keep[rel] = cid
removed = restored = 0
for rel, cid in state["files"].items():
if rel in keep:
@@ -579,6 +606,10 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING
path = existing(pfx / rel)
saved = compat / BACKUP / rel
if saved.is_file():
if foreign_component(rel, saved, machines):
if path is not None and not path.is_symlink():
path.unlink()
continue
target = path or (pfx / rel)
os.replace(saved, target)
restored += 1
@@ -590,6 +621,14 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING
(compat / "config_info").unlink()
except FileNotFoundError:
pass
def override_name(entry):
name = entry.partition("=")[0].strip().lower()
return name[:-4] if name.endswith(".dll") else name
dropped = skipped - placed_stems
overrides = [entry for entry in overrides if override_name(entry) not in dropped]
for cid in kept_overrides:
kept_overrides[cid] = [entry for entry in kept_overrides[cid] if override_name(entry) not in dropped]
compat.mkdir(parents=True, exist_ok=True)
new_state = {"files": keep, "components": installed + sorted(held), "overrides": kept_overrides,
"requires": kept_requires, "registry": state.get("registry", {})}
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/python3
from pathlib import Path
PE_MACHINES = {
"system32": ("aarch64-windows", frozenset((0xAA64, 0xA641))),
"syswow64": ("i386-windows", frozenset((0x14C,))),
}
def pe_machine(path):
try:
with open(path, "rb") as image:
head = image.read(64)
if head[:2] != b"MZ" or len(head) < 64:
return None
image.seek(int.from_bytes(head[60:64], "little"))
header = image.read(6)
except OSError:
return None
if header[:4] != b"PE\0\0" or len(header) < 6:
return None
return int.from_bytes(header[4:6], "little")
def pe_has_arm64ec_metadata(path):
try:
with open(path, "rb") as image:
image.seek(60)
raw = image.read(4)
if len(raw) != 4:
return False
pe_offset = int.from_bytes(raw, "little")
image.seek(pe_offset)
if image.read(4) != b"PE\0\0":
return False
coff = image.read(20)
if len(coff) != 20:
return False
section_count = int.from_bytes(coff[2:4], "little")
if section_count > 96:
return False
optional_size = int.from_bytes(coff[16:18], "little")
optional_offset = pe_offset + 24
image.seek(optional_offset)
optional = image.read(optional_size)
if len(optional) != optional_size or int.from_bytes(optional[:2], "little") != 0x20B:
return False
directory_count = int.from_bytes(optional[108:112], "little")
directory_offset = 112 + 10 * 8
if directory_count <= 10 or directory_offset + 8 > len(optional):
return False
load_config_rva = int.from_bytes(optional[directory_offset:directory_offset + 4], "little")
load_config_size = int.from_bytes(optional[directory_offset + 4:directory_offset + 8], "little")
if not load_config_rva or load_config_size < 0xD0:
return False
section_offset = optional_offset + optional_size
for index in range(section_count):
image.seek(section_offset + index * 40)
section = image.read(40)
if len(section) != 40:
return False
virtual_address = int.from_bytes(section[12:16], "little")
raw_size = int.from_bytes(section[16:20], "little")
raw_offset = int.from_bytes(section[20:24], "little")
relative = load_config_rva - virtual_address
if 0 <= relative <= raw_size - 0xD0:
image.seek(raw_offset + relative)
load_config = image.read(0xD0)
if len(load_config) != 0xD0 or int.from_bytes(load_config[:4], "little") < 0xD0:
return False
return int.from_bytes(load_config[0xC8:0xD0], "little") != 0
except (OSError, OverflowError, ValueError):
return False
return False
def machine_compatible(path, machines, machine=None):
machine = pe_machine(path) if machine is None else machine
if machine == 0x8664 and pe_has_arm64ec_metadata(path):
return 0xA641 in machines
return machine in machines
def proton_pe_machines(proton, env=None):
dist = Path(proton).parent / "files/lib/wine"
if (dist / "aarch64-unix").is_dir() and (env or {}).get("PROTON_USE_ARM64", "1") == "1":
return PE_MACHINES
if (dist / "x86_64-unix").is_dir():
return {
"system32": ("x86_64-windows", frozenset((0x8664,))),
"syswow64": PE_MACHINES["syswow64"],
}
return None
+209
View File
@@ -708,6 +708,54 @@ class ReadConfigMappingTest(unittest.TestCase):
class GameEnvironmentTest(unittest.TestCase):
def test_component_filter_and_prefix_repair_use_per_game_proton_architecture(self):
with tempfile.TemporaryDirectory() as tmp:
home = Path(tmp)
config = home / ".config/droiddeck/game-environment.json"
config.parent.mkdir(parents=True)
config.write_text(json.dumps({"version": 1, "shared": {"PROTON_USE_ARM64": "1"},
"games": {"42": {"PROTON_USE_ARM64": "0"}}}))
seen_components, seen_repair, seen_start = [], [], []
globals = MODULE["main"].__globals__
stubs = {
"game_gpu": lambda env: env,
"mark_install_scripts": lambda command, env: [],
"install_scripts_done": lambda scripts, env: False,
"scripts_digest": lambda scripts: None,
"win_components": lambda *args: seen_components.append(args[3]) or "",
"repair_foreign_system_dlls": lambda *args: seen_repair.append(args[2]) or [],
"gl_vsync": lambda env: False,
"game_images": lambda env: None,
"skip_red_launcher": lambda command: command,
"ubisoft_overlay_off": lambda env: None,
"project_zomboid_jvm": lambda command: None,
"engine_fixes": lambda *args: ({}, [], []),
"dirt_ws2_wanted": lambda *args: None,
"apply_game_debug_logs": lambda env, config, debug_dir: env,
"offline_without_anticheat": lambda command, env: command,
"game_logs": lambda command, env, app_id: env,
"directaudio_settings": lambda env: env,
"vram_cap": lambda env: env,
"render_path_defaults": lambda *args: None,
"bridge_feature_level": lambda env: None,
"force_ssbs": lambda command, env: env,
"game_cores": lambda env: None,
"cpu_topology": lambda env, command: None,
"ego_core_plan": lambda *args: None,
"runpy": mock.Mock(run_path=lambda *args: {"launch": lambda command, env: None}),
"load_sync": lambda: {},
"select_sync": lambda command, env, sync: (command, env),
"start": lambda command, env, *args: seen_start.append(env) or 7,
}
environment = {"HOME": tmp, "STEAM_COMPAT_DATA_PATH": "/compatdata/42", "PROTON_USE_ARM64": "1"}
command = ["/stock/proton", "waitforexitandrun", "/game/game.exe"]
with patch.dict(os.environ, environment, clear=True), patch.object(sys, "argv", ["game-env", *command]), \
patch.dict(globals, stubs):
self.assertEqual(MODULE["main"](), 7)
self.assertEqual(["0"], [env["PROTON_USE_ARM64"] for env in seen_components])
self.assertEqual(["0"], [env["PROTON_USE_ARM64"] for env in seen_repair])
self.assertEqual(["0"], [env["PROTON_USE_ARM64"] for env in seen_start])
def test_optional_xbox_loader_failure_keeps_stock_launch(self):
from unittest.mock import patch
for error in (FileNotFoundError('missing'), RuntimeError('module initialization failed')):
@@ -2585,6 +2633,167 @@ class UbisoftCompositionTest(unittest.TestCase):
self.assertEqual(list(prefix.glob(".ubisoft-*.reg")), [])
def pe_image(machine):
head = bytearray(64)
head[:2] = b"MZ"
head[60:64] = (64).to_bytes(4, "little")
return bytes(head) + b"PE\0\0" + machine.to_bytes(2, "little") + b"\0" * 32
def arm64ec_pe_image():
image = bytearray(0x400)
image[:2] = b"MZ"
image[60:64] = (64).to_bytes(4, "little")
image[64:68] = b"PE\0\0"
coff = 68
image[coff:coff + 2] = (0x8664).to_bytes(2, "little")
image[coff + 2:coff + 4] = (1).to_bytes(2, "little")
image[coff + 16:coff + 18] = (240).to_bytes(2, "little")
optional = coff + 20
image[optional:optional + 2] = (0x20B).to_bytes(2, "little")
image[optional + 108:optional + 112] = (16).to_bytes(4, "little")
directory = optional + 112 + 10 * 8
image[directory:directory + 4] = (0x1000).to_bytes(4, "little")
image[directory + 4:directory + 8] = (0xD0).to_bytes(4, "little")
section = optional + 240
image[section + 8:section + 12] = (0x200).to_bytes(4, "little")
image[section + 12:section + 16] = (0x1000).to_bytes(4, "little")
image[section + 16:section + 20] = (0x200).to_bytes(4, "little")
image[section + 20:section + 24] = (0x200).to_bytes(4, "little")
image[0x200:0x204] = (0xD0).to_bytes(4, "little")
image[0x2C8:0x2D0] = (0x10000000).to_bytes(8, "little")
return bytes(image)
class ForeignSystemDllTest(unittest.TestCase):
def prefix(self, tmp, arm=True):
proton = Path(tmp) / "Proton"
dist = proton / "files/lib/wine"
if arm:
(dist / "aarch64-unix").mkdir(parents=True)
else:
(dist / "x86_64-unix").mkdir(parents=True)
for libdir, machine in (("aarch64-windows", 0xAA64), ("i386-windows", 0x14C)):
(dist / libdir).mkdir(parents=True)
for name in ("ucrtbase.dll", "rpcrt4.dll"):
(dist / libdir / name).write_bytes(pe_image(machine))
windows = Path(tmp) / "compat/pfx/drive_c/windows"
for folder in ("system32", "syswow64"):
(windows / folder).mkdir(parents=True)
return proton / "proton", Path(tmp) / "compat", windows
def run_repair(self, compat, proton):
return MODULE["repair_foreign_system_dlls"](str(compat), str(proton))
def test_x64_dll_in_arm64_system32_is_replaced_by_the_builtin(self):
with tempfile.TemporaryDirectory() as tmp:
proton, compat, windows = self.prefix(tmp)
(windows / "system32/UCRTBASE.DLL").write_bytes(pe_image(0x8664))
(windows / "system32/rpcrt4.dll").write_bytes(pe_image(0xAA64))
fixed = self.run_repair(compat, proton)
self.assertEqual(["system32/UCRTBASE.DLL (machine 0x8664)"], fixed)
self.assertTrue((windows / "system32/UCRTBASE.DLL").is_symlink())
self.assertEqual(0xAA64, MODULE["pe_machine"](windows / "system32/UCRTBASE.DLL"))
self.assertEqual(0x8664, MODULE["pe_machine"](windows / "system32/UCRTBASE.DLL.foreign-arch"))
self.assertFalse((windows / "system32/rpcrt4.dll").is_symlink())
self.assertEqual([], self.run_repair(compat, proton))
def test_64_bit_dll_in_syswow64_is_replaced(self):
with tempfile.TemporaryDirectory() as tmp:
proton, compat, windows = self.prefix(tmp)
(windows / "syswow64/ucrtbase.dll").write_bytes(pe_image(0xAA64))
self.assertEqual(["syswow64/ucrtbase.dll (machine 0xaa64)"], self.run_repair(compat, proton))
self.assertEqual(0x14C, MODULE["pe_machine"](windows / "syswow64/ucrtbase.dll"))
def test_matching_ec_symlinked_unknown_and_non_pe_files_are_left_alone(self):
with tempfile.TemporaryDirectory() as tmp:
proton, compat, windows = self.prefix(tmp)
system32 = windows / "system32"
(system32 / "ucrtbase.dll").write_bytes(pe_image(0xA641))
(system32 / "userlib.dll").write_bytes(pe_image(0x8664))
(system32 / "rpcrt4.dll").write_bytes(b"not a pe file")
self.assertEqual([], self.run_repair(compat, proton))
self.assertFalse((system32 / "ucrtbase.dll").is_symlink())
self.assertEqual(0x8664, MODULE["pe_machine"](system32 / "userlib.dll"))
def test_final_arm64ec_image_is_not_repaired_as_a_foreign_x64_dll(self):
with tempfile.TemporaryDirectory() as tmp:
proton, compat, windows = self.prefix(tmp)
dll = windows / "system32/ucrtbase.dll"
dll.write_bytes(arm64ec_pe_image())
self.assertEqual(0x8664, MODULE["pe_machine"](dll))
self.assertTrue(MODULE["PE"]["machine_compatible"](dll, frozenset((0xAA64, 0xA641))))
self.assertFalse(MODULE["PE"]["machine_compatible"](dll, frozenset((0x8664,))))
self.assertEqual([], self.run_repair(compat, proton))
self.assertFalse(dll.is_symlink())
def test_component_owned_and_native_first_foreign_dlls_are_still_replaced(self):
with tempfile.TemporaryDirectory() as tmp:
proton, compat, windows = self.prefix(tmp)
(windows / "system32/ucrtbase.dll").write_bytes(pe_image(0x8664))
(windows / "system32/rpcrt4.dll").write_bytes(pe_image(0x8664))
state = {"files": {"drive_c/windows/system32/ucrtbase.dll": {}}}
(compat / ".droiddeck-wincomponents.json").write_text(json.dumps(state))
reg = 'WINE REGISTRY Version 2\n\n[Software\\\\Wine\\\\DllOverrides] 1\n"RPCRT4.DLL"="native,builtin"\n'
(compat / "pfx/user.reg").write_text(reg)
env = {"WINEDLLOVERRIDES": "ucrtbase=n,b"}
self.assertEqual(["system32/rpcrt4.dll (machine 0x8664)", "system32/ucrtbase.dll (machine 0x8664)"],
sorted(MODULE["repair_foreign_system_dlls"](str(compat), str(proton), env)))
self.assertEqual(0xAA64, MODULE["pe_machine"](windows / "system32/ucrtbase.dll"))
self.assertEqual(0xAA64, MODULE["pe_machine"](windows / "system32/rpcrt4.dll"))
def test_newest_foreign_copy_is_the_only_backup_and_failed_final_replace_restores_it(self):
with tempfile.TemporaryDirectory() as tmp:
proton, compat, windows = self.prefix(tmp)
dll = windows / "system32/ucrtbase.dll"
dll.write_bytes(pe_image(0x8664))
backup = windows / "system32/ucrtbase.dll.foreign-arch"
backup.write_bytes(pe_image(0x1234))
self.assertEqual(["system32/ucrtbase.dll (machine 0x8664)"], self.run_repair(compat, proton))
self.assertEqual(0x1234, MODULE["pe_machine"](backup))
self.assertEqual(0x8664, MODULE["pe_machine"](backup.with_name(backup.name + ".1")))
self.assertEqual(0xAA64, MODULE["pe_machine"](dll))
with tempfile.TemporaryDirectory() as tmp:
proton, compat, windows = self.prefix(tmp)
dll = windows / "system32/ucrtbase.dll"
dll.write_bytes(pe_image(0x8664))
backup = windows / "system32/ucrtbase.dll.foreign-arch"
backup.write_bytes(pe_image(0x1234))
real = os.replace
def flaky_with_backup(src, dst):
if str(src).endswith(".bl-new"):
raise OSError("boom")
return real(src, dst)
with mock.patch("os.replace", side_effect=flaky_with_backup):
self.assertEqual([], self.run_repair(compat, proton))
self.assertEqual(0x8664, MODULE["pe_machine"](dll))
self.assertFalse((windows / "system32/ucrtbase.dll.foreign-arch.1").exists())
with tempfile.TemporaryDirectory() as tmp:
proton, compat, windows = self.prefix(tmp)
dll = windows / "system32/ucrtbase.dll"
dll.write_bytes(pe_image(0x8664))
real = os.replace
def flaky(src, dst):
if str(src).endswith(".bl-new"):
raise OSError("boom")
return real(src, dst)
with mock.patch("os.replace", side_effect=flaky):
self.assertEqual([], self.run_repair(compat, proton))
self.assertEqual(0x8664, MODULE["pe_machine"](dll))
self.assertFalse(dll.is_symlink())
self.assertFalse((windows / "system32/ucrtbase.dll.foreign-arch").exists())
self.assertFalse((windows / "system32/ucrtbase.dll.bl-new").is_symlink())
def test_non_arm64_proton_and_missing_prefix_are_untouched(self):
with tempfile.TemporaryDirectory() as tmp:
proton, compat, windows = self.prefix(tmp, arm=False)
(windows / "system32/ucrtbase.dll").write_bytes(pe_image(0x8664))
self.assertEqual([], self.run_repair(compat, proton))
self.assertEqual(0x8664, MODULE["pe_machine"](windows / "system32/ucrtbase.dll"))
self.assertEqual([], self.run_repair(Path(tmp) / "missing", proton))
if __name__ == "__main__":
unittest.main()
+129 -3
View File
@@ -35,8 +35,134 @@ class WinComponentsTest(unittest.TestCase):
def pick(self, *ids):
self.selection.write_text(json.dumps({"version": 1, "games": {"2571976725": list(ids)}}))
def apply(self):
return MODULE["apply"](self.compat, "2571976725", self.selection, self.store)
def apply(self, proton=None, env=None):
return MODULE["apply"](self.compat, "2571976725", self.selection, self.store, proton=proton, env=env)
def proton(self, arm=True):
root = Path(self.tmp.name) / ("arm-proton" if arm else "x64-proton")
(root / "files/lib/wine" / ("aarch64-unix" if arm else "x86_64-unix")).mkdir(parents=True)
return root / "proton"
def pe_image(self, machine):
head = bytearray(64)
head[:2] = b"MZ"
head[60:64] = (64).to_bytes(4, "little")
return bytes(head) + b"PE\0\0" + machine.to_bytes(2, "little") + b"\0" * 32
def arm64ec_pe_image(self):
image = bytearray(0x400)
image[:2] = b"MZ"
image[60:64] = (64).to_bytes(4, "little")
image[64:68] = b"PE\0\0"
coff = 68
image[coff:coff + 2] = (0x8664).to_bytes(2, "little")
image[coff + 2:coff + 4] = (1).to_bytes(2, "little")
image[coff + 16:coff + 18] = (240).to_bytes(2, "little")
optional = coff + 20
image[optional:optional + 2] = (0x20B).to_bytes(2, "little")
image[optional + 108:optional + 112] = (16).to_bytes(4, "little")
directory = optional + 112 + 10 * 8
image[directory:directory + 4] = (0x1000).to_bytes(4, "little")
image[directory + 4:directory + 8] = (0xD0).to_bytes(4, "little")
section = optional + 240
image[section + 8:section + 12] = (0x200).to_bytes(4, "little")
image[section + 12:section + 16] = (0x1000).to_bytes(4, "little")
image[section + 16:section + 20] = (0x200).to_bytes(4, "little")
image[section + 20:section + 24] = (0x200).to_bytes(4, "little")
image[0x200:0x204] = (0xD0).to_bytes(4, "little")
image[0x2C8:0x2D0] = (0x10000000).to_bytes(8, "little")
return bytes(image)
def test_foreign_component_dll_is_skipped_and_override_dropped(self):
proton = self.proton()
self.component("vc", ["ucrtbase"], {"system32/ucrtbase.dll": self.pe_image(0x8664)})
self.pick("vc")
names, _ = self.apply(proton)
self.assertEqual(names, [])
self.assertFalse((self.pfx / "system32/ucrtbase.dll").exists())
names, notes = self.apply(proton)
self.assertEqual(names, [])
self.assertEqual(notes, [])
def test_component_final_arm64ec_dll_is_placed_and_keeps_its_override(self):
proton = self.proton()
self.component("vc", ["ucrtbase"], {"system32/ucrtbase.dll": self.arm64ec_pe_image()})
self.pick("vc")
names, _ = self.apply(proton)
self.assertEqual(names, ["ucrtbase"])
self.assertEqual(MODULE["pe_machine"](self.pfx / "system32/ucrtbase.dll"), 0x8664)
def test_compatible_copy_keeps_override_when_other_architecture_copy_is_skipped(self):
proton = self.proton()
self.component("vc", ["ucrtbase"], {
"system32/ucrtbase.dll": self.pe_image(0x8664),
"syswow64/ucrtbase.dll": self.pe_image(0x14C),
})
self.pick("vc")
names, _ = self.apply(proton)
self.assertEqual(names, ["ucrtbase"])
self.assertFalse((self.pfx / "system32/ucrtbase.dll").exists())
self.assertEqual(MODULE["pe_machine"](self.pfx / "syswow64/ucrtbase.dll"), 0x14C)
def test_existing_foreign_component_dll_is_removed_for_repair(self):
proton = self.proton()
self.component("vc", ["ucrtbase"], {"system32/ucrtbase.dll": self.pe_image(0x8664)})
self.pick("vc")
self.pfx.joinpath("system32").mkdir(parents=True)
self.pfx.joinpath("system32/ucrtbase.dll").write_bytes(self.pe_image(0x8664))
self.compat.joinpath(".droiddeck-wincomponents.json").write_text(json.dumps({
"files": {"drive_c/windows/system32/ucrtbase.dll": "vc"}, "components": ["vc"]}))
self.apply(proton)
self.assertFalse((self.pfx / "system32/ucrtbase.dll").exists())
self.assertEqual([], GAME_ENV["repair_foreign_system_dlls"](str(self.compat), str(proton)))
self.assertEqual([], GAME_ENV["repair_foreign_system_dlls"](str(self.compat), str(proton)))
def test_foreign_component_restore_does_not_override_repair_with_native_override(self):
proton = self.proton()
builtin = Path(self.tmp.name) / "arm-proton/files/lib/wine/aarch64-windows/ucrtbase.dll"
builtin.parent.mkdir(parents=True, exist_ok=True)
builtin.write_bytes(self.pe_image(0xAA64))
system32 = self.pfx / "system32"
system32.mkdir(parents=True)
dll = system32 / "ucrtbase.dll"
dll.write_bytes(self.pe_image(0x8664))
backup = self.compat / ".droiddeck-wincomponents-backup/drive_c/windows/system32/ucrtbase.dll"
backup.parent.mkdir(parents=True)
backup.write_bytes(self.pe_image(0x8664))
self.compat.joinpath(".droiddeck-wincomponents.json").write_text(json.dumps({
"files": {"drive_c/windows/system32/ucrtbase.dll": "vc"}, "components": ["vc"]}))
self.compat.joinpath("pfx/user.reg").parent.mkdir(parents=True, exist_ok=True)
self.compat.joinpath("pfx/user.reg").write_text(
'WINE REGISTRY Version 2\n\n[Software\\\\Wine\\\\DllOverrides] 1\n"ucrtbase"="native,builtin"\n')
fixed = GAME_ENV["repair_foreign_system_dlls"](str(self.compat), str(proton))
self.assertEqual(["system32/ucrtbase.dll (machine 0x8664)"], fixed)
self.apply(proton)
self.assertTrue(dll.is_symlink())
self.assertEqual(0xAA64, MODULE["pe_machine"](dll))
def test_matching_component_dll_is_placed(self):
proton = self.proton()
self.component("vc", ["ucrtbase"], {"system32/ucrtbase.dll": self.pe_image(0xAA64)})
self.pick("vc")
names, _ = self.apply(proton)
self.assertEqual(names, ["ucrtbase"])
self.assertEqual(MODULE["pe_machine"](self.pfx / "system32/ucrtbase.dll"), 0xAA64)
def test_x64_proton_places_x64_component_dll(self):
proton = self.proton(arm=False)
self.component("vc", ["ucrtbase"], {"system32/ucrtbase.dll": self.pe_image(0x8664)})
self.pick("vc")
names, _ = self.apply(proton)
self.assertEqual(names, ["ucrtbase"])
self.assertEqual(MODULE["pe_machine"](self.pfx / "system32/ucrtbase.dll"), 0x8664)
def test_arm_proton_with_x64_selection_places_x64_component_dll(self):
proton = self.proton()
self.component("vc", ["ucrtbase"], {"system32/ucrtbase.dll": self.pe_image(0x8664)})
self.pick("vc")
names, _ = self.apply(proton, {"PROTON_USE_ARM64": "0"})
self.assertEqual(names, ["ucrtbase"])
self.assertEqual(MODULE["pe_machine"](self.pfx / "system32/ucrtbase.dll"), 0x8664)
def test_places_files_before_the_prefix_exists_and_returns_overrides(self):
self.component("openal", ["openal32"], {"syswow64/OpenAL32.dll": b"x86", "system32/OpenAL32.dll": b"x64"})
@@ -235,7 +361,7 @@ class WinComponentsTest(unittest.TestCase):
self.pick("openal")
globals_ = GAME_ENV["win_components"].__globals__
real = runpy.run_path(str(BIN / "droiddeck-wincomponents"))
real["apply"].__defaults__ = (self.selection, self.store, self.selection.parent / "no-pending.json", None)
real["apply"].__defaults__ = (self.selection, self.store, self.selection.parent / "no-pending.json", None, None, None)
with mock.patch.dict(globals_, {"runpy": mock.Mock(run_path=lambda _path: real)}):
self.assertEqual(GAME_ENV["win_components"](str(self.compat) + "/", "2571976725"), "openal32=n,b")
self.assertEqual(GAME_ENV["win_components"]("", "2571976725"), "")