mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
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:
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
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"), "")
|
||||
|
||||
Reference in new issue
Block a user