mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 11:37:01 +01:00
263 lines
11 KiB
Python
263 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Winetricks Handler Module
|
|
Handles wine component installation using bundled winetricks
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional, List
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WinetricksHandler:
|
|
"""
|
|
Handles wine component installation using bundled winetricks
|
|
"""
|
|
|
|
def __init__(self, logger=None):
|
|
self.logger = logger or logging.getLogger(__name__)
|
|
self.winetricks_path = self._get_bundled_winetricks_path()
|
|
|
|
def _get_bundled_winetricks_path(self) -> Optional[str]:
|
|
"""
|
|
Get the path to the bundled winetricks script following AppImage best practices
|
|
"""
|
|
possible_paths = []
|
|
|
|
# AppImage environment - use APPDIR (standard AppImage best practice)
|
|
if os.environ.get('APPDIR'):
|
|
appdir_path = os.path.join(os.environ['APPDIR'], 'opt', 'jackify', 'tools', 'winetricks')
|
|
possible_paths.append(appdir_path)
|
|
|
|
# Development environment - relative to module location
|
|
module_dir = Path(__file__).parent.parent.parent # Go from handlers/ up to jackify/
|
|
dev_path = module_dir / 'tools' / 'winetricks'
|
|
possible_paths.append(str(dev_path))
|
|
|
|
# Try each path until we find one that works
|
|
for path in possible_paths:
|
|
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
self.logger.debug(f"Found bundled winetricks at: {path}")
|
|
return str(path)
|
|
|
|
self.logger.error(f"Bundled winetricks not found. Tried paths: {possible_paths}")
|
|
return None
|
|
|
|
def _get_bundled_cabextract(self) -> Optional[str]:
|
|
"""
|
|
Get the path to the bundled cabextract binary, checking same locations as winetricks
|
|
"""
|
|
possible_paths = []
|
|
|
|
# AppImage environment - same pattern as winetricks detection
|
|
if os.environ.get('APPDIR'):
|
|
appdir_path = os.path.join(os.environ['APPDIR'], 'opt', 'jackify', 'tools', 'cabextract')
|
|
possible_paths.append(appdir_path)
|
|
|
|
# Development environment - relative to module location, same as winetricks
|
|
module_dir = Path(__file__).parent.parent.parent # Go from handlers/ up to jackify/
|
|
dev_path = module_dir / 'tools' / 'cabextract'
|
|
possible_paths.append(str(dev_path))
|
|
|
|
# Try each path until we find one that works
|
|
for path in possible_paths:
|
|
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
self.logger.debug(f"Found bundled cabextract at: {path}")
|
|
return str(path)
|
|
|
|
# Fallback to system PATH
|
|
try:
|
|
import shutil
|
|
system_cabextract = shutil.which('cabextract')
|
|
if system_cabextract:
|
|
self.logger.debug(f"Using system cabextract: {system_cabextract}")
|
|
return system_cabextract
|
|
except Exception:
|
|
pass
|
|
|
|
self.logger.warning("Bundled cabextract not found in tools directory")
|
|
return None
|
|
|
|
def is_available(self) -> bool:
|
|
"""
|
|
Check if winetricks is available and ready to use
|
|
"""
|
|
if not self.winetricks_path:
|
|
self.logger.error("Bundled winetricks not found")
|
|
return False
|
|
|
|
try:
|
|
env = os.environ.copy()
|
|
result = subprocess.run(
|
|
[self.winetricks_path, '--version'],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=10
|
|
)
|
|
if result.returncode == 0:
|
|
self.logger.debug(f"Winetricks version: {result.stdout.strip()}")
|
|
return True
|
|
else:
|
|
self.logger.error(f"Winetricks --version failed: {result.stderr}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Error testing winetricks: {e}")
|
|
return False
|
|
|
|
def install_wine_components(self, wineprefix: str, game_var: str, specific_components: Optional[List[str]] = None) -> bool:
|
|
"""
|
|
Install the specified Wine components into the given prefix using winetricks.
|
|
If specific_components is None, use the default set (fontsmooth=rgb, xact, xact_x64, vcrun2022).
|
|
"""
|
|
if not self.is_available():
|
|
self.logger.error("Winetricks is not available")
|
|
return False
|
|
|
|
env = os.environ.copy()
|
|
env['WINEDEBUG'] = '-all' # Suppress Wine debug output
|
|
env['WINEPREFIX'] = wineprefix
|
|
env['WINETRICKS_GUI'] = 'none' # Suppress GUI popups
|
|
# Less aggressive popup suppression - don't completely disable display
|
|
if 'DISPLAY' in env:
|
|
# Keep DISPLAY but add window manager hints to prevent focus stealing
|
|
env['WINEDLLOVERRIDES'] = 'winemenubuilder.exe=d' # Disable Wine menu integration
|
|
else:
|
|
# No display available anyway
|
|
env['DISPLAY'] = ''
|
|
|
|
# Force winetricks to use Proton wine binary - NEVER fall back to system wine
|
|
try:
|
|
from ..handlers.config_handler import ConfigHandler
|
|
from ..handlers.wine_utils import WineUtils
|
|
|
|
config = ConfigHandler()
|
|
user_proton_path = config.get('proton_path', 'auto')
|
|
|
|
# If user selected a specific Proton, try that first
|
|
wine_binary = None
|
|
if user_proton_path != 'auto':
|
|
# Check if user-selected Proton still exists
|
|
if os.path.exists(user_proton_path):
|
|
# Resolve symlinks to handle ~/.steam/steam -> ~/.local/share/Steam
|
|
resolved_proton_path = os.path.realpath(user_proton_path)
|
|
|
|
# Check for wine binary in different Proton structures
|
|
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}")
|
|
|
|
# Fall back to auto-detection if user selection failed or is 'auto'
|
|
if not wine_binary:
|
|
self.logger.info("Falling back to automatic Proton detection")
|
|
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']}")
|
|
|
|
if not wine_binary:
|
|
self.logger.error("Cannot run winetricks: No compatible Proton version found")
|
|
return False
|
|
|
|
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 False
|
|
|
|
env['WINE'] = str(wine_binary)
|
|
self.logger.info(f"Using Proton wine binary for winetricks: {wine_binary}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Cannot run winetricks: Failed to get Proton wine binary: {e}")
|
|
return False
|
|
|
|
# Set up bundled cabextract for winetricks
|
|
bundled_cabextract = self._get_bundled_cabextract()
|
|
if bundled_cabextract:
|
|
env['PATH'] = f"{os.path.dirname(bundled_cabextract)}:{env.get('PATH', '')}"
|
|
self.logger.info(f"Using bundled cabextract: {bundled_cabextract}")
|
|
else:
|
|
self.logger.warning("Bundled cabextract not found, relying on system PATH")
|
|
|
|
# Set winetricks cache to jackify_data_dir for self-containment
|
|
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:
|
|
components_to_install = specific_components
|
|
self.logger.info(f"Installing specific components: {components_to_install}")
|
|
else:
|
|
components_to_install = ["fontsmooth=rgb", "xact", "xact_x64", "vcrun2022"]
|
|
self.logger.info(f"Installing default components: {components_to_install}")
|
|
|
|
if not components_to_install:
|
|
self.logger.info("No Wine components to install.")
|
|
return True
|
|
|
|
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Components: {components_to_install}")
|
|
|
|
max_attempts = 3
|
|
for attempt in range(1, max_attempts + 1):
|
|
if attempt > 1:
|
|
self.logger.warning(f"Retrying component installation (attempt {attempt}/{max_attempts})...")
|
|
self._cleanup_wine_processes()
|
|
|
|
try:
|
|
# Build winetricks command - using --unattended for silent installation
|
|
cmd = [self.winetricks_path, '--unattended'] + components_to_install
|
|
|
|
self.logger.debug(f"Running: {' '.join(cmd)}")
|
|
self.logger.debug(f"Environment WINE={env.get('WINE', 'NOT SET')}")
|
|
self.logger.debug(f"Environment DISPLAY={env.get('DISPLAY', 'NOT SET')}")
|
|
self.logger.debug(f"Environment WINEPREFIX={env.get('WINEPREFIX', 'NOT SET')}")
|
|
result = subprocess.run(
|
|
cmd,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
timeout=600
|
|
)
|
|
|
|
self.logger.debug(f"Winetricks output: {result.stdout}")
|
|
if result.returncode == 0:
|
|
self.logger.info("Wine Component installation command completed successfully.")
|
|
return True
|
|
else:
|
|
self.logger.error(f"Winetricks command failed (Attempt {attempt}/{max_attempts}). Return Code: {result.returncode}")
|
|
self.logger.error(f"Stdout: {result.stdout.strip()}")
|
|
self.logger.error(f"Stderr: {result.stderr.strip()}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error during winetricks run (Attempt {attempt}/{max_attempts}): {e}", exc_info=True)
|
|
|
|
self.logger.error(f"Failed to install Wine components after {max_attempts} attempts.")
|
|
return False
|
|
|
|
def _cleanup_wine_processes(self):
|
|
"""
|
|
Internal method to clean up wine processes during component installation
|
|
Only cleanup winetricks processes - NEVER kill all wine processes
|
|
"""
|
|
try:
|
|
# Only cleanup winetricks processes - do NOT kill other wine apps
|
|
subprocess.run("pkill -f winetricks", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
self.logger.debug("Cleaned up winetricks processes only")
|
|
except Exception as e:
|
|
self.logger.error(f"Error cleaning up winetricks processes: {e}") |