From 582b83c12b64a82e9bf4e0a2656b300c6497e7f8 Mon Sep 17 00:00:00 2001
From: loathingKernel <142770+loathingKernel@users.noreply.github.com>
Date: Mon, 12 Feb 2024 18:30:23 +0200
Subject: [PATCH] WIP
---
.../tabs/games/game_info/cloud_saves.py | 15 +-
.../tabs/games/integrations/egl_sync_group.py | 11 +-
.../tabs/games/integrations/eos_group.py | 2 +-
.../tabs/settings/widgets/env_vars_model.py | 4 +-
.../tabs/settings/widgets/proton.py | 17 +-
.../tabs/settings/widgets/wrappers.py | 12 +-
rare/models/pathspec.py | 4 +-
rare/models/wrapper.py | 15 +-
rare/shared/rare_core.py | 12 +-
rare/shared/workers/install_info.py | 3 +-
rare/shared/workers/uninstall.py | 4 +-
rare/shared/workers/wine_resolver.py | 87 +++++----
rare/shared/wrappers.py | 6 +-
rare/utils/compat/__init__.py | 0
rare/utils/{runners => compat}/proton.py | 70 ++++----
rare/utils/compat/utils.py | 170 ++++++++++++++++++
rare/utils/{runners => compat}/wine.py | 0
rare/utils/config_helper.py | 24 +--
rare/utils/paths.py | 9 +
rare/utils/runners/__init__.py | 109 -----------
20 files changed, 338 insertions(+), 236 deletions(-)
create mode 100644 rare/utils/compat/__init__.py
rename rare/utils/{runners => compat}/proton.py (83%)
create mode 100644 rare/utils/compat/utils.py
rename rare/utils/{runners => compat}/wine.py (100%)
delete mode 100644 rare/utils/runners/__init__.py
diff --git a/rare/components/tabs/games/game_info/cloud_saves.py b/rare/components/tabs/games/game_info/cloud_saves.py
index 8e77f152..30d76f73 100644
--- a/rare/components/tabs/games/game_info/cloud_saves.py
+++ b/rare/components/tabs/games/game_info/cloud_saves.py
@@ -28,7 +28,7 @@ from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.widgets.loading_widget import LoadingWidget
from rare.widgets.side_tab import SideTabContents
-logger = getLogger("CloudWidget")
+logger = getLogger("CloudSaves")
class CloudSaves(QWidget, SideTabContents):
@@ -122,11 +122,11 @@ class CloudSaves(QWidget, SideTabContents):
except Exception as e:
logger.warning(str(e))
resolver = WineSavePathResolver(self.core, self.rgame)
- if not resolver.environ.get("WINEPREFIX"):
- del resolver
- self.cloud_save_path_edit.setText("")
- QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings")
- return
+ # if not resolver.environ.get("WINEPREFIX"):
+ # del resolver
+ # self.cloud_save_path_edit.setText("")
+ # QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings")
+ # return
self.cloud_save_path_edit.setText(self.tr("Loading..."))
self.cloud_save_path_edit.setDisabled(True)
self.compute_save_path_button.setDisabled(True)
@@ -139,7 +139,8 @@ class CloudSaves(QWidget, SideTabContents):
@pyqtSlot(str, str)
def __on_wine_resolver_result(self, path, app_name):
- logger.info(f"Wine resolver finished for {app_name}. Computed save path: {path}")
+ logger.info("Wine resolver finished for %s", app_name)
+ logger.info("Computed save path: %s", path)
if app_name == self.rgame.app_name:
self.cloud_save_path_edit.setDisabled(False)
self.compute_save_path_button.setDisabled(False)
diff --git a/rare/components/tabs/games/integrations/egl_sync_group.py b/rare/components/tabs/games/integrations/egl_sync_group.py
index 3077aaa9..531d2a4d 100644
--- a/rare/components/tabs/games/integrations/egl_sync_group.py
+++ b/rare/components/tabs/games/integrations/egl_sync_group.py
@@ -16,7 +16,7 @@ from rare.shared import RareCore
from rare.shared.workers.wine_resolver import WinePathResolver
from rare.ui.components.tabs.games.integrations.egl_sync_group import Ui_EGLSyncGroup
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup
-from rare.utils import runners
+from rare.utils.compat import utils as compat_utils
from rare.widgets.elide_label import ElideLabel
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
@@ -88,11 +88,7 @@ class EGLSyncGroup(QGroupBox):
def __run_wine_resolver(self):
self.egl_path_info.setText(self.tr("Updating..."))
- wine_resolver = WinePathResolver(
- self.core.get_app_launch_command("default"),
- runners.get_environment(self.core.get_app_environment("default")),
- PathSpec.egl_programdata()
- )
+ wine_resolver = WinePathResolver(self.core, "default", str(PathSpec.egl_programdata()))
wine_resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
QThreadPool.globalInstance().start(wine_resolver)
@@ -306,7 +302,8 @@ class EGLSyncListGroup(QGroupBox):
def items(self) -> Iterable[EGLSyncListItem]:
# for i in range(self.list.count()):
# yield self.list.item(i)
- return [self.ui.list.item(i) for i in range(self.ui.list.count())]
+ return map(self.ui.list.item, range(self.ui.list.count()))
+ # return [self.ui.list.item(i) for i in range(self.ui.list.count())]
class EGLSyncExportGroup(EGLSyncListGroup):
diff --git a/rare/components/tabs/games/integrations/eos_group.py b/rare/components/tabs/games/integrations/eos_group.py
index 2ed3bb0f..0f8efb95 100644
--- a/rare/components/tabs/games/integrations/eos_group.py
+++ b/rare/components/tabs/games/integrations/eos_group.py
@@ -224,7 +224,7 @@ class EosGroup(QGroupBox):
if platform.system() != "Windows":
prefixes = config.get_prefixes()
- prefixes = {prefix for prefix in prefixes if config.prefix_exists(prefix)}
+ prefixes = {prefix for prefix, _ in prefixes if config.prefix_exists(prefix)}
if platform.system() == "Darwin":
# TODO: add crossover support
pass
diff --git a/rare/components/tabs/settings/widgets/env_vars_model.py b/rare/components/tabs/settings/widgets/env_vars_model.py
index 29ca61b5..b8f39e26 100644
--- a/rare/components/tabs/settings/widgets/env_vars_model.py
+++ b/rare/components/tabs/settings/widgets/env_vars_model.py
@@ -11,10 +11,10 @@ from rare.lgndr.core import LegendaryCore
from rare.utils.misc import icon
if platform.system() != "Windows":
- from rare.utils.runners.wine import get_wine_environment
+ from rare.utils.compat.wine import get_wine_environment
if platform.system() in {"Linux", "FreeBSD"}:
- from rare.utils.runners.proton import get_steam_environment
+ from rare.utils.compat.proton import get_steam_environment
class EnvVarsTableModel(QAbstractTableModel):
diff --git a/rare/components/tabs/settings/widgets/proton.py b/rare/components/tabs/settings/widgets/proton.py
index 062c793f..069eb49c 100644
--- a/rare/components/tabs/settings/widgets/proton.py
+++ b/rare/components/tabs/settings/widgets/proton.py
@@ -10,7 +10,8 @@ from rare.models.wrapper import Wrapper, WrapperType
from rare.shared import RareCore
from rare.shared.wrappers import Wrappers
from rare.utils import config_helper as config
-from rare.utils.runners import proton
+from rare.utils.compat import proton
+from rare.utils.paths import proton_compat_dir
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
logger = getLogger("ProtonSettings")
@@ -82,7 +83,7 @@ class ProtonSettings(QGroupBox):
def __on_proton_changed(self, index):
steam_tool: Union[proton.ProtonTool, proton.CompatibilityTool] = self.tool_combo.itemData(index)
- steam_environ = proton.get_steam_environment(steam_tool)
+ steam_environ = proton.get_steam_environment(steam_tool, self.tool_prefix.text())
for key, value in steam_environ.items():
config.save_envvar(self.app_name, key, value)
self.environ_changed.emit(key)
@@ -101,8 +102,16 @@ class ProtonSettings(QGroupBox):
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
self.tool_prefix.setEnabled(steam_tool is not None)
- if steam_tool and not config.get_proton_compatdata(self.app_name, fallback=""):
- self.tool_prefix.setText(os.path.expanduser("~/.proton"))
+ if steam_tool:
+ if not (compatdata_path := config.get_proton_compatdata(self.app_name, fallback="")):
+ compatdata_path = proton_compat_dir(self.app_name)
+ config.save_proton_compatdata(self.app_name, str(compatdata_path))
+ target = compatdata_path.joinpath("pfx")
+ if not target.is_dir():
+ os.makedirs(target, exist_ok=True)
+ self.tool_prefix.setText(str(compatdata_path))
+ else:
+ self.tool_prefix.setText("")
self.tool_enabled.emit(steam_tool is not None)
diff --git a/rare/components/tabs/settings/widgets/wrappers.py b/rare/components/tabs/settings/widgets/wrappers.py
index c4223638..38e5780e 100644
--- a/rare/components/tabs/settings/widgets/wrappers.py
+++ b/rare/components/tabs/settings/widgets/wrappers.py
@@ -25,7 +25,7 @@ from rare.utils.misc import icon
from rare.widgets.dialogs import ButtonDialog, game_title
if pf.system() in {"Linux", "FreeBSD"}:
- from rare.utils.runners import proton
+ from rare.utils.compat import proton
logger = getLogger("WrapperSettings")
@@ -54,7 +54,7 @@ class WrapperEditDialog(ButtonDialog):
header = self.tr("Edit wrapper")
self.setWindowTitle(header)
self.setSubtitle(game_title(header, wrapper.name))
- self.line_edit.setText(wrapper.command)
+ self.line_edit.setText(wrapper.as_str)
@pyqtSlot(str)
def __on_text_changed(self, text: str):
@@ -83,7 +83,7 @@ class WrapperAddDialog(WrapperEditDialog):
self.setWindowTitle(header)
self.setSubtitle(header)
for wrapper in wrappers:
- self.combo_box.addItem(f"{wrapper.name} ({wrapper.command})", wrapper.command)
+ self.combo_box.addItem(f"{wrapper.name} ({wrapper.as_str})", wrapper.as_str)
@pyqtSlot(int)
def __on_index_changed(self, index: int):
@@ -101,7 +101,7 @@ class WrapperWidget(QFrame):
super(WrapperWidget, self).__init__(parent=parent)
self.setFrameShape(QFrame.StyledPanel)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
- self.setToolTip(wrapper.command)
+ self.setToolTip(wrapper.as_str)
text_lbl = QLabel(wrapper.name, parent=self)
text_lbl.setFont(QFont("monospace"))
@@ -296,7 +296,7 @@ class WrapperSettings(QWidget):
if pf.system() in {"Linux", "FreeBSD"}:
compat_cmds = [tool.command() for tool in proton.find_tools()]
- if wrapper.command in compat_cmds:
+ if wrapper.as_str in compat_cmds:
QMessageBox.warning(
self,
self.tr("Warning"),
@@ -306,7 +306,7 @@ class WrapperSettings(QWidget):
if wrapper.checksum in self.wrappers.get_game_md5sum_list(self.app_name):
QMessageBox.warning(
- self, self.tr("Warning"), self.tr("Wrapper {0} is already in the list").format(wrapper.command)
+ self, self.tr("Warning"), self.tr("Wrapper {0} is already in the list").format(wrapper.as_str)
)
return
diff --git a/rare/models/pathspec.py b/rare/models/pathspec.py
index 097de3d6..398d6524 100644
--- a/rare/models/pathspec.py
+++ b/rare/models/pathspec.py
@@ -39,7 +39,7 @@ class PathSpec:
possible_prefixes = get_prefixes()
prefixes = [
prefix
- for prefix in possible_prefixes
+ for prefix, _ in possible_prefixes
if os.path.exists(os.path.join(prefix, PathSpec.wine_egl_programdata()))
]
if not prefixes:
@@ -66,5 +66,5 @@ class PathSpec:
self.__egl_path_vars["{installdir}"] = igame.install_path
def resolve_egl_path_vars(self, path: str) -> Union[LiteralString, str, bytes]:
- cooked_path = [self.__egl_path_vars.get(p.lower(), p) for p in path.split("/")]
+ cooked_path = (self.__egl_path_vars.get(p.lower(), p) for p in path.split("/"))
return os.path.join(*cooked_path)
diff --git a/rare/models/wrapper.py b/rare/models/wrapper.py
index 75dabb42..376952d8 100644
--- a/rare/models/wrapper.py
+++ b/rare/models/wrapper.py
@@ -28,15 +28,20 @@ class Wrapper:
@property
def checksum(self) -> str:
- return md5(self.command.encode("utf-8")).hexdigest()
+ return md5(self.as_str.encode("utf-8")).hexdigest()
@property
def executable(self) -> str:
return shlex.quote(self.__command[0])
@property
- def command(self) -> str:
- return " ".join(shlex.quote(part) for part in self.__command)
+ def command(self) -> List[str]:
+ return self.__command
+
+ @property
+ def as_str(self) -> str:
+ # return " ".join(shlex.quote(part) for part in self.__command)
+ return " ".join(map(shlex.quote, self.__command))
@property
def name(self) -> str:
@@ -47,13 +52,13 @@ class Wrapper:
return self.__wtype
def __eq__(self, other) -> bool:
- return self.command == other.command
+ return self.as_str == other.as_str
def __hash__(self):
return hash(self.__command)
def __bool__(self) -> bool:
- return True if not self.is_editable else bool(self.command.strip())
+ return True if not self.is_editable else bool(self.as_str.strip())
@classmethod
def from_dict(cls, data: Dict):
diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py
index d812addf..28e50257 100644
--- a/rare/shared/rare_core.py
+++ b/rare/shared/rare_core.py
@@ -102,13 +102,17 @@ class RareCore(QObject):
self.__signals.application.update_statusbar.emit()
def active_workers(self) -> Iterable[QueueWorker]:
- return list(filter(lambda w: w.state == QueueWorkerState.ACTIVE, self.queue_workers))
+ # return list(filter(lambda w: w.state == QueueWorkerState.ACTIVE, self.queue_workers))
+ yield from filter(lambda w: w.state == QueueWorkerState.ACTIVE, self.queue_workers)
def queued_workers(self) -> Iterable[QueueWorker]:
- return list(filter(lambda w: w.state == QueueWorkerState.QUEUED, self.queue_workers))
+ # return list(filter(lambda w: w.state == QueueWorkerState.QUEUED, self.queue_workers))
+ yield from filter(lambda w: w.state == QueueWorkerState.QUEUED, self.queue_workers)
- def queue_info(self) -> List[QueueWorkerInfo]:
- return [w.worker_info() for w in self.queue_workers]
+ def queue_info(self) -> Iterable[QueueWorkerInfo]:
+ # return (w.worker_info() for w in self.queue_workers)
+ for w in self.queue_workers:
+ yield w.worker_info()
@staticmethod
def instance() -> 'RareCore':
diff --git a/rare/shared/workers/install_info.py b/rare/shared/workers/install_info.py
index d4ff1877..6db1f870 100644
--- a/rare/shared/workers/install_info.py
+++ b/rare/shared/workers/install_info.py
@@ -58,7 +58,8 @@ class InstallInfoWorker(Worker):
if not download.res or not download.res.failures:
self.signals.result.emit(download)
else:
- self.signals.failed.emit("\n".join(str(i) for i in download.res.failures))
+ # self.signals.failed.emit("\n".join(str(i) for i in download.res.failures))
+ self.signals.failed.emit("\n".join(map(str, download.res.failures)))
except LgndrException as ret:
self.signals.failed.emit(ret.message)
except Exception as e:
diff --git a/rare/shared/workers/uninstall.py b/rare/shared/workers/uninstall.py
index 23566cee..1a98144c 100644
--- a/rare/shared/workers/uninstall.py
+++ b/rare/shared/workers/uninstall.py
@@ -35,8 +35,8 @@ def uninstall_game(
if platform.system() == "Darwin":
# TODO: add crossover support
pass
- if prefixes is not None:
- for prefix in prefixes:
+ if len(prefixes):
+ for prefix, _ in prefixes:
remove_registry_entries(prefix)
logger.debug("Removed registry entries for prefix %s", prefix)
else:
diff --git a/rare/shared/workers/wine_resolver.py b/rare/shared/workers/wine_resolver.py
index f467193e..55b05faa 100644
--- a/rare/shared/workers/wine_resolver.py
+++ b/rare/shared/workers/wine_resolver.py
@@ -3,13 +3,14 @@ import platform
import time
from configparser import ConfigParser
from logging import getLogger
-from typing import Union, Iterable, Mapping, List
+from typing import Union, Iterable, List, Tuple, Dict
-from PyQt5.QtCore import pyqtSignal, QObject, QRunnable
+from PyQt5.QtCore import pyqtSignal, QObject
from rare.lgndr.core import LegendaryCore
from rare.models.game import RareGame
from rare.models.pathspec import PathSpec
+from rare.shared.wrappers import Wrappers
from rare.utils import config_helper as config
from rare.utils.misc import path_size, format_size
from .worker import Worker
@@ -19,7 +20,7 @@ if platform.system() == "Windows":
import winreg # pylint: disable=E0401
from legendary.lfs import windows_helpers
else:
- from rare.utils import runners
+ from rare.utils.compat import utils as compat_utils, proton
logger = getLogger("WineResolver")
@@ -28,45 +29,71 @@ class WinePathResolver(Worker):
class Signals(QObject):
result_ready = pyqtSignal(str, str)
- def __init__(self, command: List[str], environ: Mapping, path: str):
+ def __init__(self, core: LegendaryCore, app_name: str, path: str):
super(WinePathResolver, self). __init__()
self.signals = WinePathResolver.Signals()
- self.command = command
- self.environ = environ
+ self.core = core
+ self.app_name = app_name
self.path = path
+ @staticmethod
+ def _configure_process(core: LegendaryCore, app_name: str) -> Tuple[List, Dict]:
+ tool: proton.CompatibilityTool = None
+
+ if config.get_boolean(app_name, "no_wine"):
+ wrappers = Wrappers()
+ for w in wrappers.get_game_wrapper_list(app_name):
+ if w.is_compat_tool:
+ for t in proton.find_tools():
+ if t.checksum == w.checksum:
+ tool = t
+ break
+
+ cmd = core.get_app_launch_command(
+ app_name,
+ wrapper=tool.as_str(proton.SteamVerb.RUN_IN_PREFIX) if tool is not None else None,
+ disable_wine=config.get_boolean(app_name, "no_wine")
+ )
+ env = core.get_app_environment(app_name, disable_wine=config.get_boolean(app_name, "no_wine"))
+ env = compat_utils.get_host_environment(env, silent=True)
+
+ return cmd, env
+
@staticmethod
def _resolve_unix_path(cmd, env, path: str) -> str:
- if "waitforexitandrun" in cmd:
- cmd[cmd.index("waitforexitandrun")] = "runinprefix"
logger.info("Resolving path '%s'", path)
- wine_path = runners.resolve_path(cmd, env, path)
- logger.debug("Resolved Wine path '%s'", path)
- unix_path = runners.convert_to_unix_path(cmd, env, wine_path)
- logger.debug("Resolved Unix path '%s'", unix_path)
+ wine_path = compat_utils.resolve_path(cmd, env, path)
+ logger.info("Resolved Wine path '%s'", wine_path)
+ unix_path = compat_utils.convert_to_unix_path(cmd, env, wine_path)
+ logger.info("Resolved Unix path '%s'", unix_path)
return unix_path
def run_real(self):
- path = self._resolve_unix_path(self.command, self.environ, self.path)
- self.signals.result_ready.emit(path, "default")
+ command, environ = self._configure_process(self.core, self.app_name)
+ if not (command and environ):
+ logger.error("Cannot setup %s, missing infomation", {type(self).__name__})
+ self.signals.result_ready.emit("", self.app_name)
+
+ path = self._resolve_unix_path(command, environ, self.path)
+ self.signals.result_ready.emit(path, self.app_name)
return
class WineSavePathResolver(WinePathResolver):
def __init__(self, core: LegendaryCore, rgame: RareGame):
- cmd = core.get_app_launch_command(rgame.app_name, disable_wine=config.get_boolean(rgame.app_name, "no_wine"))
- env = core.get_app_environment(rgame.app_name, disable_wine=config.get_boolean(rgame.app_name, "no_wine"))
- env = runners.get_environment(env, silent=True)
path = PathSpec(core, rgame.igame).resolve_egl_path_vars(rgame.raw_save_path)
- if not (cmd and env and path):
- raise RuntimeError(f"Cannot setup {type(self).__name__}, missing infomation")
- super(WineSavePathResolver, self).__init__(cmd, env, path)
+ super(WineSavePathResolver, self).__init__(rgame.core, rgame.app_name, str(path))
self.rgame = rgame
def run_real(self):
logger.info("Resolving save path for %s (%s)", self.rgame.app_title, self.rgame.app_name)
- path = self._resolve_unix_path(self.command, self.environ, self.path)
+ command, environ = self._configure_process(self.core, self.rgame.app_name)
+ if not (command and environ):
+ logger.error("Cannot setup %s, missing infomation", {type(self).__name__})
+ self.signals.result_ready.emit("", self.rgame.app_name)
+
+ path = self._resolve_unix_path(command, environ, self.path)
# Clean wine output
# pylint: disable=E1136
if os.path.exists(path):
@@ -75,14 +102,14 @@ class WineSavePathResolver(WinePathResolver):
return
-class OriginWineWorker(QRunnable):
+class OriginWineWorker(WinePathResolver):
def __init__(self, core: LegendaryCore, games: Union[Iterable[RareGame], RareGame]):
- super(OriginWineWorker, self).__init__()
+ super(OriginWineWorker, self).__init__(core, "", "")
self.__cache: dict[str, ConfigParser] = {}
self.core = core
self.games = [games] if isinstance(games, RareGame) else games
- def run(self) -> None:
+ def run_real(self) -> None:
t = time.time()
for rgame in self.games:
@@ -102,18 +129,16 @@ class OriginWineWorker(QRunnable):
if platform.system() == "Windows":
install_dir = windows_helpers.query_registry_value(winreg.HKEY_LOCAL_MACHINE, reg_path, reg_key)
else:
- command = self.core.get_app_launch_command(rgame.app_name)
- environ = self.core.get_app_environment(rgame.app_name)
- environ = runners.get_environment(environ, silent=True)
+ command, environ = self._configure_process(self.core, rgame.app_name)
prefix = config.get_prefix(rgame.app_name)
if not prefix:
return
- use_wine = False
+ use_wine = True
if not use_wine:
# lk: this is the original way of getting the path by parsing "system.reg"
- reg = self.__cache.get(prefix, None) or runners.read_registry("system.reg", prefix)
+ reg = self.__cache.get(prefix, None) or compat_utils.read_registry("system.reg", prefix)
self.__cache[prefix] = reg
reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node")
@@ -123,11 +148,11 @@ class OriginWineWorker(QRunnable):
install_dir = reg.get(reg_path, f'"{reg_key}"', fallback=None)
else:
# lk: this is the alternative way of getting the path by using wine itself
- install_dir = runners.query_reg_key(command, environ, f"HKLM\\{reg_path}", reg_key)
+ install_dir = compat_utils.query_reg_key(command, environ, f"HKLM\\{reg_path}", reg_key)
if install_dir:
logger.debug("Found Wine install directory %s", install_dir)
- install_dir = runners.convert_to_unix_path(command, environ, install_dir)
+ install_dir = compat_utils.convert_to_unix_path(command, environ, install_dir)
if install_dir:
logger.debug("Found Unix install directory %s", install_dir)
else:
diff --git a/rare/shared/wrappers.py b/rare/shared/wrappers.py
index 707c8438..bf17ce36 100644
--- a/rare/shared/wrappers.py
+++ b/rare/shared/wrappers.py
@@ -69,7 +69,7 @@ class Wrappers:
# yield wrap
def get_game_wrapper_string(self, app_name: str) -> str:
- commands = [wrapper.command for wrapper in self.get_game_wrapper_list(app_name)]
+ commands = [wrapper.as_str for wrapper in self.get_game_wrapper_list(app_name)]
return " ".join(commands)
def get_game_wrapper_list(self, app_name: str) -> List[Wrapper]:
@@ -122,7 +122,7 @@ if __name__ == "__main__":
from pprint import pprint
from argparse import Namespace
- from rare.utils.runners import proton
+ from rare.utils.compat import proton
global config_dir
config_dir = os.getcwd
@@ -159,7 +159,7 @@ if __name__ == "__main__":
for i, tool in enumerate(proton.find_tools()):
wt = Wrapper(command=tool.command(), name=tool.name, wtype=WrapperType.COMPAT_TOOL)
wr.set_game_wrapper_list(f"compat_game_{i}", [wt])
- print(wt.command)
+ print(wt.as_str)
for wrp in wr.user_wrappers:
pprint(wrp)
diff --git a/rare/utils/compat/__init__.py b/rare/utils/compat/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/rare/utils/runners/proton.py b/rare/utils/compat/proton.py
similarity index 83%
rename from rare/utils/runners/proton.py
rename to rare/utils/compat/proton.py
index 2f646796..87b79d2c 100644
--- a/rare/utils/runners/proton.py
+++ b/rare/utils/compat/proton.py
@@ -2,9 +2,10 @@ import platform as pf
import os
import shlex
from dataclasses import dataclass
+from enum import StrEnum
from hashlib import md5
from logging import getLogger
-from typing import Optional, Union, List, Dict
+from typing import Optional, Union, List, Dict, Set
if pf.system() in {"Linux", "FreeBSD"}:
# noinspection PyUnresolvedReferences
@@ -22,11 +23,12 @@ def find_steam() -> str:
return path
-def find_libraries(steam_path: str) -> List[str]:
+def find_libraries(steam_path: str) -> Set[str]:
vdf_path = os.path.join(steam_path, "steamapps", "libraryfolders.vdf")
with open(vdf_path, "r") as f:
libraryfolders = vdf.load(f)["libraryfolders"]
- libraries = [os.path.join(folder["path"], "steamapps") for key, folder in libraryfolders.items()]
+ # libraries = [os.path.join(folder["path"], "steamapps") for key, folder in libraryfolders.items()]
+ libraries = {os.path.join(folder["path"], "steamapps") for key, folder in libraryfolders.items()}
return libraries
@@ -41,6 +43,16 @@ def find_libraries(steam_path: str) -> List[str]:
# is a good trade-off for the amount of complexity supporting everything would ensue.
+class SteamVerb(StrEnum):
+ RUN = "run"
+ WAIT_FOR_EXIT_AND_RUN = "waitforexitandrun"
+ RUN_IN_PREFIX = "runinprefix"
+ DESTROY_PREFIX = "destroyprefix"
+ GET_COMPAT_PATH = "getcompatpath"
+ GET_NATIVE_PATH = "getnativepath"
+ DEFAULT = WAIT_FOR_EXIT_AND_RUN
+
+
@dataclass
class SteamBase:
steam_path: str
@@ -57,19 +69,20 @@ class SteamBase:
def required_tool(self) -> Optional[str]:
return self.toolmanifest["manifest"].get("require_tool_appid", None)
- def command(self, setup: bool = False) -> List[str]:
+ def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> List[str]:
tool_path = os.path.normpath(self.tool_path)
cmd = "".join([shlex.quote(tool_path), self.toolmanifest["manifest"]["commandline"]])
# NOTE: "waitforexitandrun" seems to be the verb used in by steam to execute stuff
# `run` is used when setting up the environment, so use that if we are setting up the prefix.
- verb = "run" if setup else "waitforexitandrun"
- cmd = cmd.replace("%verb%", verb)
+ cmd = cmd.replace("%verb%", str(verb))
return shlex.split(cmd)
+ def as_str(self, verb: SteamVerb = SteamVerb.DEFAULT):
+ return " ".join(map(shlex.quote, self.command(verb)))
+
@property
def checksum(self) -> str:
- command = " ".join(shlex.quote(part) for part in self.command(setup=False))
- return md5(command.encode("utf-8")).hexdigest()
+ return md5(self.as_str().encode("utf-8")).hexdigest()
@dataclass
@@ -95,9 +108,9 @@ class ProtonTool(SteamRuntime):
return self.runtime is not None and self.runtime.appid == appid
return True
- def command(self, setup: bool = False) -> List[str]:
- cmd = self.runtime.command(setup)
- cmd.extend(super().command(setup))
+ def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> List[str]:
+ cmd = self.runtime.command(verb)
+ cmd.extend(super().command(verb))
return cmd
@@ -116,9 +129,9 @@ class CompatibilityTool(SteamBase):
name, data = list(self.compatibilitytool["compatibilitytools"]["compat_tools"].items())[0]
return data["display_name"]
- def command(self, setup: bool = False) -> List[str]:
- cmd = self.runtime.command(setup) if self.runtime is not None else []
- cmd.extend(super().command(setup))
+ def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> List[str]:
+ cmd = self.runtime.command(verb) if self.runtime is not None else []
+ cmd.extend(super().command(verb))
return cmd
@@ -229,6 +242,7 @@ def get_steam_environment(
# IMPORTANT: keep this in sync with the code below
environ = {"STEAM_COMPAT_DATA_PATH": compat_path if compat_path else ""}
if tool is None:
+ environ["STEAM_COMPAT_DATA_PATH"] = ""
environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = ""
environ["STEAM_COMPAT_LIBRARY_PATHS"] = ""
environ["STEAM_COMPAT_MOUNTS"] = ""
@@ -282,29 +296,5 @@ if __name__ == "__main__":
for tool in _tools:
print(get_steam_environment(tool))
print(tool.name)
- print(tool.command())
- print(" ".join(tool.command()))
-
-
-def find_proton_combos():
- possible_proton_combos = []
- compatibilitytools_dirs = [
- os.path.expanduser("~/.steam/steam/steamapps/common"),
- "/usr/share/steam/compatibilitytools.d",
- os.path.expanduser("~/.steam/compatibilitytools.d"),
- os.path.expanduser("~/.steam/root/compatibilitytools.d"),
- ]
- for c in compatibilitytools_dirs:
- if os.path.exists(c):
- for i in os.listdir(c):
- proton = os.path.join(c, i, "proton")
- compatibilitytool = os.path.join(c, i, "compatibilitytool.vdf")
- toolmanifest = os.path.join(c, i, "toolmanifest.vdf")
- if os.path.exists(proton) and (
- os.path.exists(compatibilitytool) or os.path.exists(toolmanifest)
- ):
- wrapper = f'"{proton}" run'
- possible_proton_combos.append(wrapper)
- if not possible_proton_combos:
- logger.warning("Unable to find any Proton version")
- return possible_proton_combos
+ print(tool.command(SteamVerb.RUN))
+ print(" ".join(tool.command(SteamVerb.RUN_IN_PREFIX)))
diff --git a/rare/utils/compat/utils.py b/rare/utils/compat/utils.py
new file mode 100644
index 00000000..929a5057
--- /dev/null
+++ b/rare/utils/compat/utils.py
@@ -0,0 +1,170 @@
+import os
+import platform
+import subprocess
+from configparser import ConfigParser
+from logging import getLogger
+from typing import Mapping, Dict, List, Tuple
+
+from PyQt5.QtCore import QProcess, QProcessEnvironment
+
+from rare.utils import config_helper as config
+if platform.system() != "Windows":
+ from . import wine
+ if platform.system() != "Darwin":
+ from . import proton
+
+logger = getLogger("CompatUtils")
+
+
+# this is a copied function from legendary.utils.wine_helpers, but registry file can be specified
+def read_registry(registry: str, prefix: str) -> ConfigParser:
+ accepted = ["system.reg", "user.reg"]
+ if registry not in accepted:
+ raise RuntimeError(f'Unknown target "{registry}" not in {accepted}')
+ reg = ConfigParser(comment_prefixes=(';', '#', '/', 'WINE'), allow_no_value=True,
+ strict=False)
+ reg.optionxform = str
+ reg.read(os.path.join(prefix, 'system.reg'))
+ return reg
+
+
+def get_configured_qprocess(command: List[str], environment: Mapping) -> QProcess:
+ logger.debug("Executing command: %s", command)
+ proc = QProcess()
+ proc.setProcessChannelMode(QProcess.SeparateChannels)
+ penv = QProcessEnvironment()
+ for ek, ev in environment.items():
+ penv.insert(ek, ev)
+ proc.setProcessEnvironment(penv)
+ proc.setProgram(command[0])
+ proc.setArguments(command[1:])
+ return proc
+
+
+def get_configured_subprocess(command: List[str], environment: Mapping) -> subprocess.Popen:
+ logger.debug("Executing command: %s", command)
+ return subprocess.Popen(
+ command,
+ stdin=None,
+ stdout=subprocess.PIPE,
+ stderr=None,
+ env=environment,
+ shell=False,
+ text=False,
+ )
+
+
+def execute_subprocess(command: List[str], arguments: List[str], environment: Mapping) -> Tuple[str, str]:
+ proc = get_configured_subprocess(command + arguments, environment)
+ print(proc.args)
+ out, err = proc.communicate()
+ out, err = out.decode("utf-8", "ignore") if out else "", err.decode("utf-8", "ignore") if err else ""
+
+ # lk: the following is a work-around for wineserver sometimes hanging around after
+ proc = get_configured_subprocess(command + ["wineboot", "-k"], environment)
+ _, _ = proc.communicate()
+ return out, err
+
+
+def execute_qprocess(command: List[str], arguments: List[str], environment: Mapping) -> Tuple[str, str]:
+ proc = get_configured_qprocess(command + arguments, environment)
+ proc.start()
+ proc.waitForFinished(-1)
+ out, err = (
+ proc.readAllStandardOutput().data().decode("utf-8", "ignore"),
+ proc.readAllStandardError().data().decode("utf-8", "ignore")
+ )
+ proc.deleteLater()
+
+ # lk: the following is a work-around for wineserver sometimes hanging around after
+ proc = get_configured_qprocess(command + ["wineboot", "-k"], environment)
+ proc.start()
+ proc.waitForFinished(-1)
+ proc.deleteLater()
+
+ return out, err
+
+
+def execute(command: List[str], arguments: List[str], environment: Mapping) -> Tuple[str, str]:
+ # Use the current environment if we are in flatpak or our own if we are on host
+ # In flatpak our environment is passed through `flatpak-spawn` arguments
+ if os.environ.get("container") == "flatpak":
+ flatpak_command = ["flatpak-spawn", "--host"]
+ for name, value in environment.items():
+ flatpak_command.append(f"--env={name}={value}")
+ _command = flatpak_command + command
+ _environment = os.environ.copy()
+ else:
+ _command = command
+ _environment = environment
+
+ try:
+ out, err = execute_qprocess(_command, arguments, _environment)
+ except Exception as e:
+ out, err = "", str(e)
+
+ return out, err
+
+
+def resolve_path(command: List[str], environment: Mapping, path: str) -> str:
+ path = path.strip().replace("/", "\\")
+ # lk: if path does not exist form
+ arguments = ["cmd.exe", "/c", "echo", path]
+ # lk: if path exists and needs a case-sensitive interpretation form
+ # cmd = [wine_cmd, 'cmd', '/c', f'cd {path} & cd']
+ out, err = execute(command, arguments, environment)
+ out, err = out.strip(), err.strip()
+ if not out:
+ logger.error("Failed to resolve wine path due to \"%s\"", err)
+ return out
+ return out.strip('"')
+
+
+def query_reg_path(wine_exec: str, wine_env: Mapping, reg_path: str):
+ raise NotImplementedError
+
+
+def query_reg_key(command: List[str], environment: Mapping, reg_path: str, reg_key) -> str:
+ arguments = ["reg.exe", "query", reg_path, "/v", reg_key]
+ out, err = execute(command, arguments, environment)
+ out, err = out.strip(), err.strip()
+ if not out:
+ logger.error("Failed to query registry key due to \"%s\"", err)
+ return out
+ lines = out.split("\n")
+ keys: Dict = {}
+ for line in lines:
+ if line.startswith(" "*4):
+ key = [x for x in line.split(" "*4, 3) if bool(x)]
+ keys.update({key[0]: key[2]})
+ return keys.get(reg_key, "")
+
+
+def convert_to_windows_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
+ raise NotImplementedError
+
+
+def convert_to_unix_path(command: List[str], environment: Mapping, path: str) -> str:
+ path = path.strip().strip('"')
+ arguments = ["winepath.exe", "-u", path]
+ out, err = execute(command, arguments, environment)
+ out, err = out.strip(), err.strip()
+ if not out:
+ logger.error("Failed to convert to unix path due to \"%s\"", err)
+ return os.path.realpath(out) if (out := out.strip()) else out
+
+
+def get_host_environment(app_environment: Dict, silent: bool = True) -> Dict:
+ # Get a clean environment if we are in flatpak, this environment will be passed
+ # to `flatpak-spawn`, otherwise use the system's.
+ _environ = {} if os.environ.get("container") == "flatpak" else os.environ.copy()
+ _environ.update(app_environment)
+ if silent:
+ _environ["WINEESYNC"] = "0"
+ _environ["WINEFSYNC"] = "0"
+ _environ["WINE_DISABLE_FAST_SYNC"] = "1"
+ _environ["WINEDEBUG"] = "-all"
+ _environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
+ # lk: pressure-vessel complains about this but it doesn't fail due to it
+ _environ["DISPLAY"] = ""
+ return _environ
diff --git a/rare/utils/runners/wine.py b/rare/utils/compat/wine.py
similarity index 100%
rename from rare/utils/runners/wine.py
rename to rare/utils/compat/wine.py
diff --git a/rare/utils/config_helper.py b/rare/utils/config_helper.py
index 1411c231..75488675 100644
--- a/rare/utils/config_helper.py
+++ b/rare/utils/config_helper.py
@@ -1,5 +1,5 @@
import os
-from typing import Callable, Optional, Set, Any, List
+from typing import Callable, Optional, Set, Any, List, Tuple
from legendary.core import LegendaryCore
from legendary.models.config import LGDConf
@@ -127,27 +127,27 @@ def get_proton_compatdata_fallback(app_name: Optional[str] = None, fallback: Any
return _compat
-def get_wine_prefixes() -> Set[str]:
- _prefixes: Set[str] = set()
+def get_wine_prefixes() -> Set[Tuple[str, str]]:
+ _prefixes: Set[Tuple[str, str]] = set()
for name, section in _config.items():
pfx = section.get("WINEPREFIX") or section.get("wine_prefix")
if pfx:
- _prefixes.update([pfx])
- _prefixes = {os.path.expanduser(prefix) for prefix in _prefixes}
- return set(filter(os.path.isdir, _prefixes))
+ _prefixes.update([(pfx, name[:-len(".env")] if name.endswith(".env") else name)])
+ _prefixes = {(os.path.expanduser(p), n) for p, n in _prefixes}
+ return {(p, n) for p, n in _prefixes if os.path.isdir(p)}
-def get_proton_prefixes() -> Set[str]:
- _prefixes: Set[str] = set()
+def get_proton_prefixes() -> Set[Tuple[str, str]]:
+ _prefixes: Set[Tuple[str, str]] = set()
for name, section in _config.items():
pfx = os.path.join(compatdata, "pfx") if (compatdata := section.get("STEAM_COMPAT_DATA_PATH")) else ""
if pfx:
- _prefixes.update([pfx])
- _prefixes = {os.path.expanduser(prefix) for prefix in _prefixes}
- return set(filter(os.path.isdir, _prefixes))
+ _prefixes.update([(pfx, name[:-len(".env")] if name.endswith(".env") else name)])
+ _prefixes = {(os.path.expanduser(p), n) for p, n in _prefixes}
+ return {(p, n) for p, n in _prefixes if os.path.isdir(p)}
-def get_prefixes() -> Set[str]:
+def get_prefixes() -> Set[Tuple[str, str]]:
return get_wine_prefixes().union(get_proton_prefixes())
diff --git a/rare/utils/paths.py b/rare/utils/paths.py
index ac7630cb..ba7ab9e3 100644
--- a/rare/utils/paths.py
+++ b/rare/utils/paths.py
@@ -81,6 +81,15 @@ def desktop_dir() -> Path:
def applications_dir() -> Path:
return Path(QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation))
+
+def proton_compat_dir(app_name: str) -> Path:
+ return data_dir().joinpath(f"compatdata/{app_name}")
+
+
+def wine_compat_dir(app_name: str) -> Path:
+ return proton_compat_dir(app_name).joinpath("pfx")
+
+
# fmt: off
__link_suffix = {
"Windows": {
diff --git a/rare/utils/runners/__init__.py b/rare/utils/runners/__init__.py
deleted file mode 100644
index 3df2f755..00000000
--- a/rare/utils/runners/__init__.py
+++ /dev/null
@@ -1,109 +0,0 @@
-import os
-import platform
-import subprocess
-from configparser import ConfigParser
-from logging import getLogger
-from typing import Mapping, Dict, List, Tuple, Optional
-
-from rare.utils import config_helper as config
-if platform.system() != "Windows":
- from . import wine
- if platform.system() != "Darwin":
- from . import proton
-
-logger = getLogger("Runners")
-
-
-# this is a copied function from legendary.utils.wine_helpers, but registry file can be specified
-def read_registry(registry: str, prefix: str) -> ConfigParser:
- accepted = ["system.reg", "user.reg"]
- if registry not in accepted:
- raise RuntimeError(f'Unknown target "{registry}" not in {accepted}')
- reg = ConfigParser(comment_prefixes=(';', '#', '/', 'WINE'), allow_no_value=True,
- strict=False)
- reg.optionxform = str
- reg.read(os.path.join(prefix, 'system.reg'))
- return reg
-
-
-def execute(command: List[str], environment: Mapping) -> Tuple[str, str]:
- if os.environ.get("container") == "flatpak":
- flatpak_command = ["flatpak-spawn", "--host"]
- for name, value in environment.items():
- flatpak_command.append(f"--env={name}={value}")
- command = flatpak_command + command
- try:
- proc = subprocess.Popen(
- command,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- # Use the current environment if we are in flatpak or our own if we are on host
- # In flatpak our environment is passed through `flatpak-spawn` arguments
- env=os.environ.copy() if os.environ.get("container") == "flatpak" else environment,
- shell=False,
- text=True,
- )
- res = proc.communicate()
- except (FileNotFoundError, PermissionError) as e:
- res = ("", str(e))
- return res
-
-
-def resolve_path(command: List[str], environment: Mapping, path: str) -> str:
- path = path.strip().replace("/", "\\")
- # lk: if path does not exist form
- cmd = command + ["cmd", "/c", "echo", path]
- # lk: if path exists and needs a case-sensitive interpretation form
- # cmd = [wine_cmd, 'cmd', '/c', f'cd {path} & cd']
- out, err = execute(cmd, environment)
- out, err = out.strip(), err.strip()
- if not out:
- logger.error("Failed to resolve wine path due to \"%s\"", err)
- return out
- return out.strip('"')
-
-
-def query_reg_path(wine_exec: str, wine_env: Mapping, reg_path: str):
- raise NotImplementedError
-
-
-def query_reg_key(command: List[str], environment: Mapping, reg_path: str, reg_key) -> str:
- cmd = command + ["reg", "query", reg_path, "/v", reg_key]
- out, err = execute(cmd, environment)
- out, err = out.strip(), err.strip()
- if not out:
- logger.error("Failed to query registry key due to \"%s\"", err)
- return out
- lines = out.split("\n")
- keys: Dict = {}
- for line in lines:
- if line.startswith(" "*4):
- key = [x for x in line.split(" "*4, 3) if bool(x)]
- keys.update({key[0]: key[2]})
- return keys.get(reg_key, "")
-
-
-def convert_to_windows_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
- raise NotImplementedError
-
-
-def convert_to_unix_path(command: List[str], environment: Mapping, path: str) -> str:
- path = path.strip().strip('"')
- cmd = command + ["winepath.exe", "-u", path]
- out, err = execute(cmd, environment)
- out, err = out.strip(), err.strip()
- if not out:
- logger.error("Failed to convert to unix path due to \"%s\"", err)
- return os.path.realpath(out) if (out := out.strip()) else out
-
-
-def get_environment(app_environment: Dict, silent: bool = True) -> Dict:
- # Get a clean environment if we are in flatpak, this environment will be passed
- # to `flatpak-spawn`, otherwise use the system's.
- _environ = {} if os.environ.get("container") == "flatpak" else os.environ.copy()
- _environ.update(app_environment)
- if silent:
- _environ["WINEDEBUG"] = "-all"
- _environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
- _environ["DISPLAY"] = ""
- return _environ