mirror of
https://github.com/tcsenpai/UWINE.git
synced 2025-06-06 11:35:20 +00:00

- Prioritize checking the Proton version first instead of including the logic as apart of fetching the Proton. This way, we also allow clients to assign other Proton forks such as GE-Proton instead of strictly ULWGL-Proton.
317 lines
10 KiB
Python
Executable File
317 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import argparse
|
|
from traceback import print_exception
|
|
from argparse import ArgumentParser, Namespace
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, Any, List, Set, Union, Tuple
|
|
from ulwgl_plugins import enable_steam_game_drive, set_env_toml
|
|
from re import match
|
|
import subprocess
|
|
from ulwgl_dl_util import get_ulwgl_proton
|
|
from ulwgl_consts import Level
|
|
from ulwgl_util import msg
|
|
from ulwgl_log import log, console_handler, debug_formatter
|
|
|
|
verbs: Set[str] = {
|
|
"waitforexitandrun",
|
|
"run",
|
|
"runinprefix",
|
|
"destroyprefix",
|
|
"getcompatpath",
|
|
"getnativepath",
|
|
}
|
|
|
|
|
|
def parse_args() -> Union[Namespace, Tuple[str, List[str]]]: # noqa: D103
|
|
opt_args: Set[str] = {"--help", "-h", "--config"}
|
|
exe: str = Path(__file__).name
|
|
usage: str = f"""
|
|
example usage:
|
|
GAMEID= {exe} /home/foo/example.exe
|
|
WINEPREFIX= GAMEID= {exe} /home/foo/example.exe
|
|
WINEPREFIX= GAMEID= PROTONPATH= {exe} /home/foo/example.exe
|
|
WINEPREFIX= GAMEID= PROTONPATH= {exe} /home/foo/example.exe -opengl
|
|
WINEPREFIX= GAMEID= PROTONPATH= {exe} ""
|
|
WINEPREFIX= GAMEID= PROTONPATH= PROTON_VERB= {exe} /home/foo/example.exe
|
|
WINEPREFIX= GAMEID= PROTONPATH= STORE= {exe} /home/foo/example.exe
|
|
ULWGL_LOG= GAMEID= {exe} /home/foo/example.exe
|
|
{exe} --config /home/foo/example.toml
|
|
"""
|
|
parser: ArgumentParser = argparse.ArgumentParser(
|
|
description="Unified Linux Wine Game Launcher",
|
|
epilog=usage,
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
)
|
|
parser.add_argument("--config", help="path to TOML file (requires Python 3.11)")
|
|
|
|
if not sys.argv[1:]:
|
|
err: str = "Please see project README.md for more info and examples.\nhttps://github.com/Open-Wine-Components/ULWGL-launcher"
|
|
parser.print_help(sys.stderr)
|
|
raise SystemExit(err)
|
|
|
|
if sys.argv[1:][0] in opt_args:
|
|
return parser.parse_args(sys.argv[1:])
|
|
|
|
if sys.argv[1] in verbs:
|
|
if "PROTON_VERB" not in os.environ:
|
|
os.environ["PROTON_VERB"] = sys.argv[1]
|
|
sys.argv.pop(1)
|
|
|
|
return sys.argv[1], sys.argv[2:]
|
|
|
|
|
|
def set_log() -> None:
|
|
"""Adjust the log level for the logger."""
|
|
levels: Set[str] = {"1", "warn", "debug"}
|
|
|
|
if os.environ["ULWGL_LOG"] not in levels:
|
|
return
|
|
|
|
if os.environ["ULWGL_LOG"] == "1":
|
|
# Show the envvars and command at this level
|
|
log.setLevel(level=Level.INFO.value)
|
|
elif os.environ["ULWGL_LOG"] == "warn":
|
|
log.setLevel(level=Level.WARNING.value)
|
|
elif os.environ["ULWGL_LOG"] == "debug":
|
|
# Show all logs
|
|
console_handler.setFormatter(debug_formatter)
|
|
log.addHandler(console_handler)
|
|
log.setLevel(level=Level.DEBUG.value)
|
|
|
|
os.environ.pop("ULWGL_LOG")
|
|
|
|
|
|
def setup_pfx(path: str) -> None:
|
|
"""Create a symlink to the WINE prefix and tracked_files file."""
|
|
pfx: Path = Path(path).joinpath("pfx").expanduser()
|
|
|
|
if pfx.is_symlink():
|
|
pfx.unlink()
|
|
|
|
if not pfx.is_dir():
|
|
pfx.symlink_to(Path(path).expanduser())
|
|
|
|
Path(path).joinpath("tracked_files").expanduser().touch()
|
|
|
|
|
|
def check_env(
|
|
env: Dict[str, str], toml: Dict[str, Any] = None
|
|
) -> Union[Dict[str, str], Dict[str, Any]]:
|
|
"""Before executing a game, check for environment variables and set them.
|
|
|
|
WINEPREFIX, GAMEID and PROTONPATH are strictly required.
|
|
"""
|
|
if "GAMEID" not in os.environ:
|
|
err: str = "Environment variable not set: GAMEID"
|
|
raise ValueError(err)
|
|
env["GAMEID"] = os.environ["GAMEID"]
|
|
|
|
if "WINEPREFIX" not in os.environ:
|
|
pfx: Path = Path.home().joinpath("Games/ULWGL/" + env["GAMEID"])
|
|
pfx.mkdir(parents=True, exist_ok=True)
|
|
os.environ["WINEPREFIX"] = pfx.as_posix()
|
|
if not Path(os.environ["WINEPREFIX"]).expanduser().is_dir():
|
|
pfx: Path = Path(os.environ["WINEPREFIX"])
|
|
pfx.mkdir(parents=True, exist_ok=True)
|
|
os.environ["WINEPREFIX"] = pfx.as_posix()
|
|
|
|
env["WINEPREFIX"] = os.environ["WINEPREFIX"]
|
|
|
|
# Proton Version
|
|
if (
|
|
"PROTONPATH" in os.environ
|
|
and os.environ["PROTONPATH"]
|
|
and Path(
|
|
"~/.local/share/Steam/compatibilitytools.d/" + os.environ["PROTONPATH"]
|
|
)
|
|
.expanduser()
|
|
.is_dir()
|
|
):
|
|
log.debug(msg("Proton version selected", Level.DEBUG))
|
|
os.environ["PROTONPATH"] = (
|
|
Path("~/.local/share/Steam/compatibilitytools.d")
|
|
.joinpath(os.environ["PROTONPATH"])
|
|
.expanduser()
|
|
.as_posix()
|
|
)
|
|
|
|
if "PROTONPATH" not in os.environ:
|
|
os.environ["PROTONPATH"] = ""
|
|
get_ulwgl_proton(env)
|
|
|
|
env["PROTONPATH"] = os.environ["PROTONPATH"]
|
|
|
|
# If download fails/doesn't exist in the system, raise an error
|
|
if not os.environ["PROTONPATH"]:
|
|
err: str = "Download failed.\nProton could not be found in cache or compatibilitytools.d\nPlease set $PROTONPATH or visit https://github.com/Open-Wine-Components/ULWGL-Proton/releases"
|
|
raise FileNotFoundError(err)
|
|
|
|
return env
|
|
|
|
|
|
def set_env(
|
|
env: Dict[str, str], args: Union[Namespace, Tuple[str, List[str]]]
|
|
) -> Dict[str, str]:
|
|
"""Set various environment variables for the Steam RT.
|
|
|
|
Filesystem paths will be formatted and expanded as POSIX
|
|
"""
|
|
# PROTON_VERB
|
|
# For invalid Proton verbs, just assign the waitforexitandrun
|
|
if "PROTON_VERB" in os.environ and os.environ["PROTON_VERB"] in verbs:
|
|
env["PROTON_VERB"] = os.environ["PROTON_VERB"]
|
|
else:
|
|
env["PROTON_VERB"] = "waitforexitandrun"
|
|
|
|
# EXE
|
|
# Empty string for EXE will be used to create a prefix
|
|
if isinstance(args, tuple) and isinstance(args[0], str) and not args[0]:
|
|
env["EXE"] = ""
|
|
env["STEAM_COMPAT_INSTALL_PATH"] = ""
|
|
env["PROTON_VERB"] = "waitforexitandrun"
|
|
elif isinstance(args, tuple):
|
|
env["EXE"] = Path(args[0]).expanduser().as_posix()
|
|
env["STEAM_COMPAT_INSTALL_PATH"] = Path(env["EXE"]).parent.as_posix()
|
|
else:
|
|
# Config branch
|
|
env["EXE"] = Path(env["EXE"]).expanduser().as_posix()
|
|
env["STEAM_COMPAT_INSTALL_PATH"] = Path(env["EXE"]).parent.as_posix()
|
|
|
|
if "STORE" in os.environ:
|
|
env["STORE"] = os.environ["STORE"]
|
|
|
|
# ULWGL_ID
|
|
env["ULWGL_ID"] = env["GAMEID"]
|
|
env["STEAM_COMPAT_APP_ID"] = "0"
|
|
|
|
if match(r"^ulwgl-[\d\w]+$", env["ULWGL_ID"]):
|
|
env["STEAM_COMPAT_APP_ID"] = env["ULWGL_ID"][env["ULWGL_ID"].find("-") + 1 :]
|
|
env["SteamAppId"] = env["STEAM_COMPAT_APP_ID"]
|
|
env["SteamGameId"] = env["SteamAppId"]
|
|
|
|
# PATHS
|
|
env["WINEPREFIX"] = Path(env["WINEPREFIX"]).expanduser().as_posix()
|
|
env["PROTONPATH"] = Path(env["PROTONPATH"]).expanduser().as_posix()
|
|
env["STEAM_COMPAT_DATA_PATH"] = env["WINEPREFIX"]
|
|
env["STEAM_COMPAT_SHADER_PATH"] = env["STEAM_COMPAT_DATA_PATH"] + "/shadercache"
|
|
env["STEAM_COMPAT_TOOL_PATHS"] = (
|
|
env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix()
|
|
)
|
|
env["STEAM_COMPAT_MOUNTS"] = env["STEAM_COMPAT_TOOL_PATHS"]
|
|
|
|
return env
|
|
|
|
|
|
def build_command(
|
|
env: Dict[str, str], command: List[str], opts: List[str] = None
|
|
) -> List[str]:
|
|
"""Build the command to be executed."""
|
|
paths: List[Path] = [
|
|
Path.home().joinpath(".local/share/ULWGL/ULWGL"),
|
|
Path(__file__).parent.joinpath("ULWGL"),
|
|
]
|
|
entry_point: str = ""
|
|
verb: str = env["PROTON_VERB"]
|
|
|
|
# Find the ULWGL script in $HOME/.local/share then cwd
|
|
for path in paths:
|
|
if path.is_file():
|
|
entry_point = path.as_posix()
|
|
break
|
|
|
|
# Raise an error if the _v2-entry-point cannot be found
|
|
if not entry_point:
|
|
home: str = Path.home().as_posix()
|
|
dir: str = Path(__file__).parent.as_posix()
|
|
msg: str = (
|
|
f"Path to _v2-entry-point cannot be found in: {home}/.local/share or {dir}"
|
|
)
|
|
raise FileNotFoundError(msg)
|
|
|
|
if not Path(env.get("PROTONPATH")).joinpath("proton").is_file():
|
|
err: str = "The following file was not found in PROTONPATH: proton"
|
|
raise FileNotFoundError(err)
|
|
|
|
command.extend([entry_point, "--verb", verb, "--"])
|
|
command.extend(
|
|
[
|
|
Path(env.get("PROTONPATH")).joinpath("proton").as_posix(),
|
|
verb,
|
|
env.get("EXE"),
|
|
]
|
|
)
|
|
|
|
if opts:
|
|
command.extend([*opts])
|
|
|
|
return command
|
|
|
|
|
|
def main() -> int: # noqa: D103
|
|
env: Dict[str, str] = {
|
|
"WINEPREFIX": "",
|
|
"GAMEID": "",
|
|
"PROTON_CRASH_REPORT_DIR": "/tmp/ULWGL_crashreports",
|
|
"PROTONPATH": "",
|
|
"STEAM_COMPAT_APP_ID": "",
|
|
"STEAM_COMPAT_TOOL_PATHS": "",
|
|
"STEAM_COMPAT_LIBRARY_PATHS": "",
|
|
"STEAM_COMPAT_MOUNTS": "",
|
|
"STEAM_COMPAT_INSTALL_PATH": "",
|
|
"STEAM_COMPAT_CLIENT_INSTALL_PATH": "",
|
|
"STEAM_COMPAT_DATA_PATH": "",
|
|
"STEAM_COMPAT_SHADER_PATH": "",
|
|
"FONTCONFIG_PATH": "",
|
|
"EXE": "",
|
|
"SteamAppId": "",
|
|
"SteamGameId": "",
|
|
"STEAM_RUNTIME_LIBRARY_PATH": "",
|
|
"STORE": "",
|
|
"PROTON_VERB": "",
|
|
"ULWGL_ID": "",
|
|
}
|
|
command: List[str] = []
|
|
args: Union[Namespace, Tuple[str, List[str]]] = parse_args()
|
|
opts: List[str] = None
|
|
|
|
if "ULWGL_LOG" in os.environ:
|
|
set_log()
|
|
|
|
if isinstance(args, Namespace) and getattr(args, "config", None):
|
|
set_env_toml(env, args)
|
|
else:
|
|
# Reference the game options
|
|
opts = args[1]
|
|
check_env(env)
|
|
|
|
setup_pfx(env["WINEPREFIX"])
|
|
set_env(env, args)
|
|
|
|
# Game Drive
|
|
enable_steam_game_drive(env)
|
|
|
|
# Set all environment variables
|
|
# NOTE: `env` after this block should be read only
|
|
for key, val in env.items():
|
|
log.info(msg(f"{key}={val}", Level.INFO))
|
|
os.environ[key] = val
|
|
|
|
build_command(env, command, opts)
|
|
log.debug(msg(command, Level.DEBUG))
|
|
return subprocess.run(command).returncode
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except KeyboardInterrupt:
|
|
# Until Reaper is part of the command sequence, spawned process may still be alive afterwards
|
|
log.warning(msg("Keyboard Interrupt", Level.WARNING))
|
|
sys.exit(1)
|
|
except Exception as e: # noqa: BLE001
|
|
print_exception(e)
|
|
sys.exit(1)
|