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

348 lines
17 KiB
Python

"""Prefix setup methods for InstallWabbajackHandler (Mixin)."""
import logging
import os
import pwd
import re
import shutil
import subprocess
import time
from pathlib import Path
from typing import Optional, Tuple
from .ui_colors import COLOR_ERROR, COLOR_INFO, COLOR_PROMPT, COLOR_RESET
logger = logging.getLogger(__name__)
class WabbajackPrefixSetupMixin:
"""Mixin providing Wine prefix setup methods."""
def _find_steam_library_and_vdf_path(self) -> Tuple[Optional[Path], Optional[Path]]:
"""Finds the Steam library root and the path to the real libraryfolders.vdf."""
self.logger.info("Attempting to find Steam library and libraryfolders.vdf...")
try:
if isinstance(self.path_handler, type):
common_path = self.path_handler.find_steam_library()
else:
common_path = self.path_handler.find_steam_library()
if not common_path or not common_path.is_dir():
self.logger.error("Could not find Steam library common path.")
return None, None
library_root = common_path.parent.parent
self.logger.debug(f"Deduced library root: {library_root}")
vdf_path_candidates = [
library_root / 'config/libraryfolders.vdf',
library_root / '../config/libraryfolders.vdf'
]
real_vdf_path = None
for candidate in vdf_path_candidates:
resolved_candidate = candidate.resolve()
if resolved_candidate.is_file():
real_vdf_path = resolved_candidate
self.logger.info(f"Found real libraryfolders.vdf at: {real_vdf_path}")
break
if not real_vdf_path:
self.logger.error(f"Could not find libraryfolders.vdf within library root: {library_root}")
return None, None
return library_root, real_vdf_path
except Exception as e:
self.logger.error(f"Error finding Steam library/VDF: {e}", exc_info=True)
return None, None
def _link_steam_library_config(self) -> bool:
"""Creates the necessary directory structure and symlinks libraryfolders.vdf."""
if not self.compatdata_path:
self.logger.error("Cannot link Steam library: compatdata_path not set.")
return False
self.logger.info("Linking Steam library configuration (libraryfolders.vdf)...")
library_root, real_vdf_path = self._find_steam_library_and_vdf_path()
if not library_root or not real_vdf_path:
self.logger.error("Could not locate Steam library or libraryfolders.vdf.")
return False
target_dir = self.compatdata_path / 'pfx/drive_c/Program Files (x86)/Steam/config'
link_path = target_dir / 'libraryfolders.vdf'
try:
self.logger.debug(f"Backing up original libraryfolders.vdf: {real_vdf_path}")
if not self.filesystem_handler.backup_file(real_vdf_path):
self.logger.warning(f"Failed to backup {real_vdf_path}. Proceeding with caution.")
self.logger.warning("Failed to create backup of libraryfolders.vdf.")
self.logger.debug(f"Creating directory: {target_dir}")
os.makedirs(target_dir, exist_ok=True)
if link_path.is_symlink():
self.logger.debug(f"Removing existing symlink at {link_path}")
link_path.unlink()
elif link_path.exists():
self.logger.warning(f"Path {link_path} exists but is not a symlink. Removing it.")
if link_path.is_dir():
shutil.rmtree(link_path)
else:
link_path.unlink()
self.logger.info(f"Creating symlink from {real_vdf_path} to {link_path}")
os.symlink(real_vdf_path, link_path)
if link_path.is_symlink() and link_path.resolve() == real_vdf_path.resolve():
self.logger.info("Symlink created and verified successfully.")
return True
else:
self.logger.error("Symlink creation failed or verification failed.")
return False
except OSError as e:
self.logger.error(f"OSError during symlink creation: {e}", exc_info=True)
print(f"{COLOR_ERROR}Error creating Steam library link: {e}{COLOR_RESET}")
return False
except Exception as e:
self.logger.error(f"Unexpected error during symlink creation: {e}", exc_info=True)
print(f"{COLOR_ERROR}An unexpected error occurred: {e}{COLOR_RESET}")
return False
def _create_prefix_library_vdf(self) -> bool:
"""Creates the necessary directory structure and copies a modified libraryfolders.vdf."""
if not self.compatdata_path:
self.logger.error("Cannot create prefix VDF: compatdata_path not set.")
return False
self.logger.info("Creating modified libraryfolders.vdf in prefix...")
library_root, real_vdf_path = self._find_steam_library_and_vdf_path()
if not real_vdf_path:
self.logger.error("Could not locate real libraryfolders.vdf.")
return False
self.logger.debug(f"Backing up original libraryfolders.vdf: {real_vdf_path}")
if not self.filesystem_handler.backup_file(real_vdf_path):
self.logger.warning(f"Failed to backup {real_vdf_path}. Proceeding with caution.")
self.logger.warning("Failed to create backup of libraryfolders.vdf.")
target_dir = self.compatdata_path / 'pfx/drive_c/Program Files (x86)/Steam/config'
target_vdf_path = target_dir / 'libraryfolders.vdf'
try:
self.logger.debug(f"Reading content from {real_vdf_path}")
vdf_content = real_vdf_path.read_text(encoding='utf-8')
path_pattern = re.compile(r'("path"\s*")([^"]+)(")')
def replace_path(match):
prefix, linux_path_str, suffix = match.groups()
self.logger.debug(f"Found path entry to convert: {linux_path_str}")
try:
linux_path = Path(linux_path_str)
if self.filesystem_handler.is_sd_card(linux_path):
relative_sd_path_str = self.filesystem_handler._strip_sdcard_path_prefix(linux_path)
wine_path = "D:\\" + relative_sd_path_str.replace('/', '\\')
self.logger.debug(f" Converted SD card path: {linux_path_str} -> {wine_path}")
else:
wine_path = "Z:\\" + linux_path_str.strip('/').replace('/', '\\')
self.logger.debug(f" Converted standard path: {linux_path_str} -> {wine_path}")
wine_path_vdf_escaped = wine_path.replace('\\', '\\\\')
return f'{prefix}{wine_path_vdf_escaped}{suffix}'
except Exception as e:
self.logger.error(f"Error converting path '{linux_path_str}': {e}. Keeping original.")
return match.group(0)
modified_content = path_pattern.sub(replace_path, vdf_content)
if modified_content != vdf_content:
self.logger.info("Successfully converted Linux paths to Wine paths in VDF content.")
else:
self.logger.warning("VDF content unchanged after conversion attempt. Did it contain Linux paths?")
self.logger.debug(f"Ensuring target directory exists: {target_dir}")
os.makedirs(target_dir, exist_ok=True)
self.logger.info(f"Writing modified VDF content to {target_vdf_path}")
target_vdf_path.write_text(modified_content, encoding='utf-8')
if target_vdf_path.is_file():
self.logger.info("Prefix libraryfolders.vdf created successfully.")
return True
else:
self.logger.error("Failed to create prefix libraryfolders.vdf.")
return False
except Exception as e:
self.logger.error(f"Error processing or writing prefix libraryfolders.vdf: {e}", exc_info=True)
print(f"{COLOR_ERROR}An error occurred configuring the Steam library in the prefix: {e}{COLOR_RESET}")
return False
def _create_dotnet_cache_dir(self) -> bool:
"""Creates the dotnet_bundle_extract cache directory."""
if not self.install_path:
self.logger.error("Cannot create dotnet cache dir: install_path not set.")
return False
try:
username = pwd.getpwuid(os.getuid()).pw_name
except Exception as e:
self.logger.error(f"Could not determine username: {e}")
self.logger.error("Could not determine username to create cache directory.")
return False
cache_dir = self.install_path / 'home' / username / '.cache' / 'dotnet_bundle_extract'
self.logger.info(f"Creating dotnet bundle cache directory: {cache_dir}")
try:
os.makedirs(cache_dir, exist_ok=True)
self.logger.info("dotnet cache directory created successfully.")
return True
except OSError as e:
self.logger.error(f"Failed to create dotnet cache directory {cache_dir}: {e}", exc_info=True)
print(f"{COLOR_ERROR}Error creating dotnet cache directory: {e}{COLOR_RESET}")
return False
except Exception as e:
self.logger.error(f"Unexpected error creating dotnet cache directory: {e}", exc_info=True)
print(f"{COLOR_ERROR}An unexpected error occurred: {e}{COLOR_RESET}")
return False
def _check_and_prompt_flatpak_overrides(self):
"""Checks if Flatpak Steam needs filesystem overrides and prompts the user to apply them."""
self.logger.info("Checking for necessary Flatpak Steam filesystem overrides...")
is_flatpak_steam = False
if self.compatdata_path and ".var/app/com.valvesoftware.Steam" in str(self.compatdata_path):
is_flatpak_steam = True
self.logger.debug("Flatpak Steam detected based on compatdata path.")
if not is_flatpak_steam:
self.logger.info("Flatpak Steam not detected, skipping override check.")
return
paths_to_check = []
if self.install_path:
paths_to_check.append(self.install_path)
try:
all_libs = self.path_handler.get_all_steam_libraries()
paths_to_check.extend(all_libs)
except Exception as e:
self.logger.warning(f"Could not get all Steam libraries to check for overrides: {e}")
needed_overrides = set()
home_dir = Path.home()
flatpak_steam_data_dir = home_dir / ".var/app/com.valvesoftware.Steam"
for path in paths_to_check:
if not path:
continue
resolved_path = path.resolve()
is_outside_home = not str(resolved_path).startswith(str(home_dir))
is_outside_flatpak_data = not str(resolved_path).startswith(str(flatpak_steam_data_dir))
if is_outside_home and is_outside_flatpak_data:
parent_to_add = resolved_path.parent
while parent_to_add != parent_to_add.parent and len(str(parent_to_add)) > 1 and parent_to_add.name != 'home':
if parent_to_add.is_dir():
needed_overrides.add(str(parent_to_add))
self.logger.debug(f"Path {resolved_path} is outside sandbox. Adding parent {parent_to_add} to needed overrides.")
break
parent_to_add = parent_to_add.parent
if not needed_overrides:
self.logger.info("No external paths requiring Flatpak overrides detected.")
return
override_commands = []
for path_str in sorted(list(needed_overrides)):
override_commands.append(f"flatpak override --user --filesystem=\"{path_str}\" com.valvesoftware.Steam")
command_display = "\n".join([f" {cmd}" for cmd in override_commands])
print(f"\n{COLOR_PROMPT}--- Flatpak Steam Permissions ---{COLOR_RESET}")
print("Jackify has detected that you are using Flatpak Steam and have paths")
print("(e.g., Wabbajack install location or other Steam libraries) outside")
print("the standard Flatpak sandbox. For Wabbajack to access these locations,")
print("Steam needs the following filesystem permissions:")
print(f"{COLOR_INFO}{command_display}{COLOR_RESET}")
print("───────────────────────────────────────────────────────────────────")
try:
confirm = input(f"{COLOR_PROMPT}Do you want Jackify to apply these permissions now? (y/N): {COLOR_RESET}").lower().strip()
if confirm == 'y':
self.logger.info("User confirmed applying Flatpak overrides.")
success_count = 0
for cmd_str in override_commands:
self.logger.info(f"Executing: {cmd_str}")
try:
cmd_list = cmd_str.split()
result = subprocess.run(cmd_list, check=True, capture_output=True, text=True, timeout=30)
self.logger.debug(f"Override command successful: {result.stdout}")
success_count += 1
except FileNotFoundError:
print(f"{COLOR_ERROR}Error: 'flatpak' command not found. Cannot apply override.{COLOR_RESET}")
break
except subprocess.TimeoutExpired:
print(f"{COLOR_ERROR}Error: Flatpak override command timed out.{COLOR_RESET}")
except subprocess.CalledProcessError as e:
self.logger.error(f"Flatpak override failed: {cmd_str}. Error: {e.stderr}")
print(f"{COLOR_ERROR}Error applying override: {cmd_str}\n{e.stderr}{COLOR_RESET}")
except Exception as e:
self.logger.error(f"Unexpected error applying override {cmd_str}: {e}")
print(f"{COLOR_ERROR}An unexpected error occurred: {e}{COLOR_RESET}")
if success_count == len(override_commands):
print(f"{COLOR_INFO}Successfully applied necessary Flatpak permissions.{COLOR_RESET}")
else:
print(f"{COLOR_ERROR}Applied {success_count}/{len(override_commands)} permissions. Some overrides may have failed. Check logs.{COLOR_RESET}")
else:
self.logger.info("User declined applying Flatpak overrides.")
print("Permissions not applied. You may need to run the override command(s) manually")
print("if Wabbajack has issues accessing files or game installations.")
except KeyboardInterrupt:
print("\nOperation cancelled by user.")
self.logger.warning("User cancelled during Flatpak override prompt.")
except Exception as e:
self.logger.error(f"Error during Flatpak override prompt/execution: {e}")
def _disable_prefix_decoration(self) -> bool:
"""Disables window manager decoration in the Wine prefix using protontricks -c."""
if not self.final_appid:
self.logger.error("Cannot disable decoration: final_appid not set.")
return False
self.logger.info(f"Disabling window manager decoration for AppID {self.final_appid} via -c 'wine reg add...'")
command = 'wine reg add "HKCU\\Software\\Wine\\X11 Driver" /v Decorated /t REG_SZ /d N /f'
try:
if not hasattr(self, 'protontricks_handler') or not self.protontricks_handler:
self.logger.critical("ProtontricksHandler not initialized!")
self.logger.error("Internal Error: Protontricks handler not available.")
return False
result = self.protontricks_handler.run_protontricks(
'-c',
command,
self.final_appid
)
if result and result.returncode == 0:
self.logger.info("Successfully disabled window decoration (command returned 0).")
time.sleep(1)
return True
else:
err_msg = result.stderr if result else "Command execution failed or returned non-zero"
if result and not result.stderr and result.stdout:
err_msg += f"\nSTDOUT: {result.stdout}"
self.logger.error(f"Failed to disable window decoration via -c. Error: {err_msg}")
self.logger.error("Failed to disable window decoration via protontricks -c.")
return False
except Exception as e:
self.logger.error(f"Exception disabling window decoration: {e}", exc_info=True)
self.logger.error(f"Error disabling window decoration: {e}.")
return False