Files
Jackify/jackify/backend/handlers/protontricks_detection.py
2026-02-07 18:26:54 +00:00

196 lines
9.6 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Protontricks detection and version mixin.
Extracted from protontricks_handler for file-size and domain separation.
"""
import os
import re
import subprocess
from pathlib import Path
import shutil
import logging
from typing import Optional, List
import sys
from .subprocess_utils import get_clean_subprocess_env
class ProtontricksDetectionMixin:
"""Mixin providing protontricks detection, Steam dir, bundled paths, and version checks."""
def _get_steam_dir_from_libraryfolders(self) -> Optional[Path]:
"""Determine Steam installation directory from libraryfolders.vdf."""
from ..handlers.path_handler import PathHandler
vdf_paths = [
Path.home() / ".steam/steam/config/libraryfolders.vdf",
Path.home() / ".local/share/Steam/config/libraryfolders.vdf",
Path.home() / ".steam/root/config/libraryfolders.vdf",
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf",
Path.home() / ".var/app/com.valvesoftware.Steam/data/Steam/config/libraryfolders.vdf",
]
for vdf_path in vdf_paths:
if vdf_path.is_file():
steam_dir = vdf_path.parent.parent
if (steam_dir / "steamapps").exists():
self.logger.debug(f"Determined STEAM_DIR from libraryfolders.vdf: {steam_dir}")
return steam_dir
library_paths = PathHandler.get_all_steam_library_paths()
if library_paths:
first_lib = library_paths[0]
if '.var/app/com.valvesoftware.Steam' in str(first_lib):
data_steam = Path.home() / ".var/app/com.valvesoftware.Steam/data/Steam"
if (data_steam / "steamapps").exists():
self.logger.debug(f"Determined STEAM_DIR from Flatpak data path: {data_steam}")
return data_steam
if (first_lib / "steamapps").exists():
self.logger.debug(f"Determined STEAM_DIR from Flatpak library path: {first_lib}")
return first_lib
elif (first_lib / "steamapps").exists():
self.logger.debug(f"Determined STEAM_DIR from native library path: {first_lib}")
return first_lib
self.logger.warning("Could not determine STEAM_DIR from libraryfolders.vdf")
return None
def _get_bundled_winetricks_path(self) -> Optional[Path]:
"""Get path to bundled winetricks (AppImage and dev)."""
possible_paths = []
if os.environ.get('APPDIR'):
possible_paths.append(Path(os.environ['APPDIR']) / 'opt' / 'jackify' / 'tools' / 'winetricks')
module_dir = Path(__file__).parent.parent.parent
possible_paths.append(module_dir / 'tools' / 'winetricks')
for path in possible_paths:
if path.exists() and os.access(path, os.X_OK):
self.logger.debug(f"Found bundled winetricks at: {path}")
return path
self.logger.warning(f"Bundled winetricks not found. Tried paths: {possible_paths}")
return None
def _get_bundled_cabextract_path(self) -> Optional[Path]:
"""Get path to bundled cabextract (AppImage and dev)."""
possible_paths = []
if os.environ.get('APPDIR'):
possible_paths.append(Path(os.environ['APPDIR']) / 'opt' / 'jackify' / 'tools' / 'cabextract')
module_dir = Path(__file__).parent.parent.parent
possible_paths.append(module_dir / 'tools' / 'cabextract')
for path in possible_paths:
if path.exists() and os.access(path, os.X_OK):
self.logger.debug(f"Found bundled cabextract at: {path}")
return path
self.logger.warning(f"Bundled cabextract not found. Tried paths: {possible_paths}")
return None
def _get_bundled_protontricks_wrapper_path(self) -> Optional[str]:
"""Return path to bundled protontricks wrapper script if any. Returns None to use python -m fallback."""
return None
def _get_clean_subprocess_env(self):
"""Create clean environment for subprocess (remove AppImage/bundle vars)."""
env = get_clean_subprocess_env()
if 'LD_LIBRARY_PATH_ORIG' in env:
env['LD_LIBRARY_PATH'] = env['LD_LIBRARY_PATH_ORIG']
else:
env.pop('LD_LIBRARY_PATH', None)
if 'DYLD_LIBRARY_PATH' in env and hasattr(sys, '_MEIPASS'):
dyld_entries = env['DYLD_LIBRARY_PATH'].split(os.pathsep)
cleaned_dyld = [p for p in dyld_entries if not p.startswith(sys._MEIPASS)]
if cleaned_dyld:
env['DYLD_LIBRARY_PATH'] = os.pathsep.join(cleaned_dyld)
else:
env.pop('DYLD_LIBRARY_PATH', None)
return env
def _get_native_steam_service(self):
"""Get native Steam operations service instance."""
if self._native_steam_service is None:
from ..services.native_steam_operations_service import NativeSteamOperationsService
self._native_steam_service = NativeSteamOperationsService(steamdeck=self.steamdeck)
return self._native_steam_service
def detect_protontricks(self):
"""Detect if protontricks is installed (native or flatpak). Returns True if found."""
self.logger.debug("Detecting if protontricks is installed...")
protontricks_path_which = shutil.which("protontricks")
self.flatpak_path = shutil.which("flatpak")
if protontricks_path_which:
try:
with open(protontricks_path_which, 'r') as f:
content = f.read()
if "flatpak run" in content:
self.logger.debug(f"Detected Protontricks is a Flatpak wrapper at {protontricks_path_which}")
self.which_protontricks = 'flatpak'
else:
self.logger.info(f"Native Protontricks found at {protontricks_path_which}")
self.which_protontricks = 'native'
self.protontricks_path = protontricks_path_which
return True
except Exception as e:
self.logger.error(f"Error reading protontricks executable: {e}")
try:
env = self._get_clean_subprocess_env()
result_user = subprocess.run(
["flatpak", "list", "--user"],
capture_output=True, text=True, env=env
)
if result_user.returncode == 0 and "com.github.Matoking.protontricks" in result_user.stdout:
self.logger.info("Flatpak Protontricks is installed (user-level)")
self.which_protontricks = 'flatpak'
self.flatpak_install_type = 'user'
return True
result_system = subprocess.run(
["flatpak", "list", "--system"],
capture_output=True, text=True, env=env
)
if result_system.returncode == 0 and "com.github.Matoking.protontricks" in result_system.stdout:
self.logger.info("Flatpak Protontricks is installed (system-level)")
self.which_protontricks = 'flatpak'
self.flatpak_install_type = 'system'
return True
except FileNotFoundError:
self.logger.warning("'flatpak' command not found. Cannot check for Flatpak Protontricks.")
except Exception as e:
self.logger.error(f"Unexpected error checking flatpak: {e}")
self.logger.warning("Protontricks not found (native or flatpak).")
return False
def _get_flatpak_run_args(self) -> List[str]:
"""Get flatpak run arguments (--user or --system)."""
base_args = ["flatpak", "run"]
if self.flatpak_install_type == 'user':
base_args.append("--user")
elif self.flatpak_install_type == 'system':
base_args.append("--system")
return base_args
def _get_flatpak_alias_string(self, command=None) -> str:
"""Get flatpak alias string for bashrc."""
flag = f"--{self.flatpak_install_type}" if self.flatpak_install_type else ""
if command:
return f"flatpak run {flag} --command={command} com.github.Matoking.protontricks" if flag else f"flatpak run --command={command} com.github.Matoking.protontricks"
return f"flatpak run {flag} com.github.Matoking.protontricks" if flag else "flatpak run com.github.Matoking.protontricks"
def check_protontricks_version(self):
"""Check if protontricks version is sufficient (>= 1.12). Returns True if OK."""
try:
if self.which_protontricks == 'flatpak':
cmd = self._get_flatpak_run_args() + ["com.github.Matoking.protontricks", "-V"]
else:
cmd = ["protontricks", "-V"]
result = subprocess.run(cmd, capture_output=True, text=True)
version_str = result.stdout.split(' ')[1].strip('()')
cleaned_version = re.sub(r'[^0-9.]', '', version_str)
self.protontricks_version = cleaned_version
version_parts = cleaned_version.split('.')
if len(version_parts) >= 2:
major, minor = int(version_parts[0]), int(version_parts[1])
if major < 1 or (major == 1 and minor < 12):
self.logger.error(f"Protontricks version {cleaned_version} is too old. Version 1.12.0 or newer is required.")
return False
return True
self.logger.error(f"Could not parse protontricks version: {cleaned_version}")
return False
except Exception as e:
self.logger.error(f"Error checking protontricks version: {e}")
return False