mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-14 02:13:43 +02:00
Release v0.7.2 - TTW Linux Installer 0.2.0
This commit is contained in:
@@ -64,7 +64,7 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
|
||||
"modlist_downloads_base_dir": os.path.expanduser("~/Games/Modlist_Downloads"), # Configurable base directory for downloads
|
||||
"jackify_data_dir": None, # Configurable Jackify data directory (default: ~/Jackify)
|
||||
"use_winetricks_for_components": True, # DEPRECATED: Migrated to component_installation_method. Kept for backward compatibility.
|
||||
"component_installation_method": "winetricks", # "winetricks" (default) or "system_protontricks"
|
||||
"component_installation_method": "native", # "native" (default), "winetricks", or "system_protontricks"
|
||||
"game_proton_path": None, # Proton version for game shortcuts (can be any Proton 9+), separate from install proton
|
||||
"proton_path": None, # Install Proton path (for jackify-engine) - None means auto-detect
|
||||
"proton_version": None, # Install Proton version name - None means auto-detect
|
||||
@@ -140,7 +140,7 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
|
||||
Handles breaking changes and data format updates
|
||||
"""
|
||||
current_version = self.settings.get("version", "0.0.0")
|
||||
target_version = "0.2.0"
|
||||
target_version = "0.7.2"
|
||||
|
||||
if current_version == target_version:
|
||||
return
|
||||
@@ -191,6 +191,20 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
|
||||
self.save_config()
|
||||
logger.info("Config migration completed")
|
||||
|
||||
# Migration: pre-0.7.2 -> v0.7.2
|
||||
# component_installation_method semantics changed: the old lone "winetricks" value
|
||||
# actually meant "native install, winetricks fallback for unsupported components" -
|
||||
# that behavior is now the "native" option. Explicit full winetricks/protontricks
|
||||
# bypass modes are new in 0.7.2, so old configs must be remapped once rather than
|
||||
# silently reinterpreted as the new bypass meaning.
|
||||
if version.parse(current_version) < version.parse("0.7.2"):
|
||||
if self.settings.get("component_installation_method") == "winetricks":
|
||||
self.settings["component_installation_method"] = "native"
|
||||
logger.info("Migrated component_installation_method 'winetricks' -> 'native'")
|
||||
self.settings["version"] = target_version
|
||||
self.save_config()
|
||||
logger.info("Config migration completed")
|
||||
|
||||
def _read_config_from_disk(self):
|
||||
"""
|
||||
Read configuration directly from disk without caching.
|
||||
@@ -305,6 +319,7 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
|
||||
"""Set preferred resolution"""
|
||||
resolution = f"{width}x{height}"
|
||||
self.settings["resolution"] = resolution
|
||||
self._dirty_keys.add("resolution")
|
||||
logger.debug(f"Set resolution to: {resolution}")
|
||||
return True
|
||||
|
||||
@@ -315,6 +330,7 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
|
||||
def set_last_modlist(self, modlist_name):
|
||||
"""Save the last selected modlist"""
|
||||
self.settings["last_selected_modlist"] = modlist_name
|
||||
self._dirty_keys.add("last_selected_modlist")
|
||||
logger.debug(f"Set last selected modlist to: {modlist_name}")
|
||||
return True
|
||||
|
||||
@@ -325,6 +341,7 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
|
||||
def set_protontricks_path(self, path):
|
||||
"""Set the path to protontricks executable"""
|
||||
self.settings["protontricks_path"] = path
|
||||
self._dirty_keys.add("protontricks_path")
|
||||
logger.debug(f"Set protontricks path to: {path}")
|
||||
return True
|
||||
|
||||
@@ -350,7 +367,7 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
|
||||
# Clear resolution if 'Leave unchanged' or empty
|
||||
self.settings["resolution"] = None
|
||||
logger.debug("Resolution cleared")
|
||||
|
||||
self._dirty_keys.add("resolution")
|
||||
return self.save_config()
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving resolution: {e}")
|
||||
@@ -392,6 +409,7 @@ class ConfigHandler(ConfigEncryptionMixin, ConfigDirectoriesMixin, ConfigProtonM
|
||||
"""
|
||||
try:
|
||||
self.settings["resolution"] = None
|
||||
self._dirty_keys.add("resolution")
|
||||
logger.debug("Resolution cleared from configuration")
|
||||
return self.save_config()
|
||||
except Exception as e:
|
||||
|
||||
@@ -183,35 +183,6 @@ class FileSystemHandler(
|
||||
logger.warning(f"No suitable backup found for {original_file_path} in {backup_dir} or adjacent.")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def set_permissions(path: Path, permissions: int = 0o755, recursive: bool = True) -> bool:
|
||||
"""Set file or directory permissions (non-sudo)."""
|
||||
try:
|
||||
if not path.exists():
|
||||
logger.error(f"Cannot set permissions: Path does not exist - {path}")
|
||||
return False
|
||||
|
||||
if recursive and path.is_dir():
|
||||
for root, dirs, files in os.walk(path):
|
||||
try:
|
||||
os.chmod(root, 0o755)
|
||||
except Exception as dir_e:
|
||||
logger.warning(f"Failed to chmod dir {root}: {dir_e}")
|
||||
for file in files:
|
||||
try:
|
||||
os.chmod(os.path.join(root, file), 0o644)
|
||||
except Exception as file_e:
|
||||
logger.warning(f"Failed to chmod file {os.path.join(root, file)}: {file_e}")
|
||||
elif path.is_file():
|
||||
os.chmod(path, 0o644 if permissions == 0o755 else permissions)
|
||||
elif path.is_dir():
|
||||
os.chmod(path, permissions) # Set specific perm for top-level dir if not recursive
|
||||
logger.debug(f"Set permissions for {path} (recursive={recursive})")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set permissions for {path}: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_permissions(path: Path) -> Optional[int]:
|
||||
"""Get file or directory permissions (last 3 octal digits)."""
|
||||
|
||||
@@ -24,6 +24,8 @@ class FilesystemOwnershipMixin:
|
||||
for root, dirs, files in os.walk(path):
|
||||
for name in dirs + files:
|
||||
full_path = os.path.join(root, name)
|
||||
if os.path.islink(full_path):
|
||||
continue
|
||||
try:
|
||||
stat = os.stat(full_path)
|
||||
if stat.st_uid != uid or stat.st_gid not in gids:
|
||||
@@ -93,6 +95,8 @@ class FilesystemOwnershipMixin:
|
||||
for root, dirs, files in os.walk(path):
|
||||
for name in dirs + files:
|
||||
full = os.path.join(root, name)
|
||||
if os.path.islink(full):
|
||||
continue
|
||||
try:
|
||||
if os.stat(full).st_mode & 0o777 != 0o755:
|
||||
return False
|
||||
|
||||
@@ -579,13 +579,18 @@ class ModlistConfigurationMixin:
|
||||
self._re_enforce_windows_10_mode()
|
||||
|
||||
# Step 15: Apply tool compatibility settings (xEdit, Pandora, DLL overrides).
|
||||
# Only runs for standard Skyrim SE/AE modlists. Non-Skyrim games (Enderal, FNV,
|
||||
# FO3, etc.) are excluded because the mscoree AppDefault targets SkyrimSE.exe,
|
||||
# which is also Enderal's executable, causing a crash on those modlists.
|
||||
# Runs for standard Skyrim SE/AE modlists and Enderal SE. Other special types
|
||||
# (FNV, FO3, etc.) are excluded entirely - different engine, none of these
|
||||
# fixes apply. Enderal SE shares the Skyrim SE engine and plugin format, so
|
||||
# xEdit/Pandora/DLL overrides/Synthesis dotnet+NuGet setup all still apply -
|
||||
# only the mscoree AppDefault is skipped for it (apply_engine_mscoree below),
|
||||
# since that entry is scoped to SkyrimSE.exe, which is also Enderal's own
|
||||
# game process and would crash it.
|
||||
_special_type = self.detect_special_game_type(self.modlist_dir)
|
||||
_enderal = _special_type == 'enderal'
|
||||
try:
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
if ConfigHandler().get('auto_tool_compat', True) and _special_type is None:
|
||||
if ConfigHandler().get('auto_tool_compat', True) and (_special_type is None or _enderal):
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Applying tool compatibility settings")
|
||||
self.logger.info("Step 15: Applying tool compatibility settings...")
|
||||
@@ -596,10 +601,10 @@ class ModlistConfigurationMixin:
|
||||
# NSF/CSF modlists need the global *mscoree=native that winetricks dotnet48
|
||||
# set: the per-exe scoping this function applies starves NSF's CLR hosting
|
||||
# (verified directly). So preserve the global override for them.
|
||||
# dotnet9 SDK install also flips the prefix to win11; NSF/CSF was only
|
||||
# dotnet SDK install also flips the prefix to win11; NSF/CSF was only
|
||||
# verified working on win10 + global-native, so we keep that state and skip
|
||||
# the dotnet9/win11 step. Whether Synthesis runs under global-native on an NSF
|
||||
# prefix has not been specifically tested - revisit if a modlist needs live
|
||||
# the dotnet SDK/win11 step. Whether Synthesis runs under global-native on an
|
||||
# NSF prefix has not been specifically tested - revisit if a modlist needs live
|
||||
# Synthesis. The NuGet cert Synthesis needs is applied regardless (see
|
||||
# apply_tool_config), since it doesn't touch mscoree or Windows version.
|
||||
_nsf = getattr(self, '_nsf_detected', False)
|
||||
@@ -607,13 +612,21 @@ class ModlistConfigurationMixin:
|
||||
compatdata_path,
|
||||
wine_bin,
|
||||
log=lambda msg: status_callback(f"{self._get_progress_timestamp()} {msg}") if status_callback else None,
|
||||
install_dotnet9_sdk=not _nsf,
|
||||
install_dotnet_sdk=not _nsf,
|
||||
install_fxc2_d3dcompiler=True,
|
||||
preserve_global_mscoree=_nsf,
|
||||
apply_engine_mscoree=not _enderal,
|
||||
)
|
||||
self.logger.info("Step 15: Tool compatibility settings applied")
|
||||
else:
|
||||
self.logger.warning("Step 15: Could not resolve prefix path or wine binary - skipping tool compat")
|
||||
|
||||
if self.modlist_dir:
|
||||
from jackify.backend.services.synthesis_updater import update_synthesis
|
||||
update_synthesis(
|
||||
str(self.modlist_dir),
|
||||
log=lambda msg: status_callback(f"{self._get_progress_timestamp()} {msg}") if status_callback else None,
|
||||
)
|
||||
elif _special_type is not None:
|
||||
self.logger.info(f"Step 15: Skipping tool compat for {_special_type} modlist")
|
||||
except Exception as e:
|
||||
|
||||
@@ -264,39 +264,28 @@ class ModlistInstallCLITTWMixin:
|
||||
lower = clean.lower()
|
||||
rendered = ""
|
||||
|
||||
# Match GUI behavior: explicit Loading manifest counter line
|
||||
manifest_match = re.search(r'loading manifest:\s*(\d+)/(\d+)', lower)
|
||||
if manifest_match:
|
||||
current = int(manifest_match.group(1))
|
||||
total = int(manifest_match.group(2))
|
||||
phase_state["current"] = "Loading manifest"
|
||||
percent = int((current / total) * 100) if total > 0 else 0
|
||||
rendered = f"[TTW] {phase_state['current']}: {current:,}/{total:,} ({percent}%)"
|
||||
# Match GUI behavior: explicit Progress: NN% counter line
|
||||
progress_match = re.search(r'progress:\s*(\d+)%', lower)
|
||||
if progress_match:
|
||||
percent = int(progress_match.group(1))
|
||||
rendered = f"[TTW] {phase_state['current']}: {percent}%"
|
||||
else:
|
||||
# Match GUI behavior: generic [X/Y] counters with current phase name.
|
||||
progress_match = re.search(r'\[(\d+)/(\d+)\]', clean)
|
||||
if progress_match:
|
||||
current = int(progress_match.group(1))
|
||||
total = int(progress_match.group(2))
|
||||
percent = int((current / total) * 100) if total > 0 else 0
|
||||
rendered = f"[TTW] {phase_state['current']}: {current:,}/{total:,} ({percent}%)"
|
||||
else:
|
||||
# Update phase state from milestone-like lines, then echo milestones.
|
||||
if 'manifest' in lower:
|
||||
phase_state["current"] = "Loading manifest"
|
||||
elif any(token in lower for token in ('extract', 'decompress', 'installing', 'copying', 'merge')):
|
||||
phase_state["current"] = clean
|
||||
# Update phase state from milestone-like lines, then echo milestones.
|
||||
if 'manifest' in lower:
|
||||
phase_state["current"] = "Loading manifest"
|
||||
elif any(token in lower for token in ('extract', 'decompress', 'installing', 'copying', 'merge')):
|
||||
phase_state["current"] = clean
|
||||
|
||||
is_milestone = any(token in lower for token in ('===', 'complete', 'finished', 'starting', 'valid'))
|
||||
is_error = 'error:' in lower
|
||||
is_warning = 'warning:' in lower
|
||||
if is_milestone or is_error or is_warning:
|
||||
rendered = f"[TTW] {clean}"
|
||||
is_milestone = any(token in lower for token in ('===', 'complete', 'finished', 'starting', 'valid'))
|
||||
is_error = 'error:' in lower
|
||||
is_warning = 'warning:' in lower
|
||||
if is_milestone or is_error or is_warning:
|
||||
rendered = f"[TTW] {clean}"
|
||||
|
||||
if not rendered or rendered == phase_state["last_rendered"]:
|
||||
return
|
||||
phase_state["last_rendered"] = rendered
|
||||
if rendered.startswith("[TTW] Loading manifest:") or re.search(r'^\[TTW\] .+?: [\d,]+/[\d,]+ \(\d+%\)$', rendered):
|
||||
if re.search(r'^\[TTW\] .+?: \d+%$', rendered):
|
||||
# In-place progress updates for counters/phases.
|
||||
print(f"\r{COLOR_INFO}{rendered}{COLOR_RESET}", end="", flush=True)
|
||||
progress_line_active["value"] = True
|
||||
|
||||
@@ -273,18 +273,18 @@ class ModlistWineOpsMixin:
|
||||
game = (game_var_full or modlist_name or "").lower().replace(" ", "")
|
||||
# Add game-specific extras
|
||||
if "fallout4vr" in game or "fo4vr" in game:
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnetdesktop6", "vcrun2012"]
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnet9", "dotnetdesktop6", "dotnetdesktop9", "vcrun2012"]
|
||||
elif "skyrim" in game or "fallout4" in game or "starfield" in game or "oblivion_remastered" in game or "enderal" in game:
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnetdesktop6"]
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnet9", "dotnetdesktop6", "dotnetdesktop9"]
|
||||
elif "falloutnewvegas" in game or "fnv" in game or "fallout3" in game or "fo3" in game or "oblivion" in game:
|
||||
extras += ["d3dx9_43", "d3dx9"]
|
||||
elif "cp2077" in game or "cyberpunk" in game:
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnetdesktop6"]
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnet9", "dotnetdesktop6", "dotnetdesktop9"]
|
||||
elif "bg3" in game or "baldursgate" in game:
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnetdesktop6"]
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnet9", "dotnetdesktop6", "dotnetdesktop9"]
|
||||
else:
|
||||
# Unknown game type - install the union of all known component sets
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnetdesktop6", "d3dx9_43", "d3dx9"]
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7", "dotnet8", "dotnet9", "dotnetdesktop6", "dotnetdesktop9", "d3dx9_43", "d3dx9"]
|
||||
# Add modlist-specific extras
|
||||
modlist_lower = modlist_name.lower().replace(" ", "") if modlist_name else ""
|
||||
for key, components in self.MODLIST_WINE_COMPONENTS.items():
|
||||
|
||||
@@ -93,6 +93,24 @@ class NativeComponentInstaller:
|
||||
if cb:
|
||||
cb(msg)
|
||||
|
||||
def _kill_wineserver_for_prefix(self) -> None:
|
||||
"""Kill wineserver for this prefix so the vcrun2022/vcrun2012 installers start against a
|
||||
fresh wineserver instead of whatever state a prior step left behind (matches the same
|
||||
precaution winetricks_handler takes before its own wine invocations)."""
|
||||
wineserver = self.wine_env.get('WINESERVER')
|
||||
if not wineserver or not os.path.exists(wineserver):
|
||||
return
|
||||
try:
|
||||
subprocess.run(
|
||||
[wineserver, '-k'],
|
||||
env=self._wine_env_base(),
|
||||
timeout=10,
|
||||
capture_output=True,
|
||||
)
|
||||
self.logger.debug("Killed wineserver for prefix before native Wine-based installers")
|
||||
except Exception as exc:
|
||||
self.logger.debug("Wineserver -k failed (non-fatal): %s", exc)
|
||||
|
||||
def _wine_env_base(self, **extra) -> dict:
|
||||
base = {**self.wine_env, 'WINEPREFIX': self.wineprefix}
|
||||
parts = []
|
||||
@@ -117,6 +135,9 @@ class NativeComponentInstaller:
|
||||
|
||||
native_candidates = [c for c in components if c in SUPPORTED_COMPONENTS]
|
||||
|
||||
if any(c in ("vcrun2022", "vcrun2012") for c in native_candidates):
|
||||
self._kill_wineserver_for_prefix()
|
||||
|
||||
for component in components:
|
||||
if component not in SUPPORTED_COMPONENTS:
|
||||
remaining.append(component)
|
||||
@@ -408,7 +429,7 @@ class NativeComponentInstaller:
|
||||
# 3010 = reboot required - normal for VC redist, treat as success
|
||||
if r.returncode not in (0, 3010):
|
||||
self.logger.error("vcrun2022: x86 installer failed (rc=%d)", r.returncode)
|
||||
self.logger.debug("vcrun2022 x86 stderr: %s", r.stderr.decode(errors='replace'))
|
||||
self.logger.error("vcrun2022 x86 stderr: %s", r.stderr.decode(errors='replace'))
|
||||
return False
|
||||
|
||||
# x64: same msvcp140.dll pre-extraction workaround
|
||||
@@ -436,7 +457,7 @@ class NativeComponentInstaller:
|
||||
)
|
||||
if r.returncode not in (0, 3010):
|
||||
self.logger.error("vcrun2022: x64 installer failed (rc=%d)", r.returncode)
|
||||
self.logger.debug("vcrun2022 x64 stderr: %s", r.stderr.decode(errors='replace'))
|
||||
self.logger.error("vcrun2022 x64 stderr: %s", r.stderr.decode(errors='replace'))
|
||||
return False
|
||||
|
||||
critical = [
|
||||
@@ -477,7 +498,7 @@ class NativeComponentInstaller:
|
||||
)
|
||||
if r.returncode not in (0, 3010):
|
||||
self.logger.error("vcrun2012: x86 installer failed (rc=%d)", r.returncode)
|
||||
self.logger.debug("vcrun2012 x86 stderr: %s", r.stderr.decode(errors='replace'))
|
||||
self.logger.error("vcrun2012 x86 stderr: %s", r.stderr.decode(errors='replace'))
|
||||
return False
|
||||
|
||||
self.logger.info("vcrun2012: running x64 installer")
|
||||
@@ -489,7 +510,7 @@ class NativeComponentInstaller:
|
||||
)
|
||||
if r.returncode not in (0, 3010):
|
||||
self.logger.error("vcrun2012: x64 installer failed (rc=%d)", r.returncode)
|
||||
self.logger.debug("vcrun2012 x64 stderr: %s", r.stderr.decode(errors='replace'))
|
||||
self.logger.error("vcrun2012 x64 stderr: %s", r.stderr.decode(errors='replace'))
|
||||
return False
|
||||
|
||||
if not (syswow64 / 'msvcr110.dll').is_file():
|
||||
|
||||
@@ -52,12 +52,12 @@ class ProgressParserPhaseMixin:
|
||||
def _map_section_to_phase(self, section_name: str) -> InstallationPhase:
|
||||
"""Map section name to InstallationPhase enum."""
|
||||
section_lower = section_name.lower()
|
||||
if 'download' in section_lower:
|
||||
if 'hash' in section_lower or 'validate' in section_lower or 'verif' in section_lower:
|
||||
return InstallationPhase.VALIDATE
|
||||
elif 'download' in section_lower:
|
||||
return InstallationPhase.DOWNLOAD
|
||||
elif 'extract' in section_lower:
|
||||
return InstallationPhase.EXTRACT
|
||||
elif 'hash' in section_lower or 'validate' in section_lower or 'verif' in section_lower:
|
||||
return InstallationPhase.VALIDATE
|
||||
elif 'install' in section_lower:
|
||||
return InstallationPhase.INSTALL
|
||||
elif 'bsa' in section_lower or 'building' in section_lower:
|
||||
|
||||
@@ -51,11 +51,11 @@ class TTWInstallerBackendMixin:
|
||||
return False, "Could not detect Fallout 3 or Fallout New Vegas installation paths"
|
||||
cmd = [
|
||||
str(self.ttw_installer_executable_path),
|
||||
"install",
|
||||
"--fo3", str(fallout3_path),
|
||||
"--fnv", str(falloutnv_path),
|
||||
"--mpi", str(ttw_mpi_path),
|
||||
"--output", str(ttw_output_path),
|
||||
"--start"
|
||||
"--dest", str(ttw_output_path),
|
||||
]
|
||||
self.logger.info("Executing TTW_Linux_Installer: %s", ' '.join(cmd))
|
||||
try:
|
||||
@@ -133,11 +133,11 @@ class TTWInstallerBackendMixin:
|
||||
return None, "Could not detect Fallout 3 or Fallout New Vegas installation paths"
|
||||
cmd = [
|
||||
str(self.ttw_installer_executable_path),
|
||||
"install",
|
||||
"--fo3", str(fallout3_path),
|
||||
"--fnv", str(falloutnv_path),
|
||||
"--mpi", str(ttw_mpi_path),
|
||||
"--output", str(ttw_output_path),
|
||||
"--start"
|
||||
"--dest", str(ttw_output_path),
|
||||
]
|
||||
self.logger.info("Executing TTW_Linux_Installer: %s", ' '.join(cmd))
|
||||
try:
|
||||
@@ -220,11 +220,11 @@ class TTWInstallerBackendMixin:
|
||||
return False, "Could not detect Fallout 3 or Fallout New Vegas installation paths"
|
||||
cmd = [
|
||||
str(self.ttw_installer_executable_path),
|
||||
"install",
|
||||
"--fo3", str(fallout3_path),
|
||||
"--fnv", str(falloutnv_path),
|
||||
"--mpi", str(ttw_mpi_path),
|
||||
"--output", str(ttw_output_path),
|
||||
"--start"
|
||||
"--dest", str(ttw_output_path),
|
||||
]
|
||||
self.logger.info("Executing TTW_Linux_Installer: %s", ' '.join(cmd))
|
||||
try:
|
||||
|
||||
@@ -26,14 +26,13 @@ logger = logging.getLogger(__name__)
|
||||
from jackify.shared.paths import get_jackify_data_dir
|
||||
JACKIFY_BASE_DIR = get_jackify_data_dir()
|
||||
DEFAULT_TTW_INSTALLER_DIR = JACKIFY_BASE_DIR / "TTW_Linux_Installer"
|
||||
TTW_INSTALLER_EXECUTABLE_NAME = "ttw_linux_gui" # Same executable, runs in CLI mode with args
|
||||
TTW_INSTALLER_EXECUTABLE_NAME = "mpi_installer"
|
||||
|
||||
# GitHub release info
|
||||
TTW_INSTALLER_REPO = "SulfurNitride/TTW_Linux_Installer"
|
||||
TTW_INSTALLER_RELEASE_URL = f"https://api.github.com/repos/{TTW_INSTALLER_REPO}/releases/latest"
|
||||
# Pin to 0.0.7 - last version with old format (ttw_linux_gui, universal-mpi-installer)
|
||||
# Set to None to use latest release
|
||||
TTW_INSTALLER_PINNED_VERSION = "0.0.7"
|
||||
TTW_INSTALLER_PINNED_VERSION = "0.2.0"
|
||||
|
||||
|
||||
class TTWInstallerHandler(TTWInstallerBackendMixin):
|
||||
@@ -73,25 +72,18 @@ class TTWInstallerHandler(TTWInstallerBackendMixin):
|
||||
self.ttw_installer_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _check_installation(self):
|
||||
"""Check if TTW_Linux_Installer is installed at expected location.
|
||||
"""Check if TTW_Linux_Installer is installed at expected location."""
|
||||
potential_exe_path = self.ttw_installer_dir / TTW_INSTALLER_EXECUTABLE_NAME
|
||||
if potential_exe_path.is_file() and os.access(potential_exe_path, os.X_OK):
|
||||
self.ttw_installer_executable_path = potential_exe_path
|
||||
self.ttw_installer_installed = True
|
||||
self.logger.info(f"Found TTW_Linux_Installer at: {self.ttw_installer_executable_path}")
|
||||
return
|
||||
|
||||
Checks for both old format (ttw_linux_gui) and new format (mpi_installer) executables.
|
||||
"""
|
||||
|
||||
# Check for both old (ttw_linux_gui) and new (mpi_installer) executable names
|
||||
exe_names = [TTW_INSTALLER_EXECUTABLE_NAME, "mpi_installer"]
|
||||
for exe_name in exe_names:
|
||||
potential_exe_path = self.ttw_installer_dir / exe_name
|
||||
if potential_exe_path.is_file() and os.access(potential_exe_path, os.X_OK):
|
||||
self.ttw_installer_executable_path = potential_exe_path
|
||||
self.ttw_installer_installed = True
|
||||
self.logger.info(f"Found TTW_Linux_Installer at: {self.ttw_installer_executable_path}")
|
||||
return
|
||||
|
||||
# Not found
|
||||
self.ttw_installer_installed = False
|
||||
self.ttw_installer_executable_path = None
|
||||
self.logger.info(f"TTW_Linux_Installer not found (searched for: {', '.join(exe_names)})")
|
||||
self.logger.info(f"TTW_Linux_Installer not found (searched for: {TTW_INSTALLER_EXECUTABLE_NAME})")
|
||||
|
||||
def install_ttw_installer(self, install_dir: Optional[Path] = None) -> Tuple[bool, str]:
|
||||
"""Download and install TTW_Linux_Installer from GitHub releases.
|
||||
@@ -138,15 +130,14 @@ class TTWInstallerHandler(TTWInstallerBackendMixin):
|
||||
data = resp.json()
|
||||
release_tag = data.get("tag_name") or data.get("name")
|
||||
|
||||
# Find Linux asset - universal-mpi-installer pattern (can be .zip or .tar.gz)
|
||||
# Find Linux asset - mpi-installer-linux-* pattern (can be .zip or .tar.gz)
|
||||
linux_asset = None
|
||||
asset_names = [asset.get("name", "") for asset in data.get("assets", [])]
|
||||
self.logger.info(f"Available release assets: {asset_names}")
|
||||
|
||||
|
||||
for asset in data.get("assets", []):
|
||||
name = asset.get("name", "").lower()
|
||||
# Look for universal-mpi-installer pattern
|
||||
if "universal-mpi-installer" in name and name.endswith((".zip", ".tar.gz")):
|
||||
if "linux" in name and "mpi-installer" in name and name.endswith((".zip", ".tar.gz")):
|
||||
linux_asset = asset
|
||||
self.logger.info(f"Found Linux asset: {asset.get('name')}")
|
||||
break
|
||||
@@ -185,39 +176,19 @@ class TTWInstallerHandler(TTWInstallerBackendMixin):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Find executable - support both old (ttw_linux_gui) and new (mpi_installer) names
|
||||
# Try old name first (since we're pinning to 0.0.7)
|
||||
exe_names = [TTW_INSTALLER_EXECUTABLE_NAME, "mpi_installer"]
|
||||
exe_path = None
|
||||
|
||||
for exe_name in exe_names:
|
||||
potential_path = target_dir / exe_name
|
||||
if potential_path.is_file():
|
||||
exe_path = potential_path
|
||||
self.logger.info(f"Found executable: {exe_name}")
|
||||
break
|
||||
# Search recursively
|
||||
for p in target_dir.rglob(exe_name):
|
||||
potential_path = target_dir / TTW_INSTALLER_EXECUTABLE_NAME
|
||||
if potential_path.is_file():
|
||||
exe_path = potential_path
|
||||
else:
|
||||
for p in target_dir.rglob(TTW_INSTALLER_EXECUTABLE_NAME):
|
||||
if p.is_file():
|
||||
exe_path = p
|
||||
self.logger.info(f"Found executable: {exe_name} at {p}")
|
||||
break
|
||||
if exe_path:
|
||||
break
|
||||
|
||||
if not exe_path or not exe_path.is_file():
|
||||
return False, f"TTW_Linux_Installer executable not found after extraction (searched for: {', '.join(exe_names)})"
|
||||
|
||||
# Remove any other executable versions to avoid confusion
|
||||
for exe_name in exe_names:
|
||||
if exe_name != exe_path.name:
|
||||
other_exe = target_dir / exe_name
|
||||
if other_exe.is_file():
|
||||
self.logger.info(f"Removing other version executable: {other_exe}")
|
||||
try:
|
||||
other_exe.unlink()
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to remove {other_exe}: {e}")
|
||||
return False, f"TTW_Linux_Installer executable not found after extraction (searched for: {TTW_INSTALLER_EXECUTABLE_NAME})"
|
||||
self.logger.info(f"Found executable: {exe_path}")
|
||||
|
||||
# Set executable permissions
|
||||
try:
|
||||
@@ -259,16 +230,10 @@ class TTWInstallerHandler(TTWInstallerBackendMixin):
|
||||
# If we have a pinned version, compare against that instead of latest
|
||||
if TTW_INSTALLER_PINNED_VERSION:
|
||||
if not installed:
|
||||
# No version recorded - check if executable exists to infer version
|
||||
if self.ttw_installer_installed and self.ttw_installer_executable_path:
|
||||
exe_name = self.ttw_installer_executable_path.name
|
||||
# If pinned to 0.0.7 but found mpi_installer, it's wrong version
|
||||
if TTW_INSTALLER_PINNED_VERSION == "0.0.7" and exe_name == "mpi_installer":
|
||||
return (True, None, TTW_INSTALLER_PINNED_VERSION)
|
||||
# If pinned to 0.0.7 and found ttw_linux_gui, assume correct
|
||||
elif TTW_INSTALLER_PINNED_VERSION == "0.0.7" and exe_name == "ttw_linux_gui":
|
||||
return (False, None, TTW_INSTALLER_PINNED_VERSION)
|
||||
# Not installed - don't show as update available
|
||||
# Installed but no version on record (e.g. pre-upgrade install) - force a
|
||||
# re-download so the recorded version matches the pinned executable.
|
||||
if self.ttw_installer_installed:
|
||||
return (True, None, TTW_INSTALLER_PINNED_VERSION)
|
||||
return (False, None, TTW_INSTALLER_PINNED_VERSION)
|
||||
|
||||
# Compare against pinned version
|
||||
|
||||
@@ -58,9 +58,24 @@ class WinetricksHandler(
|
||||
native_wineprefix = env.get('WINEPREFIX', wineprefix)
|
||||
wine_binary = env.get('WINE', '')
|
||||
|
||||
# Check user preference for component installation method
|
||||
from ..handlers.config_handler import ConfigHandler
|
||||
config_handler = ConfigHandler()
|
||||
|
||||
# Get component installation method with migration
|
||||
method = config_handler.get('component_installation_method', 'native')
|
||||
|
||||
# Migrate bundled_protontricks to system_protontricks (no longer supported)
|
||||
if method == 'bundled_protontricks':
|
||||
self.logger.warning("Bundled protontricks no longer supported, migrating to system_protontricks")
|
||||
method = 'system_protontricks'
|
||||
config_handler.set('component_installation_method', 'system_protontricks')
|
||||
|
||||
# Native installer tier: direct-source downloads, no winetricks dependency.
|
||||
# Only runs in "native" mode - "winetricks" and "system_protontricks" are explicit
|
||||
# full-bypass modes, e.g. as an escape hatch if a native install regression ships.
|
||||
native = None
|
||||
if wine_binary:
|
||||
if method == 'native' and wine_binary:
|
||||
try:
|
||||
from .native_component_installer import NativeComponentInstaller
|
||||
native = NativeComponentInstaller(native_wineprefix, wine_binary, env, self.logger)
|
||||
@@ -110,19 +125,6 @@ class WinetricksHandler(
|
||||
components_to_install, wineprefix, game_var, status_callback, appid=appid
|
||||
)
|
||||
|
||||
# Check user preference for component installation method
|
||||
from ..handlers.config_handler import ConfigHandler
|
||||
config_handler = ConfigHandler()
|
||||
|
||||
# Get component installation method with migration
|
||||
method = config_handler.get('component_installation_method', 'winetricks')
|
||||
|
||||
# Migrate bundled_protontricks to system_protontricks (no longer supported)
|
||||
if method == 'bundled_protontricks':
|
||||
self.logger.warning("Bundled protontricks no longer supported, migrating to system_protontricks")
|
||||
method = 'system_protontricks'
|
||||
config_handler.set('component_installation_method', 'system_protontricks')
|
||||
|
||||
# Choose installation method based on user preference
|
||||
if method == 'system_protontricks':
|
||||
self.logger.info("=" * 80)
|
||||
|
||||
Reference in New Issue
Block a user