Fixed issues with encodings and wine / proton compatibilities.

This commit is contained in:
2026-08-30 00:08:41 +02:00
parent d1587b785a
commit bef8d04d72
13 changed files with 772 additions and 132 deletions

View File

@@ -34,6 +34,9 @@ WINETRICKS_CODEPAGE_PROBE = (
WINETRICKS_PROTON7_PROBE = 'winetricks_early_wine_arch cmd.exe /c "echo %$1%"'
RUNTIME_DEFAULT_SYNC = "runtime-default"
CACHYOS_MARKER = "cachyos"
CURRENT_UMU_DISTRIBUTIONS = frozenset(
{"arch", "cachyos", "endeavouros", "garuda", "manjaro"}
)
GAME_RUNTIME_CONFIGURATION_VERSION = 1
@@ -52,10 +55,30 @@ class RuntimeSelection:
def select_runtime(
settings: Settings,
environment: Mapping[str, str] | None = None,
*,
distribution_id: str | None = None,
kernel_release: str | None = None,
) -> RuntimeSelection:
env = os.environ if environment is None else environment
umu = shutil.which("umu-run", path=env.get("PATH"))
if settings.proton_path:
current_umu_host = prefers_current_umu_host(
distribution_id=distribution_id,
kernel_release=kernel_release,
)
auto_container = (
settings.runtime == "auto"
and bool(umu)
and not current_umu_host
)
if settings.runtime == "umu":
if umu:
return RuntimeSelection("umu", umu, None, None)
raise RuntimeErrorPT("UMU was requested but umu-run was not found")
if settings.proton_path and settings.runtime in {
"auto",
"proton7",
"proton7-container",
}:
proton = Path(settings.proton_path).expanduser().resolve()
if not proton.is_file():
raise RuntimeErrorPT(f"configured Proton script does not exist: {proton}")
@@ -71,7 +94,8 @@ def select_runtime(
str(proton),
str(steam_root),
)
if settings.runtime == "auto" and umu:
if auto_container:
assert umu is not None
return RuntimeSelection(
"proton7-container",
umu,
@@ -79,13 +103,17 @@ def select_runtime(
str(steam_root),
)
return RuntimeSelection("proton7", sys.executable, str(proton), str(steam_root))
# Current UMU avoids several rolling-distribution container/media failures and
# remains the broad Arch-family compatibility default. Direct Proton 7 is also
# known to authenticate on CachyOS, so this is a compatibility policy rather
# than an assertion that Proton 7 itself causes server-side Code 1/Code 3.
if settings.runtime == "auto" and current_umu_host and umu:
return RuntimeSelection("umu", umu, None, None)
if settings.runtime in {"auto", "proton7", "proton7-container"}:
proton_result = find_steam_proton7(env)
if proton_result is not None:
proton, steam_root = proton_result
if settings.runtime == "proton7-container" or (
settings.runtime == "auto" and umu
):
if settings.runtime == "proton7-container" or auto_container:
if not umu:
raise RuntimeErrorPT(
"containerized Proton 7 was requested but umu-run was not found"
@@ -377,6 +405,49 @@ def run_proton_prefix_command(
)
def run_runtime_prefix_command(
paths: AppPaths,
selection: RuntimeSelection,
runner: CommandRunner,
arguments: Sequence[str],
*,
check: bool = True,
timeout_seconds: float = 60.0,
) -> CommandOutcome:
"""Run a credential-free helper inside the selected game's Wine prefix."""
if not arguments:
raise ValueError("runtime-prefix command arguments cannot be empty")
if selection.kind == "umu":
command = [selection.executable, *arguments]
environment = _umu_environment(paths)
elif selection.kind == "proton7-container":
if not selection.proton_path or not selection.steam_root:
raise RuntimeErrorPT("incomplete Proton 7 runtime selection")
command = [selection.executable, *arguments]
environment = _umu_proton7_environment(paths, selection)
elif selection.kind == "proton7":
if not selection.proton_path or not selection.steam_root:
raise RuntimeErrorPT("incomplete Proton 7 runtime selection")
command = [
sys.executable,
"-m",
"ptlinux.proton_compat",
selection.proton_path,
"runinprefix",
*arguments,
]
environment = _proton_environment(paths, selection)
else:
raise RuntimeErrorPT(f"unsupported runtime kind: {selection.kind}")
return runner.run(
command,
env_overrides=environment,
timeout_seconds=timeout_seconds,
check=check,
)
def _runtime_python_path(existing: str | None) -> str:
"""Make the installed package importable after changing into runtime_view."""
package_root = str(Path(__file__).resolve().parent.parent)
@@ -455,6 +526,38 @@ def detect_distribution_id() -> str:
return value.strip().casefold()
def is_cachyos_host(
*,
distribution_id: str | None = None,
kernel_release: str | None = None,
) -> bool:
"""Identify CachyOS narrowly for runtime compatibility policy."""
distribution = detect_distribution_id() if distribution_id is None else distribution_id
kernel = platform.release() if kernel_release is None else kernel_release
return (
CACHYOS_MARKER in distribution.casefold()
or CACHYOS_MARKER in kernel.casefold()
)
def prefers_current_umu_host(
*,
distribution_id: str | None = None,
kernel_release: str | None = None,
) -> bool:
"""Use current UMU automatically on rolling Arch-family hosts."""
distribution = detect_distribution_id() if distribution_id is None else distribution_id
return (
distribution.strip().casefold() in CURRENT_UMU_DISTRIBUTIONS
or is_cachyos_host(
distribution_id=distribution,
kernel_release=kernel_release,
)
)
def resolve_proton_sync_mode(
configured: str,
*,
@@ -467,9 +570,10 @@ def resolve_proton_sync_mode(
raise ValueError(f"unsupported Proton synchronization mode: {configured}")
if configured != "auto":
return configured
distribution = detect_distribution_id() if distribution_id is None else distribution_id
kernel = platform.release() if kernel_release is None else kernel_release
if CACHYOS_MARKER in distribution.casefold() or CACHYOS_MARKER in kernel.casefold():
if is_cachyos_host(
distribution_id=distribution_id,
kernel_release=kernel_release,
):
return "esync"
return RUNTIME_DEFAULT_SYNC
@@ -552,21 +656,23 @@ def prepare_proton7_winetricks(
dry_run: bool = False,
) -> Path:
try:
content = source.read_text(encoding="utf-8")
content = source.read_bytes()
except OSError as exc:
raise RuntimeErrorPT(f"could not read Winetricks script {source}: {exc}") from exc
if WINETRICKS_CODEPAGE_PROBE not in content:
probe = WINETRICKS_CODEPAGE_PROBE.encode("utf-8")
replacement = WINETRICKS_PROTON7_PROBE.encode("utf-8")
if probe not in content:
return source
if dry_run:
return destination
destination.parent.mkdir(parents=True, exist_ok=True)
patched = content.replace(
WINETRICKS_CODEPAGE_PROBE,
WINETRICKS_PROTON7_PROBE,
probe,
replacement,
1,
)
temporary = destination.with_name(f".{destination.name}.{os.getpid()}.tmp")
temporary.write_text(patched, encoding="utf-8")
temporary.write_bytes(patched)
temporary.chmod(source.stat().st_mode & 0o777)
temporary.replace(destination)
return destination