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

304 lines
14 KiB
Python

"""
TTW_Linux_Installer Handler
Handles downloading, installation, and execution of TTW_Linux_Installer for TTW installations.
Replaces hoolamike for TTW-specific functionality.
"""
import logging
import os
import subprocess
import tarfile
import zipfile
from pathlib import Path
from typing import Optional, Tuple
import requests
from .path_handler import PathHandler
from .filesystem_handler import FileSystemHandler
from .config_handler import ConfigHandler
from .logging_handler import LoggingHandler
from .ttw_installer_backend import TTWInstallerBackendMixin
logger = logging.getLogger(__name__)
# Define default TTW_Linux_Installer paths
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
# 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(TTWInstallerBackendMixin):
"""Handles TTW installation using TTW_Linux_Installer (replaces hoolamike for TTW)."""
def __init__(self, steamdeck: bool, verbose: bool, filesystem_handler: FileSystemHandler,
config_handler: ConfigHandler, menu_handler=None):
"""Initialize the handler."""
self.steamdeck = steamdeck
self.verbose = verbose
self.path_handler = PathHandler()
self.filesystem_handler = filesystem_handler
self.config_handler = config_handler
self.menu_handler = menu_handler
# Set up logging
logging_handler = LoggingHandler()
logging_handler.rotate_log_for_logger('ttw-install', 'TTW_Install_workflow.log')
self.logger = logging_handler.setup_logger('ttw-install', 'TTW_Install_workflow.log')
# Installation paths
self.ttw_installer_dir: Path = DEFAULT_TTW_INSTALLER_DIR
self.ttw_installer_executable_path: Optional[Path] = None
self.ttw_installer_installed: bool = False
# Load saved install path from config
saved_path_str = self.config_handler.get('ttw_installer_install_path')
if saved_path_str and Path(saved_path_str).is_dir():
self.ttw_installer_dir = Path(saved_path_str)
self.logger.info(f"Loaded TTW_Linux_Installer path from config: {self.ttw_installer_dir}")
# Check if already installed
self._check_installation()
def _ensure_dirs_exist(self):
"""Ensure base directories exist."""
self.ttw_installer_dir.mkdir(parents=True, exist_ok=True)
def _check_installation(self):
"""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()
# 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.
Args:
install_dir: Optional directory to install to (defaults to ~/Jackify/TTW_Linux_Installer)
Returns:
(success: bool, message: str)
"""
try:
self._ensure_dirs_exist()
target_dir = Path(install_dir) if install_dir else self.ttw_installer_dir
target_dir.mkdir(parents=True, exist_ok=True)
# Fetch release info - always use pinned version when set; never use latest
if TTW_INSTALLER_PINNED_VERSION:
tag_candidates = [
TTW_INSTALLER_PINNED_VERSION,
f"v{TTW_INSTALLER_PINNED_VERSION}" if not TTW_INSTALLER_PINNED_VERSION.startswith("v") else None,
]
tag_candidates = [t for t in tag_candidates if t]
data = None
release_tag = None
for tag in tag_candidates:
release_url = f"https://api.github.com/repos/{TTW_INSTALLER_REPO}/releases/tags/{tag}"
self.logger.info(f"Fetching pinned TTW_Linux_Installer version {tag} from {release_url}")
resp = requests.get(release_url, timeout=15, verify=True)
if resp.status_code == 200:
data = resp.json()
release_tag = data.get("tag_name") or data.get("name")
break
if resp.status_code != 404:
resp.raise_for_status()
if not data:
return False, (
f"Pinned release {TTW_INSTALLER_PINNED_VERSION} not found on GitHub "
f"(tried tags: {', '.join(tag_candidates)}). Check repo and tag names."
)
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")
# Find Linux asset - universal-mpi-installer 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")):
linux_asset = asset
self.logger.info(f"Found Linux asset: {asset.get('name')}")
break
if not linux_asset:
all_assets = [asset.get("name", "") for asset in data.get("assets", [])]
self.logger.error(f"No suitable Linux asset found. Available assets: {all_assets}")
release_desc = f"release {release_tag}" if release_tag else "release"
return False, f"No suitable Linux TTW_Linux_Installer asset found in {release_desc}. Available assets: {', '.join(all_assets)}"
download_url = linux_asset.get("browser_download_url")
asset_name = linux_asset.get("name")
if not download_url or not asset_name:
return False, f"Release {release_tag or 'unknown'} is missing required asset metadata"
# Download to target directory
temp_path = target_dir / asset_name
self.logger.info(f"Downloading {asset_name} from {download_url}")
if not self.filesystem_handler.download_file(download_url, temp_path, overwrite=True, quiet=True):
return False, "Failed to download TTW_Linux_Installer asset"
# Extract archive (zip or tar.gz)
try:
self.logger.info(f"Extracting {asset_name} to {target_dir}")
if asset_name.lower().endswith('.tar.gz'):
with tarfile.open(temp_path, "r:gz") as tf:
tf.extractall(path=target_dir)
elif asset_name.lower().endswith('.zip'):
with zipfile.ZipFile(temp_path, "r") as zf:
zf.extractall(path=target_dir)
else:
return False, f"Unsupported archive format: {asset_name}"
finally:
try:
temp_path.unlink(missing_ok=True) # cleanup
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):
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}")
# Set executable permissions
try:
os.chmod(exe_path, 0o755)
except Exception as e:
self.logger.warning(f"Failed to chmod +x on {exe_path}: {e}")
# Update state
self.ttw_installer_dir = target_dir
self.ttw_installer_executable_path = exe_path
self.ttw_installer_installed = True
self.config_handler.set('ttw_installer_install_path', str(target_dir))
if release_tag:
self.config_handler.set('ttw_installer_version', release_tag)
self.logger.info(f"TTW_Linux_Installer installed successfully at {exe_path}")
return True, f"TTW_Linux_Installer installed at {target_dir}"
except Exception as e:
self.logger.error(f"Error installing TTW_Linux_Installer: {e}", exc_info=True)
return False, f"Error installing TTW_Linux_Installer: {e}"
def get_installed_ttw_installer_version(self) -> Optional[str]:
"""Return the installed TTW_Linux_Installer version stored in Jackify config, if any."""
try:
v = self.config_handler.get('ttw_installer_version')
return str(v) if v else None
except Exception:
return None
def is_ttw_installer_update_available(self) -> Tuple[bool, Optional[str], Optional[str]]:
"""
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"
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)
return (False, None, None)
try:
resp = requests.get(TTW_INSTALLER_RELEASE_URL, timeout=10, verify=True)
resp.raise_for_status()
latest = resp.json().get('tag_name') or resp.json().get('name')
if not latest:
return (False, installed, None)
if not installed:
# No version recorded and executable doesn't exist; treat as not installed
return (False, None, str(latest))
return (installed != str(latest), installed, str(latest))
except Exception as e:
self.logger.warning(f"Error checking for TTW_Linux_Installer updates: {e}")
return (False, installed, None)