Fixed some proton 7 path issues and the override in settings.

This commit is contained in:
2026-09-01 19:07:00 +02:00
parent d819809197
commit 78dade0d86
2 changed files with 123 additions and 40 deletions

View File

@@ -11,6 +11,7 @@ import shutil
from typing import Mapping from typing import Mapping
from .launcher import find_wine_tools from .launcher import find_wine_tools
from .runtime import find_steam_proton7
ARCH_FAMILY_DISTRIBUTIONS = frozenset( ARCH_FAMILY_DISTRIBUTIONS = frozenset(
@@ -176,19 +177,7 @@ def _package_hint(distribution: str) -> str | None:
def _has_steam_proton7(environment: Mapping[str, str]) -> bool: def _has_steam_proton7(environment: Mapping[str, str]) -> bool:
home_value = environment.get("HOME") return find_steam_proton7(environment) is not None
if not home_value:
return False
home = Path(home_value)
for steam_root in (
home / ".local/share/Steam",
home / ".steam/steam",
home / "snap/steam/common/.local/share/Steam",
home / ".var/app/com.valvesoftware.Steam/.local/share/Steam",
):
if (steam_root / "steamapps/common/Proton 7.0/proton").is_file():
return True
return False
def _platform_optional_tools(distribution: str, *, architecture: str) -> list[ToolStatus]: def _platform_optional_tools(distribution: str, *, architecture: str) -> list[ToolStatus]:

View File

@@ -9,6 +9,7 @@ import json
import os import os
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
import platform import platform
import re
import shlex import shlex
import shutil import shutil
import sys import sys
@@ -36,6 +37,12 @@ CURRENT_UMU_DISTRIBUTIONS = frozenset(
{"arch", "cachyos", "endeavouros", "garuda", "manjaro"} {"arch", "cachyos", "endeavouros", "garuda", "manjaro"}
) )
GAME_RUNTIME_CONFIGURATION_VERSION = 1 GAME_RUNTIME_CONFIGURATION_VERSION = 1
PROTON7_RELATIVE_PATH = Path("steamapps/common/Proton 7.0/proton")
STEAM_LIBRARY_FOLDERS_RELATIVE_PATH = Path("steamapps/libraryfolders.vdf")
STEAM_LIBRARY_FOLDERS_MAX_BYTES = 4 * 1024 * 1024
_VDF_QUOTED_PAIR = re.compile(
r'"((?:\\.|[^"\\])*)"\s*"((?:\\.|[^"\\])*)"'
)
class RuntimeErrorPT(RuntimeError): class RuntimeErrorPT(RuntimeError):
@@ -80,7 +87,7 @@ def select_runtime(
proton = Path(settings.proton_path).expanduser().resolve() proton = Path(settings.proton_path).expanduser().resolve()
if not proton.is_file(): if not proton.is_file():
raise RuntimeErrorPT(f"configured Proton script does not exist: {proton}") raise RuntimeErrorPT(f"configured Proton script does not exist: {proton}")
steam_root = _steam_root_for_proton(proton) steam_root = _steam_root_for_proton(proton, env)
if settings.runtime == "proton7-container": if settings.runtime == "proton7-container":
if not umu: if not umu:
raise RuntimeErrorPT( raise RuntimeErrorPT(
@@ -137,33 +144,108 @@ def select_runtime(
def find_steam_proton7( def find_steam_proton7(
environment: Mapping[str, str] | None = None, environment: Mapping[str, str] | None = None,
) -> tuple[Path, Path] | None: ) -> tuple[Path, Path] | None:
"""Find Proton 7 in a Steam client root or one of its registered libraries.
The returned Steam root is always the client installation, even when the
Proton tool itself lives in a secondary library. Proton 7 uses that client
root for Steam's bundled media compatibility libraries.
"""
env = os.environ if environment is None else environment env = os.environ if environment is None else environment
home_value = env.get("HOME") for steam_root in _steam_client_roots(env):
if not home_value: for library_root in _steam_library_roots(steam_root):
return None proton = library_root / PROTON7_RELATIVE_PATH
home = Path(home_value).expanduser() if proton.is_file():
roots: list[Path] = [] return proton.resolve(), steam_root
explicit = env.get("STEAM_COMPAT_CLIENT_INSTALL_PATH") return None
def _steam_client_roots(environment: Mapping[str, str]) -> tuple[Path, ...]:
candidates: list[Path] = []
explicit = environment.get("STEAM_COMPAT_CLIENT_INSTALL_PATH")
if explicit: if explicit:
roots.append(Path(explicit).expanduser()) candidates.append(Path(explicit).expanduser())
roots.extend( home_value = environment.get("HOME")
( if home_value:
home / ".local/share/Steam", home = Path(home_value).expanduser()
home / ".steam/steam", candidates.extend(
home / "snap/steam/common/.local/share/Steam", (
home / ".var/app/com.valvesoftware.Steam/.local/share/Steam", home / ".local/share/Steam",
home / ".steam/steam",
home / ".steam/root",
home / ".steam/debian-installation",
home / "snap/steam/common/.local/share/Steam",
home / "snap/steam/common/.steam/steam",
home / ".var/app/com.valvesoftware.Steam/.local/share/Steam",
)
) )
return _unique_resolved_paths(candidates)
def _steam_library_roots(steam_root: Path) -> tuple[Path, ...]:
registered = _read_steam_library_folders(
steam_root / STEAM_LIBRARY_FOLDERS_RELATIVE_PATH
) )
return _unique_resolved_paths((steam_root, *registered))
def _read_steam_library_folders(path: Path) -> tuple[Path, ...]:
"""Read current and legacy Steam libraryfolders.vdf path records."""
try:
with path.open("rb") as handle:
content = handle.read(STEAM_LIBRARY_FOLDERS_MAX_BYTES + 1)
except OSError:
return ()
if len(content) > STEAM_LIBRARY_FOLDERS_MAX_BYTES:
return ()
text = content.decode("utf-8-sig", errors="replace")
result: list[Path] = []
for raw_key, raw_value in _VDF_QUOTED_PAIR.findall(text):
key = _decode_vdf_string(raw_key)
value = _decode_vdf_string(raw_value)
# Current files use a named "path" property. Older Steam clients used
# numeric top-level keys, which are distinguishable from app-size pairs
# because a Linux library path must be absolute.
if key.casefold() != "path" and not key.isdecimal():
continue
if not value or "\x00" in value:
continue
library = Path(value).expanduser()
if library.is_absolute():
result.append(library)
return tuple(result)
def _decode_vdf_string(value: str) -> str:
result: list[str] = []
index = 0
while index < len(value):
character = value[index]
if character == "\\" and index + 1 < len(value):
next_character = value[index + 1]
if next_character in {'"', "\\"}:
result.append(next_character)
index += 2
continue
result.append(character)
index += 1
return "".join(result)
def _unique_resolved_paths(paths: Sequence[Path]) -> tuple[Path, ...]:
result: list[Path] = []
seen: set[Path] = set() seen: set[Path] = set()
for root in roots: for path in paths:
resolved = root.resolve() try:
resolved = path.resolve()
except (OSError, RuntimeError):
continue
if resolved in seen: if resolved in seen:
continue continue
seen.add(resolved) seen.add(resolved)
proton = resolved / "steamapps/common/Proton 7.0/proton" result.append(resolved)
if proton.is_file(): return tuple(result)
return proton, resolved
return None
def prepare_game_runtime( def prepare_game_runtime(
@@ -653,15 +735,27 @@ def _ensure_proton_game_link(paths: AppPaths) -> None:
link.symlink_to(paths.runtime_view, target_is_directory=True) link.symlink_to(paths.runtime_view, target_is_directory=True)
def _steam_root_for_proton(proton: Path) -> Path: def _steam_root_for_proton(
parts = proton.parts proton: Path,
try: environment: Mapping[str, str] | None = None,
index = parts.index("steamapps") ) -> Path:
except ValueError as exc: steamapps = next(
(parent for parent in proton.parents if parent.name.casefold() == "steamapps"),
None,
)
if steamapps is None:
raise RuntimeErrorPT( raise RuntimeErrorPT(
"configured Proton path must be inside a Steam steamapps directory" "configured Proton path must be inside a Steam steamapps directory"
) from exc )
return Path(*parts[:index]) library_root = steamapps.parent.resolve()
env = os.environ if environment is None else environment
for steam_root in _steam_client_roots(env):
if library_root in _steam_library_roots(steam_root):
return steam_root
# Preserve support for an otherwise valid, manually managed Steam layout.
# Without a registered client root, the library root is the best available
# compatibility value and matches the launcher's historical behaviour.
return library_root
def native_d3d_ready(prefix: Path) -> bool: def native_d3d_ready(prefix: Path) -> bool: