From 286d51e6a1601a34d23505d91344ac9670af25d3 Mon Sep 17 00:00:00 2001 From: Omni Date: Sat, 24 Jan 2026 22:02:29 +0000 Subject: [PATCH] Sync from development - prepare for v0.2.2.1 --- CHANGELOG.md | 9 ++ jackify/__init__.py | 2 +- jackify/backend/handlers/menu_handler.py | 148 ++++++++++-------- .../backend/handlers/ttw_installer_handler.py | 108 ++++++++++--- jackify/frontends/gui/screens/install_ttw.py | 21 ++- 5 files changed, 192 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dce4e2..34262fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Jackify Changelog +## v0.2.2.1 - TTW Installer Pinning and Configure New Modlist CLI Fix +**Release Date:** 2026-01-24 + +### Bug Fixes +- **Configure New Modlist CLI**: Fixed manual Proton setup prompts appearing in CLI. Now uses automated prefix workflow like the install command. +- **TTW_Linux_Installer Version Pinning**: Pinned to v0.0.7. Will re-introduce latest version following more testing. + +--- + ## v0.2.2 - VNV Automation and First-Launch Improvements **Release Date:** 2026-01-21 diff --git a/jackify/__init__.py b/jackify/__init__.py index f9951d5..2b0304e 100644 --- a/jackify/__init__.py +++ b/jackify/__init__.py @@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing Wabbajack modlists natively on Linux systems. """ -__version__ = "0.2.2" +__version__ = "0.2.2.1" diff --git a/jackify/backend/handlers/menu_handler.py b/jackify/backend/handlers/menu_handler.py index 03e60c1..ef8312b 100644 --- a/jackify/backend/handlers/menu_handler.py +++ b/jackify/backend/handlers/menu_handler.py @@ -392,78 +392,88 @@ class ModlistMenuHandler: except Exception as e: self.logger.warning(f"Failed to rename 'Steam Icons' to 'SteamIcons': {e}") self.logger.debug(f"[DEBUG] After normalization, SteamIcons exists: {os.path.isdir(steamicons_path)}") - # --- Create shortcut with working NativeSteamService --- + # --- Use automated prefix workflow (replaces old manual workflow) --- try: - from ..services.native_steam_service import NativeSteamService - steam_service = NativeSteamService() - - success, app_id = steam_service.create_shortcut_with_proton( - app_name=modlist_name, - exe_path=mo2_path, - start_dir=os.path.dirname(mo2_path), - launch_options="%command%", - tags=["Jackify"], - proton_version="proton_experimental" - ) - if not success or not app_id: - self.logger.error("Failed to create Steam shortcut.") - print(f"\n{COLOR_ERROR}Failed to create Steam shortcut. Check the logs for details.{COLOR_RESET}") - return True mo2_dir = os.path.dirname(mo2_path) - if os.environ.get('JACKIFY_GUI_MODE'): - print('[PROMPT:RESTART_STEAM]') - input() # Wait for GUI to send confirmation - print('[PROMPT:MANUAL_STEPS]') - input() # Wait for GUI to send confirmation - # Continue as before - else: - print("\n───────────────────────────────────────────────────────────────────") - print(f"{COLOR_INFO}Important:{COLOR_RESET} Steam needs to restart to detect the new shortcut.") - print("This process involves several manual steps after the restart.") - restart_choice = input("\nRestart Steam automatically now? (Y/n): ").strip().lower() - if restart_choice == 'n': - self.logger.info("User opted out of automatic Steam restart.") - print("\nPlease restart Steam manually to see your new shortcut:") - print("1. Exit Steam completely (Steam -> Exit or right-click tray icon -> Exit)") - print("2. Wait a few seconds") - print("3. Start Steam again") - print("\nAfter restarting, you MUST perform the manual Proton setup steps:") - self._display_manual_proton_steps(modlist_name) - print(f"\n{COLOR_ERROR}You will need to re-run this configuration option after completing these steps.{COLOR_RESET}") - print("───────────────────────────────────────────────────────────────────") - return True - self.logger.info("Attempting secure Steam restart...") - print() - status_line = "" - def update_status(msg): - nonlocal status_line - if status_line: - print("\r" + " " * len(status_line), end="\r") - status_line = f"\r{COLOR_INFO}{msg}{COLOR_RESET}" - print(status_line, end="", flush=True) - # Actually restart Steam and wait for completion - if self.shortcut_handler.secure_steam_restart(status_callback=update_status): - print() - self.logger.info("Secure Steam restart successful.") - self._display_manual_proton_steps(modlist_name) - print() - input(f"{COLOR_PROMPT}Once you have completed ALL the steps above, press Enter to continue...{COLOR_RESET}") - self.logger.info("User confirmed completion of manual steps.") - # Re-detect the shortcut and get the new, positive AppID - new_app_id = self.shortcut_handler.get_appid_for_shortcut(modlist_name, mo2_path) - self.logger.info(f"Pre-launch AppID: {app_id}, Post-launch AppID: {new_app_id}") - if not new_app_id or not new_app_id.isdigit() or int(new_app_id) < 0: - print(f"{COLOR_ERROR}Could not find a valid AppID for '{modlist_name}' after launch. Please ensure you launched the shortcut from Steam at least once, then try again.{COLOR_RESET}") + install_dir = mo2_dir + + # Use automated prefix service for modern workflow + print(f"\n{COLOR_INFO}Using automated Steam setup workflow...{COLOR_RESET}") + + from ..services.automated_prefix_service import AutomatedPrefixService + prefix_service = AutomatedPrefixService() + + # Define progress callback for CLI with jackify-engine style timestamps + import time + start_time = time.time() + + def progress_callback(message): + elapsed = time.time() - start_time + hours = int(elapsed // 3600) + minutes = int((elapsed % 3600) // 60) + seconds = int(elapsed % 60) + timestamp = f"[{hours:02d}:{minutes:02d}:{seconds:02d}]" + print(f"{COLOR_INFO}{timestamp} {message}{COLOR_RESET}") + + # Run the automated workflow + result = prefix_service.run_working_workflow( + modlist_name, install_dir, mo2_path, progress_callback, steamdeck=self.steamdeck + ) + + # Handle the result + if isinstance(result, tuple) and len(result) == 4: + if result[0] == "CONFLICT": + # Handle conflict - ask user what to do + conflicts = result[1] + print(f"\n{COLOR_WARNING}Found existing Steam shortcut(s) with the same name and path:{COLOR_RESET}") + for i, conflict in enumerate(conflicts, 1): + print(f" {i}. Name: {conflict['name']}") + print(f" Executable: {conflict['exe']}") + print(f" Start Directory: {conflict['startdir']}") + print(f"\n{COLOR_PROMPT}Options:{COLOR_RESET}") + print(" 1. Use existing shortcut (recommended)") + print(" 2. Create new shortcut anyway") + choice = input(f"{COLOR_PROMPT}Enter choice (1-2): {COLOR_RESET}").strip() + if choice == "1": + # Use existing shortcut + existing_appid = conflicts[0].get('appid') + if existing_appid: + context = { + "name": modlist_name, + "appid": str(existing_appid), + "path": mo2_dir, + "manual_steps_completed": True, + "resolution": None + } + return self.run_modlist_configuration_phase(context) + elif choice == "2": + # Create new shortcut - would need to handle this, but for now just fail + print(f"{COLOR_ERROR}Creating new shortcut with same name not supported in this flow.{COLOR_RESET}") return True - context = { - "name": modlist_name, - "appid": new_app_id, - "path": mo2_dir, - "manual_steps_completed": True, - "resolution": None - } - self.logger.debug(f"[DEBUG] New Modlist Context (post-launch): {context}") - return self.run_modlist_configuration_phase(context) + else: + print(f"{COLOR_ERROR}Invalid choice.{COLOR_RESET}") + return True + else: + # Success - get the results + success, prefix_path, appid_int, last_timestamp = result + if success and appid_int: + context = { + "name": modlist_name, + "appid": str(appid_int), + "path": mo2_dir, + "manual_steps_completed": True, + "resolution": None + } + self.logger.debug(f"[DEBUG] New Modlist Context (automated workflow): {context}") + return self.run_modlist_configuration_phase(context) + else: + print(f"{COLOR_ERROR}Automated workflow completed but no AppID was returned.{COLOR_RESET}") + return True + else: + # Unexpected result format + print(f"{COLOR_ERROR}Automated workflow returned unexpected format.{COLOR_RESET}") + self.logger.error(f"Unexpected result format from automated workflow: {result}") + return True except Exception as e: self.logger.error(f"Error creating Steam shortcut: {e}", exc_info=True) print(f"\n{COLOR_ERROR}Failed to create Steam shortcut: {e}{COLOR_RESET}") diff --git a/jackify/backend/handlers/ttw_installer_handler.py b/jackify/backend/handlers/ttw_installer_handler.py index 57b6fc7..ad9c717 100644 --- a/jackify/backend/handlers/ttw_installer_handler.py +++ b/jackify/backend/handlers/ttw_installer_handler.py @@ -31,6 +31,9 @@ TTW_INSTALLER_EXECUTABLE_NAME = "ttw_linux_gui" # Same executable, runs in CLI # 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" class TTWInstallerHandler: @@ -70,18 +73,26 @@ class TTWInstallerHandler: 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. + + Checks for both old format (ttw_linux_gui) and new format (mpi_installer) executables. + """ self._ensure_dirs_exist() - 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}") - else: - self.ttw_installer_installed = False - self.ttw_installer_executable_path = None - self.logger.info(f"TTW_Linux_Installer not found at {potential_exe_path}") + # 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)})") def install_ttw_installer(self, install_dir: Optional[Path] = None) -> Tuple[bool, str]: """Download and install TTW_Linux_Installer from GitHub releases. @@ -97,9 +108,15 @@ class TTWInstallerHandler: target_dir = Path(install_dir) if install_dir else self.ttw_installer_dir target_dir.mkdir(parents=True, exist_ok=True) - # Fetch latest release info - self.logger.info(f"Fetching latest TTW_Linux_Installer release from {TTW_INSTALLER_RELEASE_URL}") - resp = requests.get(TTW_INSTALLER_RELEASE_URL, timeout=15, verify=True) + # Fetch release info (pinned version or latest) + if TTW_INSTALLER_PINNED_VERSION: + release_url = f"https://api.github.com/repos/{TTW_INSTALLER_REPO}/releases/tags/{TTW_INSTALLER_PINNED_VERSION}" + self.logger.info(f"Fetching pinned TTW_Linux_Installer version {TTW_INSTALLER_PINNED_VERSION} from {release_url}") + else: + release_url = TTW_INSTALLER_RELEASE_URL + self.logger.info(f"Fetching latest TTW_Linux_Installer release from {release_url}") + + resp = requests.get(release_url, timeout=15, verify=True) resp.raise_for_status() data = resp.json() release_tag = data.get("tag_name") or data.get("name") @@ -151,17 +168,39 @@ class TTWInstallerHandler: except Exception: pass - # Find executable (may be in subdirectory or root) - exe_path = target_dir / TTW_INSTALLER_EXECUTABLE_NAME - if not exe_path.is_file(): - # Search for it - for p in target_dir.rglob(TTW_INSTALLER_EXECUTABLE_NAME): + # 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): 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.is_file(): - return False, "TTW_Linux_Installer executable not found after extraction" + 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}") # Set executable permissions try: @@ -194,13 +233,36 @@ class TTWInstallerHandler: def is_ttw_installer_update_available(self) -> Tuple[bool, Optional[str], Optional[str]]: """ - Check GitHub for the latest TTW_Linux_Installer release and compare with installed version. - Returns (update_available, installed_version, latest_version). + Check if TTW_Linux_Installer update is available. + If a version is pinned, compares against pinned version instead of latest. + Returns (update_available, installed_version, target_version). """ installed = self.get_installed_ttw_installer_version() + # 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 + return (False, None, TTW_INSTALLER_PINNED_VERSION) + + # Compare against pinned version + if installed != TTW_INSTALLER_PINNED_VERSION: + # Installed version doesn't match pinned - show as out of date (allows downgrade) + return (True, installed, TTW_INSTALLER_PINNED_VERSION) + else: + return (False, installed, TTW_INSTALLER_PINNED_VERSION) + + # No pinned version - check against latest release (original behavior) # If executable exists but no version is recorded, don't show as "out of date" - # This can happen if the executable was installed before version tracking was added if not installed and self.ttw_installer_installed: self.logger.info("TTW_Linux_Installer executable found but no version recorded in config") # Don't treat as update available - just show as "Ready" (unknown version) diff --git a/jackify/frontends/gui/screens/install_ttw.py b/jackify/frontends/gui/screens/install_ttw.py index b5b7b23..89dcfe0 100644 --- a/jackify/frontends/gui/screens/install_ttw.py +++ b/jackify/frontends/gui/screens/install_ttw.py @@ -489,10 +489,25 @@ class InstallTTWScreen(QWidget): ttw_installer_handler._check_installation() if ttw_installer_handler.ttw_installer_installed: - # Check version against latest - update_available, installed_v, latest_v = ttw_installer_handler.is_ttw_installer_update_available() + # Check version against pinned/latest + update_available, installed_v, target_v = ttw_installer_handler.is_ttw_installer_update_available() if update_available: - version_text = f"Out of date (v{installed_v} → v{latest_v})" if installed_v and latest_v else "Out of date" + # Determine if this is a downgrade or upgrade + from jackify.backend.handlers.ttw_installer_handler import TTW_INSTALLER_PINNED_VERSION + if TTW_INSTALLER_PINNED_VERSION and installed_v and target_v: + # If we have a pinned version and installed is newer, it's a downgrade + try: + # Simple version comparison - if installed version string is longer/more complex, likely newer + # For now, just check if they're different and show appropriate message + if installed_v != target_v: + version_text = f"Update to v{target_v} (currently v{installed_v})" + else: + version_text = f"Update available (v{installed_v} → v{target_v})" + except Exception: + version_text = f"Update to v{target_v}" if target_v else "Update available" + else: + # Normal update (newer version available) + version_text = f"Update available (v{installed_v} → v{target_v})" if installed_v and target_v else "Update available" self.ttw_installer_status.setText(version_text) self.ttw_installer_status.setStyleSheet("color: #f44336;") self.ttw_installer_btn.setText("Update now")