mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-07 22:57:45 +02:00
Sync from development - prepare for v0.2.2.1
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user