Fixed some proton 7 path issues and the override in settings.
This commit is contained in:
@@ -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]:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
proton = library_root / PROTON7_RELATIVE_PATH
|
||||||
|
if proton.is_file():
|
||||||
|
return proton.resolve(), steam_root
|
||||||
return None
|
return None
|
||||||
home = Path(home_value).expanduser()
|
|
||||||
roots: list[Path] = []
|
|
||||||
explicit = env.get("STEAM_COMPAT_CLIENT_INSTALL_PATH")
|
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 = Path(home_value).expanduser()
|
||||||
|
candidates.extend(
|
||||||
(
|
(
|
||||||
home / ".local/share/Steam",
|
home / ".local/share/Steam",
|
||||||
home / ".steam/steam",
|
home / ".steam/steam",
|
||||||
|
home / ".steam/root",
|
||||||
|
home / ".steam/debian-installation",
|
||||||
home / "snap/steam/common/.local/share/Steam",
|
home / "snap/steam/common/.local/share/Steam",
|
||||||
|
home / "snap/steam/common/.steam/steam",
|
||||||
home / ".var/app/com.valvesoftware.Steam/.local/share/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:
|
||||||
|
|||||||
Reference in New Issue
Block a user