"""Utilities for manipulating Wine""" import os from collections import OrderedDict from gettext import gettext as _ from typing import Any, Dict, List, Optional, Set, Tuple from lutris.exceptions import MisconfigurationError, UnspecifiedVersionError from lutris.settings import WINE_DIR from lutris.util import cache_single, linux, system from lutris.util.log import logger from lutris.util.strings import get_natural_sort_key, parse_version from lutris.util.wine import fsync, proton WINE_DEFAULT_ARCH: str = "win64" if linux.LINUX_SYSTEM.is_64_bit else "win32" GE_PROTON_LATEST: str = "ge-proton" WINE_PATHS: Dict[str, str] = { "winehq-devel": "/opt/wine-devel/bin/wine", "winehq-staging": "/opt/wine-staging/bin/wine", "wine-development": "/usr/lib/wine-development/wine", "system": "wine", } # Insert additional system-wide Wine installations. try: if system.path_exists("/usr/lib"): for _candidate in os.listdir("/usr/lib/"): if _candidate.startswith("wine-"): _wine_path = os.path.join("/usr/lib/", _candidate, "bin/wine") if os.path.isfile(_wine_path): WINE_PATHS["System " + _candidate] = _wine_path _candidate = None _wine_path = None except Exception as ex: logger.exception("Unable to enumerate system Wine versions: %s", ex) def detect_arch(prefix_path: Optional[str] = None, wine_path: Optional[str] = None) -> str: """Given a Wine prefix path, return its architecture""" if wine_path: if proton.is_proton_path(wine_path) or system.path_exists(wine_path + "64"): return "win64" if prefix_path and is_prefix_directory(prefix_path): return detect_prefix_arch(prefix_path) return "win32" def is_prefix_directory(prefix_path: str) -> bool: """Detects if a path is ther oot of a Wine prefix; to be one, it must contain a 'system.reg' file.""" if not prefix_path: return False prefix_path = os.path.expanduser(prefix_path) registry_path = os.path.join(prefix_path, "system.reg") return os.path.isdir(prefix_path) and os.path.isfile(registry_path) def detect_prefix_arch(prefix_path: str) -> str: """Return the architecture of the prefix found in `prefix_path`""" if not is_prefix_directory(prefix_path): raise RuntimeError("Prefix not found: %s" % prefix_path) prefix_path = os.path.expanduser(prefix_path) registry_path = os.path.join(prefix_path, "system.reg") with open(registry_path, "r", encoding="utf-8") as registry: for _line_no in range(5): line = registry.readline() if "win64" in line: return "win64" if "win32" in line: return "win32" logger.error("Failed to detect Wine prefix architecture in %s; defaulting to 32-bit.", prefix_path) return "win32" def set_drive_path(prefix: str, letter: str, path: str) -> None: """Changes the path to a Wine drive""" dosdevices_path = os.path.join(prefix, "dosdevices") if not system.path_exists(dosdevices_path): raise OSError("Invalid prefix path %s" % prefix) drive_path = os.path.join(dosdevices_path, letter + ":") if system.path_exists(drive_path): os.remove(drive_path) logger.debug("Linking %s to %s", drive_path, path) system.create_symlink(path, drive_path) def is_gstreamer_build(wine_path: str) -> bool: """Returns whether a wine build ships with gstreamer libraries. This allows to set GST_PLUGIN_SYSTEM_PATH_1_0 for the builds that support it. """ base_path = os.path.dirname(os.path.dirname(wine_path)) return system.path_exists(os.path.join(base_path, "lib64/gstreamer-1.0")) def is_winewayland_available(runner_version: str) -> bool: """Check if winewayland graphics driver available""" files_dir = get_runner_files_dir_for_version(runner_version) if files_dir: winewayland_paths = ( "lib64/wine/x86_64-windows/winewayland.drv", "lib64/wine/i386-windows/winewayland.drv", "lib/wine/x86_64-windows/winewayland.drv", "lib/wine/i386-windows/winewayland.drv", ) if not any(os.path.exists(os.path.join(files_dir, winewayland_path)) for winewayland_path in winewayland_paths): return False return True def is_installed_systemwide() -> bool: """Return whether Wine is installed outside of Lutris""" for build in WINE_PATHS.values(): if system.can_find_executable(build): return True return False def list_system_wine_versions() -> List[str]: """Return the list of wine versions installed on the system""" versions = [name for name, path in WINE_PATHS.items() if get_system_wine_version(path)] return sorted(versions, key=get_natural_sort_key, reverse=True) def list_lutris_wine_versions() -> List[str]: """Return the list of wine versions installed by lutris""" if not system.path_exists(WINE_DIR): return [] versions = [] for dirname in version_sort(os.listdir(WINE_DIR), reverse=True): try: wine_path = os.path.join(WINE_DIR, dirname, "bin/wine") if wine_path and os.path.isfile(wine_path): versions.append(dirname) except MisconfigurationError: pass # if it's not properly installed, skip it return sorted(versions, key=get_natural_sort_key, reverse=True) @cache_single def get_installed_wine_versions() -> List[str]: """Return the list of Wine versions installed, with no duplicates and in the presentation order.""" versions: Set[str] = { GE_PROTON_LATEST, } for v in proton.list_proton_versions(): if v not in versions: versions.add(v) for v in list_lutris_wine_versions(): if v not in versions: versions.add(v) for v in list_system_wine_versions(): if v not in versions: versions.add(v) return list(versions) def clear_wine_version_cache() -> None: get_installed_wine_versions.cache_clear() proton.get_proton_versions.cache_clear() proton.get_umu_path.cache_clear() def get_runner_files_dir_for_version(version: str) -> Optional[str]: """This returns the path to the root of the Wine files for a specific version. The 'bin' directory for that version is there, and we can place more directories there. If we shouldn't do that, this will return None.""" if version == GE_PROTON_LATEST: return None if version in WINE_PATHS: return None elif proton.is_proton_version(version): return os.path.join(WINE_DIR, version, "files") else: return os.path.join(WINE_DIR, version) def get_wine_path_for_version(version: str, config: Optional[dict] = None) -> str: """Return the absolute path of a wine executable for a given version, or the configured version if you don't ask for a version.""" if not version and config: version = config["version"] if not version: raise UnspecifiedVersionError(_("The Wine version must be specified.")) if version == GE_PROTON_LATEST: return proton.get_umu_path() if version in WINE_PATHS: return system.find_required_executable(WINE_PATHS[version]) if proton.is_proton_version(version): return proton.get_proton_wine_path(version) if version == "custom": if config is None: raise RuntimeError("Custom wine paths are only supported when a configuration is available.") wine_path = config.get("custom_wine_path") if not wine_path: raise RuntimeError("The 'custom' Wine version can be used only if the custom wine path is set.") return wine_path return os.path.join(WINE_DIR, version, "bin/wine") def parse_wine_version(version: str) -> Tuple[List[int], str, str]: """This is a specialized parse_version() that adjusts some odd Wine versions for correct parsing.""" version = version.replace("Proton7-", "Proton-7.") version = version.replace("Proton8-", "Proton-8.") version = version.replace("Proton9-", "Proton-9.") return parse_version(version) def version_sort(versions: List[str], reverse: bool = False) -> List[str]: def version_key(version: str) -> List[Any]: version_list, prefix, suffix = parse_wine_version(version) # Normalize the length of sub-versions sort_key: List[Any] = list(version_list) + [0] * (10 - len(version_list)) sort_key.append(prefix) sort_key.append(suffix) return sort_key return sorted(versions, key=version_key, reverse=reverse) def is_esync_limit_set() -> bool: """Checks if the number of files open is acceptable for esync usage.""" return linux.LINUX_SYSTEM.has_enough_file_descriptors() def is_fsync_supported() -> bool: """Checks if the running kernel has Valve's futex patch applied.""" return fsync.get_fsync_support() def get_default_wine_version() -> str: """Return the default version of wine.""" return GE_PROTON_LATEST def get_system_wine_version(wine_path: str = "wine") -> str: """Return the version of Wine installed on the system.""" if wine_path != "wine" and not system.path_exists(wine_path): return "" if wine_path == "wine" and not system.can_find_executable("wine"): return "" version = system.read_process_output([wine_path, "--version"]) if not version: logger.error("Error reading wine version for %s", wine_path) return "" if version.startswith("wine-"): version = version[5:] return version def get_real_executable(windows_executable: str, working_dir: Optional[str]) -> Tuple[str, List[str], Optional[str]]: """Given a Windows executable, return the real program capable of launching it along with necessary arguments.""" exec_name = windows_executable.lower() if exec_name.endswith(".msi"): return ("msiexec", ["/i", windows_executable], working_dir) if exec_name.endswith(".bat"): if not working_dir or os.path.dirname(windows_executable) == working_dir: working_dir = os.path.dirname(windows_executable) windows_executable = os.path.basename(windows_executable) return ("cmd", ["/C", windows_executable], working_dir) if exec_name.endswith(".lnk"): return ("start", ["/unix", windows_executable], working_dir) return (windows_executable, [], working_dir) def get_overrides_env(overrides: Dict[str, str]) -> str: """ Output a string of dll overrides usable with WINEDLLOVERRIDES See: https://wiki.winehq.org/Wine_User%27s_Guide#WINEDLLOVERRIDES.3DDLL_Overrides """ default_overrides = {"winemenubuilder": ""} overrides.update(default_overrides) override_buckets: Dict[str, List] = OrderedDict( [("n,b", []), ("b,n", []), ("b", []), ("n", []), ("d", []), ("", [])] ) for dll, value in overrides.items(): if not value: value = "" value = value.replace(" ", "") value = value.replace("builtin", "b") value = value.replace("native", "n") value = value.replace("disabled", "") try: override_buckets[value].append(dll) except KeyError: logger.error("Invalid override value %s", value) continue override_strings = [] for value, dlls in override_buckets.items(): if not dlls: continue override_strings.append("{}={}".format(",".join(sorted(dlls)), value)) return ";".join(override_strings)