mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-07 22:47:45 +02:00
289 lines
12 KiB
Python
289 lines
12 KiB
Python
"""Prefix creation methods for AutomatedPrefixService (Mixin)."""
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
import logging
|
|
import os
|
|
import time
|
|
import subprocess
|
|
import re
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class PrefixCreationMixin:
|
|
"""Mixin providing prefix creation methods for AutomatedPrefixService."""
|
|
|
|
def _get_preferred_steam_root_and_type(self) -> tuple[Optional[Path], Optional[str]]:
|
|
"""Resolve the active Steam root/type using the shared v0.5 selector."""
|
|
from jackify.shared.steam_utils import (
|
|
STEAM_PREFERENCE_AUTO,
|
|
resolve_preferred_steam_installation,
|
|
)
|
|
from ..handlers.config_handler import ConfigHandler
|
|
|
|
preference = STEAM_PREFERENCE_AUTO
|
|
try:
|
|
preference = ConfigHandler().get("steam_install_preference", STEAM_PREFERENCE_AUTO)
|
|
except Exception:
|
|
logger.debug("Could not read steam_install_preference; falling back to auto", exc_info=True)
|
|
|
|
preferred_type, preferred_root = resolve_preferred_steam_installation(preference)
|
|
return preferred_root, preferred_type
|
|
|
|
def _get_library_roots_for_steam_root(self, steam_root: Path) -> list[Path]:
|
|
"""
|
|
Read library roots for one chosen Steam install only.
|
|
|
|
This avoids mixing native and Flatpak libraries in dual-install environments.
|
|
"""
|
|
roots: list[Path] = [steam_root]
|
|
vdf_path = steam_root / "config" / "libraryfolders.vdf"
|
|
if not vdf_path.is_file():
|
|
return roots
|
|
|
|
try:
|
|
text = vdf_path.read_text(encoding="utf-8", errors="ignore")
|
|
for match in re.finditer(r'"path"\s*"([^"]+)"', text):
|
|
raw_path = match.group(1).replace("\\\\", "\\")
|
|
lib_root = Path(raw_path).expanduser()
|
|
try:
|
|
resolved = lib_root.resolve()
|
|
except (OSError, RuntimeError):
|
|
resolved = lib_root
|
|
if resolved not in roots:
|
|
roots.append(resolved)
|
|
except Exception:
|
|
logger.debug("Failed reading libraryfolders.vdf for %s", steam_root, exc_info=True)
|
|
return roots
|
|
|
|
def _get_compatdata_path_for_appid(self, appid: int) -> Optional[Path]:
|
|
"""
|
|
Get the compatdata path for a given AppID.
|
|
|
|
First tries to find existing compatdata, then constructs path from libraryfolders.vdf
|
|
for creating new prefixes.
|
|
|
|
Args:
|
|
appid: The AppID to get the path for
|
|
|
|
Returns:
|
|
Path to the compatdata directory, or None if not found
|
|
"""
|
|
from ..handlers.path_handler import PathHandler
|
|
|
|
# First, try to find existing compatdata
|
|
compatdata_path = PathHandler.find_compat_data(str(appid))
|
|
if compatdata_path:
|
|
return compatdata_path
|
|
|
|
# Prefix doesn't exist yet - derive it from the selected active Steam root,
|
|
# not from a mixed native/Flatpak library list.
|
|
preferred_root, _preferred_type = self._get_preferred_steam_root_and_type()
|
|
if preferred_root:
|
|
compatdata_base = preferred_root / "steamapps" / "compatdata"
|
|
return compatdata_base / str(appid)
|
|
|
|
# Only fallback if VDF parsing completely fails
|
|
logger.warning("Could not get library paths from libraryfolders.vdf, using fallback locations")
|
|
fallback_bases = [
|
|
Path.home() / ".var/app/com.valvesoftware.Steam/data/Steam/steamapps/compatdata",
|
|
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/compatdata",
|
|
Path.home() / ".steam/steam/steamapps/compatdata",
|
|
Path.home() / ".local/share/Steam/steamapps/compatdata",
|
|
]
|
|
|
|
for base_path in fallback_bases:
|
|
if base_path.is_dir():
|
|
return base_path / str(appid)
|
|
|
|
return None
|
|
|
|
def verify_prefix_creation(self, prefix_path: Path) -> bool:
|
|
"""
|
|
Verify that the prefix was created successfully.
|
|
|
|
Args:
|
|
prefix_path: Path to the prefix directory
|
|
|
|
Returns:
|
|
True if prefix is valid, False otherwise
|
|
"""
|
|
try:
|
|
logger.info(f"Verifying prefix: {prefix_path}")
|
|
|
|
# Check if prefix exists and has proper structure
|
|
if not prefix_path.exists():
|
|
logger.error("Prefix directory does not exist")
|
|
return False
|
|
|
|
pfx_dir = prefix_path / "pfx"
|
|
if not pfx_dir.exists():
|
|
logger.error("Prefix exists but no pfx subdirectory")
|
|
return False
|
|
|
|
# Check for key Wine files
|
|
system_reg = pfx_dir / "system.reg"
|
|
user_reg = pfx_dir / "user.reg"
|
|
drive_c = pfx_dir / "drive_c"
|
|
|
|
if not system_reg.exists():
|
|
logger.error("No system.reg found in prefix")
|
|
return False
|
|
|
|
if not user_reg.exists():
|
|
logger.error("No user.reg found in prefix")
|
|
return False
|
|
|
|
if not drive_c.exists():
|
|
logger.error("No drive_c directory found in prefix")
|
|
return False
|
|
|
|
logger.info("Prefix structure verified successfully")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error verifying prefix: {e}")
|
|
return False
|
|
|
|
def wait_for_prefix_completion(self, prefix_id: str, timeout: int = 60) -> bool:
|
|
"""
|
|
Wait for system.reg to stop growing (indicates prefix creation is complete).
|
|
|
|
Args:
|
|
prefix_id: The Steam prefix ID to monitor
|
|
timeout: Maximum seconds to wait
|
|
|
|
Returns:
|
|
True if prefix creation completed, False if timeout
|
|
"""
|
|
try:
|
|
prefix_path = Path.home() / f".local/share/Steam/steamapps/compatdata/{prefix_id}"
|
|
system_reg = prefix_path / "pfx/system.reg"
|
|
|
|
logger.info(f"Monitoring prefix completion: {system_reg}")
|
|
|
|
last_size = 0
|
|
stable_count = 0
|
|
|
|
for i in range(timeout):
|
|
if system_reg.exists():
|
|
current_size = system_reg.stat().st_size
|
|
logger.debug(f"system.reg size: {current_size} bytes")
|
|
|
|
if current_size == last_size:
|
|
stable_count += 1
|
|
if stable_count >= 3: # Stable for 3 seconds
|
|
logger.info(" system.reg size stable - prefix creation complete")
|
|
return True
|
|
else:
|
|
stable_count = 0
|
|
last_size = current_size
|
|
|
|
time.sleep(1)
|
|
|
|
logger.warning(f"Timeout waiting for prefix completion after {timeout} seconds")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error monitoring prefix completion: {e}")
|
|
return False
|
|
|
|
def create_prefix_with_proton_wrapper(self, appid: int) -> bool:
|
|
"""
|
|
Create a Proton prefix directly using Proton's wrapper and STEAM_COMPAT_DATA_PATH.
|
|
|
|
Args:
|
|
appid: The AppID to create the prefix for
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
try:
|
|
# Determine Steam locations from the selected active Steam install only.
|
|
steam_root, steam_type = self._get_preferred_steam_root_and_type()
|
|
if not steam_root:
|
|
logger.error("Could not determine active Steam root for prefix creation")
|
|
return False
|
|
|
|
if not steam_root.is_dir():
|
|
logger.error("Preferred Steam root does not exist: %s", steam_root)
|
|
return False
|
|
|
|
compatdata_dir = steam_root / "steamapps" / "compatdata"
|
|
proton_common_dir = steam_root / "steamapps" / "common"
|
|
logger.info(
|
|
"Prefix creation using preferred Steam install: type=%s root=%s",
|
|
steam_type or "unknown",
|
|
steam_root,
|
|
)
|
|
|
|
# Ensure compatdata root exists and is a directory we actually want to use
|
|
if not compatdata_dir.is_dir():
|
|
logger.error(f"Compatdata root does not exist: {compatdata_dir}. Aborting prefix creation.")
|
|
return False
|
|
|
|
# Find a Proton wrapper to use
|
|
proton_path = self._find_proton_binary(proton_common_dir)
|
|
if not proton_path:
|
|
logger.error("No Proton wrapper found")
|
|
return False
|
|
|
|
# Set up environment variables
|
|
env = os.environ.copy()
|
|
env['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = str(steam_root)
|
|
env['STEAM_COMPAT_DATA_PATH'] = str(compatdata_dir / str(abs(appid)))
|
|
# Suppress GUI windows using jackify-engine's proven approach
|
|
env['DISPLAY'] = ''
|
|
env['WAYLAND_DISPLAY'] = ''
|
|
env['WINEDEBUG'] = '-all'
|
|
env['WINEDLLOVERRIDES'] = 'msdia80.dll=n;conhost.exe=d;cmd.exe=d'
|
|
|
|
# Create the compatdata directory for this AppID (but never the whole tree)
|
|
compat_dir = compatdata_dir / str(abs(appid))
|
|
compat_dir.mkdir(exist_ok=True)
|
|
|
|
logger.info(f"Creating Proton prefix for AppID {appid}")
|
|
logger.info(f"STEAM_COMPAT_CLIENT_INSTALL_PATH={env['STEAM_COMPAT_CLIENT_INSTALL_PATH']}")
|
|
logger.info(f"STEAM_COMPAT_DATA_PATH={env['STEAM_COMPAT_DATA_PATH']}")
|
|
|
|
# Run proton run wineboot -u to initialize the prefix
|
|
cmd = [str(proton_path), 'run', 'wineboot', '-u']
|
|
logger.info(f"Running: {' '.join(cmd)}")
|
|
|
|
# Adjust timeout for SD card installations on Steam Deck (slower I/O)
|
|
from ..services.platform_detection_service import PlatformDetectionService
|
|
platform_service = PlatformDetectionService.get_instance()
|
|
is_steamdeck_sdcard = (platform_service.is_steamdeck and
|
|
str(proton_path).startswith('/run/media/'))
|
|
timeout = 180 if is_steamdeck_sdcard else 60
|
|
if is_steamdeck_sdcard:
|
|
logger.info(f"Using extended timeout ({timeout}s) for Steam Deck SD card Proton installation")
|
|
|
|
# Use jackify-engine's approach: UseShellExecute=false, CreateNoWindow=true equivalent
|
|
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=timeout,
|
|
shell=False, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
|
logger.info(f"Proton exit code: {result.returncode}")
|
|
|
|
if result.stdout:
|
|
logger.info(f"stdout: {result.stdout.strip()[:500]}")
|
|
if result.stderr:
|
|
logger.info(f"stderr: {result.stderr.strip()[:500]}")
|
|
|
|
# Give a moment for files to land
|
|
time.sleep(3)
|
|
|
|
# Check if prefix was created
|
|
pfx = compat_dir / 'pfx'
|
|
if pfx.exists():
|
|
logger.info(f" Proton prefix created at: {pfx}")
|
|
return True
|
|
else:
|
|
logger.warning(f"Proton prefix not found at: {pfx}")
|
|
return False
|
|
|
|
except subprocess.TimeoutExpired:
|
|
logger.warning("Proton timed out; prefix may still be initializing")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"Error creating prefix: {e}")
|
|
return False
|