mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:17:58 +02:00
348 lines
17 KiB
Python
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
|