mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:07:45 +02:00
302 lines
15 KiB
Python
302 lines
15 KiB
Python
"""
|
|
Winetricks environment and dependency setup for install_wine_components.
|
|
Builds env dict, checks downloaders/deps, resolves components list.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import logging
|
|
from typing import Optional, List, Callable, Tuple
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_clean_winetricks_base_env() -> dict:
|
|
"""
|
|
Base environment for winetricks subprocess with no AppImage/bundle vars.
|
|
Wine and wineserver must not see _MEIPASS, bundle PATH/LD_LIBRARY_PATH or
|
|
connection reset / regsvr32 failures can occur when running from AppImage.
|
|
"""
|
|
preserve = [
|
|
"HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LANGUAGE",
|
|
"DISPLAY", "WAYLAND_DISPLAY", "XDG_RUNTIME_DIR", "XAUTHORITY",
|
|
"XDG_SESSION_TYPE", "DBUS_SESSION_BUS_ADDRESS", "XDG_DATA_DIRS", "XDG_CONFIG_DIRS",
|
|
"XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP", "QT_QPA_PLATFORM", "GDK_BACKEND",
|
|
]
|
|
env = {}
|
|
for var in preserve:
|
|
if var in os.environ:
|
|
env[var] = os.environ[var]
|
|
if "HOME" not in env and "HOME" in os.environ:
|
|
env["HOME"] = os.environ["HOME"]
|
|
path = os.environ.get("PATH", "")
|
|
if getattr(sys, "_MEIPASS", None):
|
|
path = os.pathsep.join(p for p in path.split(os.pathsep) if not p.startswith(sys._MEIPASS))
|
|
env["PATH"] = path or "/usr/bin:/bin"
|
|
return env
|
|
|
|
|
|
class WinetricksEnvMixin:
|
|
"""Mixin providing env build and dependency check for WinetricksHandler.install_wine_components."""
|
|
|
|
def _build_winetricks_env(
|
|
self,
|
|
wineprefix: str,
|
|
status_callback: Optional[Callable[[str], None]],
|
|
specific_components: Optional[List[str]],
|
|
) -> Tuple[Optional[dict], Optional[List[str]]]:
|
|
"""
|
|
Build environment and resolve components for winetricks. Returns (env, components_to_install) or (None, None).
|
|
Uses a clean base env (no AppImage/bundle vars) so wine/wineserver see only Proton and system.
|
|
"""
|
|
env = _get_clean_winetricks_base_env()
|
|
env['WINEDEBUG'] = '-all'
|
|
env['WINEPREFIX'] = wineprefix
|
|
env['WINETRICKS_GUI'] = 'none'
|
|
if 'DISPLAY' in env:
|
|
env['WINEDLLOVERRIDES'] = 'winemenubuilder.exe=d'
|
|
else:
|
|
env['DISPLAY'] = env.get('DISPLAY', '')
|
|
|
|
try:
|
|
from ..handlers.config_handler import ConfigHandler
|
|
from ..handlers.wine_utils import WineUtils
|
|
|
|
config = ConfigHandler()
|
|
user_proton_path = config.get_proton_path()
|
|
wine_binary = None
|
|
if user_proton_path and user_proton_path != 'auto':
|
|
if os.path.exists(user_proton_path):
|
|
resolved_proton_path = os.path.realpath(user_proton_path)
|
|
valve_proton_wine = os.path.join(resolved_proton_path, 'dist', 'bin', 'wine')
|
|
ge_proton_wine = os.path.join(resolved_proton_path, 'files', 'bin', 'wine')
|
|
if os.path.exists(valve_proton_wine):
|
|
wine_binary = valve_proton_wine
|
|
self.logger.info(f"Using user-selected Proton: {user_proton_path}")
|
|
elif os.path.exists(ge_proton_wine):
|
|
wine_binary = ge_proton_wine
|
|
self.logger.info(f"Using user-selected GE-Proton: {user_proton_path}")
|
|
else:
|
|
self.logger.warning(f"User-selected Proton path invalid: {user_proton_path}")
|
|
else:
|
|
self.logger.warning(f"User-selected Proton no longer exists: {user_proton_path}")
|
|
|
|
if not wine_binary:
|
|
if user_proton_path == 'auto':
|
|
self.logger.info("Auto-detecting Proton (user selected 'auto')")
|
|
best_proton = WineUtils.select_best_proton()
|
|
if best_proton:
|
|
wine_binary = WineUtils.find_proton_binary(best_proton['name'])
|
|
self.logger.info(f"Auto-selected Proton: {best_proton['name']} at {best_proton['path']}")
|
|
else:
|
|
self.logger.error("Auto-detection failed - no Proton versions found")
|
|
available_versions = WineUtils.scan_all_proton_versions()
|
|
if available_versions:
|
|
self.logger.error(f"Available Proton versions: {[v['name'] for v in available_versions]}")
|
|
else:
|
|
self.logger.error("No Proton versions detected in standard Steam locations")
|
|
else:
|
|
self.logger.error(f"Cannot use configured Proton: {user_proton_path}")
|
|
self.logger.error("Please check Settings and ensure the Proton version still exists")
|
|
return (None, None)
|
|
|
|
if not wine_binary:
|
|
self.logger.error("Cannot run winetricks: No compatible Proton version found")
|
|
self.logger.error("Please ensure you have Proton 9+ or GE-Proton installed through Steam")
|
|
return (None, None)
|
|
|
|
if not (os.path.exists(wine_binary) and os.access(wine_binary, os.X_OK)):
|
|
self.logger.error(f"Cannot run winetricks: Wine binary not found or not executable: {wine_binary}")
|
|
return (None, None)
|
|
|
|
proton_dist_path = os.path.dirname(os.path.dirname(wine_binary))
|
|
self.logger.debug(f"Proton dist path: {proton_dist_path}")
|
|
|
|
# Create wine wrapper scripts (like protontricks does) to ensure proper
|
|
# LD_LIBRARY_PATH setup when winetricks spawns wine subprocesses
|
|
from .wine_wrapper import WineWrapperManager
|
|
wrapper_manager = WineWrapperManager()
|
|
wrapper_dir = wrapper_manager.create_wrappers(proton_dist_path)
|
|
|
|
if wrapper_dir:
|
|
wine_wrapper = wrapper_dir / "wine"
|
|
wineserver_wrapper = wrapper_dir / "wineserver"
|
|
env['WINE'] = str(wine_wrapper)
|
|
env['WINELOADER'] = str(wine_wrapper)
|
|
env['WINESERVER'] = str(wineserver_wrapper)
|
|
# Put wrapper dir first in PATH so winetricks finds our wrappers
|
|
env['PATH'] = f"{wrapper_dir}:{proton_dist_path}/bin:{env.get('PATH', '')}"
|
|
self.logger.info(f"Using wine wrappers for winetricks: {wrapper_dir}")
|
|
self.logger.debug(f"WINE={wine_wrapper}, WINESERVER={wineserver_wrapper}")
|
|
else:
|
|
# Fallback to direct binary paths if wrapper creation fails
|
|
self.logger.warning("Wine wrapper creation failed, using direct binary paths")
|
|
env['WINE'] = str(wine_binary)
|
|
env['WINELOADER'] = str(wine_binary)
|
|
wineserver_bin = os.path.join(proton_dist_path, 'bin', 'wineserver')
|
|
if os.path.exists(wineserver_bin) and os.access(wineserver_bin, os.X_OK):
|
|
env['WINESERVER'] = wineserver_bin
|
|
env['PATH'] = f"{proton_dist_path}/bin:{env.get('PATH', '')}"
|
|
|
|
env['WINEDLLPATH'] = f"{proton_dist_path}/lib64/wine:{proton_dist_path}/lib/wine"
|
|
# LD_LIBRARY_PATH is now set by wrapper scripts, but set it here too for completeness
|
|
ld_prepend = f"{proton_dist_path}/lib64:{proton_dist_path}/lib"
|
|
env['LD_LIBRARY_PATH'] = f"{ld_prepend}:{env.get('LD_LIBRARY_PATH', '')}" if env.get('LD_LIBRARY_PATH') else ld_prepend
|
|
self.logger.debug(f"Set LD_LIBRARY_PATH for Proton (prepend): {ld_prepend[:80]}...")
|
|
|
|
dll_overrides = {
|
|
"beclient": "b,n", "beclient_x64": "b,n", "dxgi": "n", "d3d9": "n",
|
|
"d3d10core": "n", "d3d11": "n", "d3d12": "n", "d3d12core": "n",
|
|
"nvapi": "n", "nvapi64": "n", "nvofapi64": "n", "nvcuda": "b"
|
|
}
|
|
existing_overrides = env.get('WINEDLLOVERRIDES', '')
|
|
if existing_overrides:
|
|
for override in existing_overrides.split(';'):
|
|
if '=' in override:
|
|
name, value = override.split('=', 1)
|
|
dll_overrides[name] = value
|
|
env['WINEDLLOVERRIDES'] = ';'.join(f"{name}={setting}" for name, setting in dll_overrides.items())
|
|
env['WINE_LARGE_ADDRESS_AWARE'] = '1'
|
|
env['DXVK_ENABLE_NVAPI'] = '1'
|
|
self.logger.debug(f"Set protontricks environment: WINEDLLPATH={env['WINEDLLPATH']}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Cannot run winetricks: Failed to get Proton wine binary: {e}")
|
|
return (None, None)
|
|
|
|
has_downloader = False
|
|
for tool in ['aria2c', 'curl', 'wget']:
|
|
try:
|
|
result = subprocess.run(['which', tool], capture_output=True, timeout=2, env=os.environ.copy())
|
|
if result.returncode == 0:
|
|
has_downloader = True
|
|
self.logger.info(f"System has {tool} available - winetricks will auto-select best option")
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
if not has_downloader:
|
|
self._handle_missing_downloader_error()
|
|
return (None, None)
|
|
|
|
tools_dir = None
|
|
bundled_tools = []
|
|
for tool_name in ['cabextract', 'unzip', '7z', 'xz', 'sha256sum']:
|
|
bundled_tool = self._get_bundled_tool(tool_name, fallback_to_system=False)
|
|
if bundled_tool:
|
|
bundled_tools.append(tool_name)
|
|
if tools_dir is None:
|
|
tools_dir = os.path.dirname(bundled_tool)
|
|
if tools_dir:
|
|
env['PATH'] = f"{env.get('PATH', '')}:{tools_dir}"
|
|
bundling_msg = f"Using bundled tools directory (after system PATH): {tools_dir}"
|
|
self.logger.info(bundling_msg)
|
|
if status_callback:
|
|
status_callback(bundling_msg)
|
|
tools_msg = f"Bundled tools available: {', '.join(bundled_tools)}"
|
|
self.logger.info(tools_msg)
|
|
if status_callback:
|
|
status_callback(tools_msg)
|
|
else:
|
|
self.logger.debug("No bundled tools found, relying on system PATH")
|
|
|
|
deps_check_msg = "=== Checking winetricks dependencies ==="
|
|
self.logger.info(deps_check_msg)
|
|
if status_callback:
|
|
status_callback(deps_check_msg)
|
|
missing_deps = []
|
|
bundled_tools_list = ['aria2c', 'wget', 'unzip', '7z', 'xz', 'sha256sum', 'cabextract']
|
|
dependency_checks = {
|
|
'wget': 'wget', 'curl': 'curl', 'aria2c': 'aria2c', 'unzip': 'unzip',
|
|
'7z': ['7z', '7za', '7zr'], 'xz': 'xz',
|
|
'sha256sum': ['sha256sum', 'sha256', 'shasum'], 'perl': 'perl'
|
|
}
|
|
for dep_name, commands in dependency_checks.items():
|
|
found = False
|
|
if isinstance(commands, str):
|
|
commands = [commands]
|
|
if dep_name in bundled_tools_list:
|
|
for cmd in commands:
|
|
bundled_tool = self._get_bundled_tool(cmd, fallback_to_system=False)
|
|
if bundled_tool:
|
|
dep_msg = f" {dep_name}: {bundled_tool} (bundled)"
|
|
self.logger.info(dep_msg)
|
|
if status_callback:
|
|
status_callback(dep_msg)
|
|
found = True
|
|
break
|
|
if not found:
|
|
for cmd in commands:
|
|
try:
|
|
result = subprocess.run(['which', cmd], capture_output=True, timeout=2)
|
|
if result.returncode == 0:
|
|
cmd_path = result.stdout.decode().strip()
|
|
dep_msg = f" {dep_name}: {cmd_path} (system)"
|
|
self.logger.info(dep_msg)
|
|
if status_callback:
|
|
status_callback(dep_msg)
|
|
found = True
|
|
break
|
|
except Exception:
|
|
pass
|
|
if not found:
|
|
missing_deps.append(dep_name)
|
|
if dep_name in bundled_tools_list:
|
|
self.logger.warning(f" {dep_name}: NOT FOUND (neither bundled nor system)")
|
|
else:
|
|
self.logger.warning(f" {dep_name}: NOT FOUND (system only - not bundled)")
|
|
|
|
if missing_deps:
|
|
download_deps = [d for d in missing_deps if d in ['wget', 'curl', 'aria2c']]
|
|
verbose = getattr(self, 'verbose', False)
|
|
if verbose:
|
|
critical_deps = [d for d in missing_deps if d not in ['aria2c']]
|
|
if critical_deps:
|
|
self.logger.warning(f"Missing critical winetricks dependencies: {', '.join(critical_deps)}")
|
|
self.logger.warning("Winetricks may fail if these are required for component installation")
|
|
optional_deps = [d for d in missing_deps if d in ['aria2c']]
|
|
if optional_deps:
|
|
self.logger.info(f"Optional dependencies not found (will use alternatives): {', '.join(optional_deps)}")
|
|
all_downloaders = {'wget', 'curl', 'aria2c'}
|
|
if set(download_deps) == all_downloaders:
|
|
self.logger.error("=" * 80)
|
|
self.logger.error("CRITICAL: No download tools found (wget, curl, or aria2c)")
|
|
self.logger.error("Winetricks requires at least ONE download tool to install components")
|
|
self.logger.error("")
|
|
self.logger.error("SOLUTION: Install one of the following:")
|
|
self.logger.error(" - aria2c (preferred): sudo apt install aria2 # or equivalent for your distro")
|
|
self.logger.error(" - curl: sudo apt install curl # or equivalent for your distro")
|
|
self.logger.error(" - wget: sudo apt install wget # or equivalent for your distro")
|
|
self.logger.error("=" * 80)
|
|
elif getattr(self, 'verbose', False):
|
|
self.logger.warning("Critical dependencies: wget/curl (download), unzip/7z (extract)")
|
|
elif getattr(self, 'verbose', False):
|
|
self.logger.info("All winetricks dependencies found")
|
|
if getattr(self, 'verbose', False):
|
|
self.logger.info("========================================")
|
|
|
|
from jackify.shared.paths import get_jackify_data_dir
|
|
jackify_cache_dir = get_jackify_data_dir() / 'winetricks_cache'
|
|
jackify_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
env['WINETRICKS_CACHE'] = str(jackify_cache_dir)
|
|
|
|
if specific_components is not None:
|
|
all_components = specific_components
|
|
self.logger.info(f"Installing specific components: {all_components}")
|
|
else:
|
|
all_components = ["fontsmooth=rgb", "xact", "xact_x64", "vcrun2022"]
|
|
self.logger.info(f"Installing default components: {all_components}")
|
|
|
|
if not all_components:
|
|
self.logger.info("No Wine components to install.")
|
|
if status_callback:
|
|
status_callback("No Wine components to install")
|
|
return (env, [])
|
|
|
|
components_to_install = self._reorder_components_for_installation(all_components)
|
|
self.logger.info(f"WINEPREFIX: {wineprefix}, Ordered Components: {components_to_install}")
|
|
if status_callback:
|
|
status_callback(f"Installing Wine components: {', '.join(components_to_install)}")
|
|
return (env, components_to_install)
|