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