Skip to content

Commit f4fb158

Browse files
authored
Add functionality to use winetricks (#109)
* umu_proton: add proton class * umu_util: add winetricks functionality * umu_run: add checks when using winetricks * umu_run: set environment variables for winetricks * umu_run: config winetricks to run unattended * umu_run: change directory when using winetricks * umu_run: fix exception when passing no winetricks verbs * umu_run: set WINEDLLPATH * umu_run: fix exit 3 status code for winetricks * umu_run: delete winetricks and protonfix paths * umu_run: update comments * umu_util: add missing type * umu_run: fix not setting EXE and STEAM_COMPAT_INSTALL_PATH - Without these variables set, winetricks will be missing and will result in a 'ShellExecuteEx failed: File not found' error. * umu_util: allow processing winetricks verbs in bulk - Allows passing verbs separated by spaces to apply verbs in bulk to the prefix * umu_run: update logic when swapping option and verb * Ruff lint * umu_run: respect order of winetricks verbs * umu_run: add winetricks as positional argument * umu_run: prefer surrounding verb with quotes * umu_run: update comment * umu_run: prefer assigning a new list than list.insert * umu_util: fix bug when parsing verbs * umu_util: update logic * umu_util: rename function * umu_util: add checks for is_installed_verb * umu_test: add test for is_installed_verb * umu_test: add test for is_winetricks_verb * umu_test: add tests when passing winetricks as positional argument * docs: add example of installing winetricks verbs * umu_util: fix logic when checking winetricks verbs - It doesn't make sense to check if the value in the log file is a verb and not the input. * umu_util: refactor is_winetricks_verb to log messages * umu_run: remove error messages for winetricks * umu_util: always process in bulk * umu_util: always pass verbs as a list * umu_test: update tests * Ruff lint * umu_util: fix AttributeError when checking winetricks verbs * umu_test: update tests * docs: update example
1 parent 4618ded commit f4fb158

File tree

5 files changed

+408
-6
lines changed

5 files changed

+408
-6
lines changed

docs/umu.1.scd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ $ WINEPREFIX=~/.wine GAMEID=0 PROTONPATH=GE-Proton9-1 umu-run ~/foo.exe
118118
$ WINEPREFIX=~/.wine GAMEID=0 PROTONPATH=GE-Proton umu-run ~/foo.exe
119119
```
120120

121+
*Example 11. Run winetricks verbs*
122+
123+
```
124+
$ GAMEID=0 PROTONPATH=GE-Proton umu-run winetricks quartz wmp11 qasf
125+
```
126+
121127
# SEE ALSO
122128

123129
_umu_(5)

umu/umu_proton.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@
2525
tar_filter: Callable[[str, str], TarInfo] = None
2626

2727

28+
class Proton:
29+
"""Model paths to relevant files and directories for Proton."""
30+
31+
def __init__(self, base_dir: str) -> None: # noqa: D107
32+
self.base_dir = base_dir + "/"
33+
self.dist_dir = self.path("files/")
34+
self.bin_dir = self.path("files/bin/")
35+
self.lib_dir = self.path("files/lib/")
36+
self.lib64_dir = self.path("files/lib64/")
37+
self.version_file = self.path("version")
38+
self.wine_bin = self.bin_dir + "wine"
39+
self.wine64_bin = self.bin_dir + "wine64"
40+
self.wineserver_bin = self.bin_dir + "wineserver"
41+
42+
def path(self, dir: str) -> str: # noqa: D102
43+
return self.base_dir + dir
44+
45+
2846
def get_umu_proton(
2947
env: dict[str, str], thread_pool: ThreadPoolExecutor
3048
) -> dict[str, str]:

umu/umu_run.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
)
2727
from umu_log import CustomFormatter, console_handler, log
2828
from umu_plugins import set_env_toml
29-
from umu_proton import get_umu_proton
29+
from umu_proton import Proton, get_umu_proton
3030
from umu_runtime import setup_umu
31-
from umu_util import get_libc
31+
from umu_util import get_libc, is_installed_verb, is_winetricks_verb
3232

3333
THREAD_POOL: ThreadPoolExecutor = ThreadPoolExecutor()
3434

@@ -46,11 +46,30 @@ def parse_args() -> Namespace | tuple[str, list[str]]: # noqa: D103
4646
parser.add_argument(
4747
"--config", help=("path to TOML file (requires Python 3.11+)")
4848
)
49+
parser.add_argument(
50+
"winetricks",
51+
help=("run winetricks (requires UMU-Proton or GE-Proton)"),
52+
nargs="?",
53+
default=None,
54+
)
4955

5056
if not sys.argv[1:]:
5157
parser.print_help(sys.stderr)
5258
sys.exit(1)
5359

60+
# Winetricks
61+
# Exit if no winetricks verbs were passed
62+
if sys.argv[1].endswith("winetricks") and not sys.argv[2:]:
63+
err: str = "No winetricks verb specified"
64+
log.error(err)
65+
sys.exit(1)
66+
67+
# Exit if argument is not a verb
68+
if sys.argv[1].endswith("winetricks") and not is_winetricks_verb(
69+
sys.argv[2:]
70+
):
71+
sys.exit(1)
72+
5473
if sys.argv[1:][0] in opt_args:
5574
return parser.parse_args(sys.argv[1:])
5675

@@ -80,8 +99,6 @@ def set_log() -> None:
8099
log.addHandler(console_handler)
81100
log.setLevel(level=DEBUG)
82101

83-
os.environ.pop("UMU_LOG")
84-
85102

86103
def setup_pfx(path: str) -> None:
87104
"""Create a symlink to the WINE prefix and tracked_files file."""
@@ -203,6 +220,21 @@ def set_env(
203220
env["EXE"] = ""
204221
env["STEAM_COMPAT_INSTALL_PATH"] = ""
205222
env["PROTON_VERB"] = "waitforexitandrun"
223+
elif isinstance(args, tuple) and args[0] == "winetricks":
224+
# Make an absolute path to winetricks that is within GE-Proton or
225+
# UMU-Proton, which includes the dependencies bundled within the
226+
# protonfixes directory. Fixes exit 3 status codes after applying
227+
# winetricks verbs
228+
bin: str = (
229+
Path(env["PROTONPATH"], "protonfixes", "winetricks")
230+
.expanduser()
231+
.resolve(strict=True)
232+
.as_posix()
233+
)
234+
log.debug("EXE: %s -> %s", args[0], bin)
235+
args: tuple[str, list[str]] = (bin, args[1])
236+
env["EXE"] = bin
237+
env["STEAM_COMPAT_INSTALL_PATH"] = Path(env["EXE"]).parent.as_posix()
206238
elif isinstance(args, tuple):
207239
try:
208240
env["EXE"] = (
@@ -257,6 +289,24 @@ def set_env(
257289
# Game drive
258290
enable_steam_game_drive(env)
259291

292+
# Winetricks
293+
if env.get("EXE").endswith("winetricks"):
294+
proton: Proton = Proton(os.environ["PROTONPATH"])
295+
env["WINE"] = proton.wine_bin
296+
env["WINELOADER"] = proton.wine_bin
297+
env["WINESERVER"] = proton.wineserver_bin
298+
env["WINETRICKS_LATEST_VERSION_CHECK"] = "disabled"
299+
env["LD_PRELOAD"] = ""
300+
env["WINEDLLPATH"] = ":".join(
301+
[
302+
Path(proton.lib_dir, "wine").as_posix(),
303+
Path(proton.lib64_dir, "wine").as_posix(),
304+
]
305+
)
306+
env["WINETRICKS_SUPER_QUIET"] = (
307+
"" if os.environ.get("UMU_LOG") == "debug" else "1"
308+
)
309+
260310
return env
261311

262312

@@ -338,6 +388,12 @@ def build_command(
338388
err: str = "The following file was not found in PROTONPATH: proton"
339389
raise FileNotFoundError(err)
340390

391+
# Configure winetricks to not be prompted for any windows
392+
if env.get("EXE").endswith("winetricks") and opts:
393+
# The position of arguments matter for winetricks
394+
# Usage: ./winetricks [options] [command|verb|path-to-verb] ...
395+
opts = ["-q", *opts]
396+
341397
if opts:
342398
command.extend(
343399
[
@@ -378,6 +434,7 @@ def run_command(command: list[str]) -> int:
378434
proc: Popen = None
379435
ret: int = 0
380436
libc: str = get_libc()
437+
cwd: str = ""
381438

382439
if not command:
383440
err: str = f"Command list is empty or None: {command}"
@@ -386,6 +443,12 @@ def run_command(command: list[str]) -> int:
386443
if not libc:
387444
log.warning("Will not set subprocess as subreaper")
388445

446+
# For winetricks, change directory to $PROTONPATH/protonfixes
447+
if os.environ.get("EXE").endswith("winetricks"):
448+
cwd = Path(os.environ.get("PROTONPATH"), "protonfixes").as_posix()
449+
else:
450+
cwd = Path.cwd().as_posix()
451+
389452
# Create a subprocess but do not set it as subreaper
390453
# Unnecessary in a Flatpak and prctl() will fail if libc could not be found
391454
if FLATPAK_PATH or not libc:
@@ -408,6 +471,7 @@ def run_command(command: list[str]) -> int:
408471
command,
409472
start_new_session=True,
410473
preexec_fn=lambda: prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0, 0),
474+
cwd=cwd,
411475
)
412476
ret = proc.wait()
413477
log.debug("Child %s exited with wait status: %s", proc.pid, ret)
@@ -441,7 +505,7 @@ def main() -> int: # noqa: D103
441505
"UMU_ZENITY": "",
442506
}
443507
command: list[str] = []
444-
opts: list[str] = None
508+
opts: list[str] = []
445509
root: Path = Path(__file__).resolve(strict=True).parent
446510
future: Future = None
447511
args: Namespace | tuple[str, list[str]] = parse_args()
@@ -519,6 +583,12 @@ def main() -> int: # noqa: D103
519583
future.result()
520584
THREAD_POOL.shutdown()
521585

586+
# Exit if the winetricks verb is already installed to avoid reapplying it
587+
if env.get("EXE").endswith("winetricks") and is_installed_verb(
588+
opts, Path(env.get("WINEPREFIX"))
589+
):
590+
sys.exit(1)
591+
522592
# Run
523593
build_command(env, UMU_LOCAL, command, opts)
524594
log.debug("%s", command)

0 commit comments

Comments
 (0)