summaryrefslogtreecommitdiff
path: root/py_modules/lsfg_vk/installation.py
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-07-30 16:26:27 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-07-30 16:26:27 -0400
commit4390d600ffd35184c4c30bc64480f58e468627de (patch)
treea99fbdadacd15395125e5344a462abbe5327e17e /py_modules/lsfg_vk/installation.py
parent6c5e8a8ef81b7b3d2bf19b1783250e6b0cefedf7 (diff)
downloaddecky-lsfg-vk-4390d600ffd35184c4c30bc64480f58e468627de.tar.gz
decky-lsfg-vk-4390d600ffd35184c4c30bc64480f58e468627de.zip
feat: migrate Decky plugin to lsfg-vk v2
Diffstat (limited to 'py_modules/lsfg_vk/installation.py')
-rw-r--r--py_modules/lsfg_vk/installation.py597
1 files changed, 226 insertions, 371 deletions
diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py
index 49ed711..cb6b161 100644
--- a/py_modules/lsfg_vk/installation.py
+++ b/py_modules/lsfg_vk/installation.py
@@ -1,443 +1,298 @@
-"""
-Installation service for lsfg-vk.
-"""
+"""Installation and in-place migration service for lsfg-vk v2."""
-import os
+import json
import platform
import shutil
-import traceback
-import zipfile
+import tarfile
import tempfile
-import json
+import traceback
from pathlib import Path
-from typing import Dict, Any
+from typing import Any, Dict
from .base_service import BaseService
+from .config_schema import ConfigurationManager, ProfileData
from .constants import (
- LIB_FILENAME, JSON_FILENAME, ZIP_FILENAME, BIN_DIR,
- SO_EXT, JSON_EXT, ARM_LIB_FILENAME, ARMADA_DEVICE_ENV
+ ARMADA_DEVICE_ENV,
+ BIN_DIR,
+ JSON_FILENAME,
+ LEGACY_JSON_FILENAME,
+ LEGACY_LIB_FILENAME,
+ LIB_FILENAME,
+ get_layer_archive_filename,
)
-from .config_schema import ConfigurationManager
-from .types import InstallationResponse, UninstallationResponse, InstallationCheckResponse
+from .types import InstallationCheckResponse, InstallationResponse, UninstallationResponse
class InstallationService(BaseService):
- """Service for handling lsfg-vk installation and uninstallation"""
-
+ """Install architecture-matched v2 assets and migrate the public config."""
+
def __init__(self, logger=None):
super().__init__(logger)
-
self.lib_file = self.local_lib_dir / LIB_FILENAME
self.json_file = self.local_share_dir / JSON_FILENAME
-
+ self.legacy_lib_file = self.local_lib_dir / LEGACY_LIB_FILENAME
+ self.legacy_json_file = self.local_share_dir / LEGACY_JSON_FILENAME
+
def install(self) -> InstallationResponse:
- """Install lsfg-vk by extracting the zip file to ~/.local
-
- Returns:
- InstallationResponse with success status and message/error
- """
try:
plugin_dir = Path(__file__).parent.parent.parent
- zip_path = plugin_dir / BIN_DIR / ZIP_FILENAME
-
- if not zip_path.exists():
- error_msg = f"{ZIP_FILENAME} not found at {zip_path}"
+ archive_name = get_layer_archive_filename(self._is_arm_architecture())
+ archive_path = plugin_dir / BIN_DIR / archive_name
+ if not archive_path.exists():
+ error_msg = f"{archive_name} not found at {archive_path}"
self.log.error(error_msg)
return self._error_response(InstallationResponse, error_msg, message="")
-
+
self._ensure_directories()
-
- self._extract_and_install_files(zip_path)
-
- # If on ARM, overwrite the .so with the ARM version
- if self._is_arm_architecture():
- self.log.info("Detected ARM architecture, using ARM binary")
- arm_so_path = plugin_dir / BIN_DIR / ARM_LIB_FILENAME
- self._copy_plugin_file(arm_so_path, self.lib_file)
- self.log.info(f"Overwrote with ARM binary: {self.lib_file}")
-
- self._create_config_file()
-
- self._create_lsfg_launch_script()
-
- self.log.info("lsfg-vk installed successfully")
- return self._success_response(InstallationResponse, "lsfg-vk installed successfully")
-
- except (OSError, zipfile.BadZipFile, shutil.Error) as e:
- error_msg = f"Error installing lsfg-vk: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(InstallationResponse, str(e), message="")
- except Exception as e:
- error_msg = f"Unexpected error installing lsfg-vk: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(InstallationResponse, str(e), message="")
-
+ self._extract_and_install_files(archive_path)
+ profile_data = self._create_config_file()
+ self._create_lsfg_launch_script(profile_data)
+ self._remove_legacy_layer_files()
+
+ self.log.info("lsfg-vk v2 installed successfully from %s", archive_name)
+ return self._success_response(InstallationResponse, "lsfg-vk v2 installed successfully")
+ except (OSError, tarfile.TarError, shutil.Error) as error:
+ self.log.error("Error installing lsfg-vk: %s", error)
+ return self._error_response(InstallationResponse, str(error), message="")
+ except Exception as error:
+ self.log.error("Unexpected error installing lsfg-vk: %s", error)
+ return self._error_response(InstallationResponse, str(error), message="")
+
def _is_arm_architecture(self) -> bool:
- """Check if running on ARM architecture
-
- Returns:
- True if running on ARM (aarch64), False otherwise
- """
- if platform.machine().lower() in ('aarch64', 'arm64'):
+ """Detect the native game host, including Decky running under FEX on Armada."""
+ if platform.machine().lower() in ("aarch64", "arm64"):
return True
-
- # Decky runs through FEX on Armada, so Python reports x86_64 even
- # though the host is AArch64. Armada exposes this native helper only
- # on its ARM image, including inside Decky's FEX rootfs.
if ARMADA_DEVICE_ENV.is_file():
self.log.info("Detected native AArch64 Armada host through device-env")
return True
-
- # Fall back to the native PID 1 ELF header. e_machine 183 is AArch64.
try:
- with Path('/proc/1/exe').open('rb') as host_executable:
+ with Path("/proc/1/exe").open("rb") as host_executable:
elf_header = host_executable.read(20)
- if elf_header[:4] == b'\x7fELF' and elf_header[5] in (1, 2):
- byte_order = 'little' if elf_header[5] == 1 else 'big'
+ if elf_header[:4] == b"\x7fELF" and elf_header[5] in (1, 2):
+ byte_order = "little" if elf_header[5] == 1 else "big"
if int.from_bytes(elf_header[18:20], byte_order) == 183:
self.log.info("Detected native AArch64 host through PID 1")
return True
- except OSError as e:
- self.log.debug(f"Could not inspect native host architecture: {e}")
-
+ except OSError as error:
+ self.log.debug("Could not inspect native host architecture: %s", error)
return False
@staticmethod
- def _copy_plugin_file(src_file: Path, dst_file: Path) -> None:
- """Copy plugin content without preserving FEX-incompatible metadata."""
- shutil.copyfile(src_file, dst_file)
- dst_file.chmod(0o644)
-
- def _extract_and_install_files(self, zip_path: Path) -> None:
- """Extract zip file and install files to appropriate locations
-
- Args:
- zip_path: Path to the zip file to extract
-
- Raises:
- zipfile.BadZipFile: If zip file is corrupted
- OSError: If file operations fail
- """
- # Destination mapping for file types
- dest_map = {
- SO_EXT: self.local_lib_dir,
- JSON_EXT: self.local_share_dir
+ def _copy_plugin_file(source: Path, destination: Path, mode: int = 0o644) -> None:
+ """Copy payload content without preserving FEX-incompatible metadata."""
+ shutil.copyfile(source, destination)
+ destination.chmod(mode)
+
+ def _extract_and_install_files(self, archive_path: Path) -> None:
+ """Extract just the v2 library and manifest from an owner release archive."""
+ destinations = {
+ LIB_FILENAME: self.lib_file,
+ JSON_FILENAME: self.json_file,
}
-
- with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+ found = set()
+ with tarfile.open(archive_path, "r:xz") as archive:
with tempfile.TemporaryDirectory() as temp_dir:
- temp_path = Path(temp_dir)
- zip_ref.extractall(temp_path)
-
- # Process extracted files
- for root, dirs, files in os.walk(temp_path):
- root_path = Path(root)
- for file in files:
- src_file = root_path / file
- file_path = Path(file)
-
- # Check if we know where this file type should go
- dst_dir = dest_map.get(file_path.suffix)
- if dst_dir:
- dst_file = dst_dir / file
-
- # Special handling for JSON files - need to modify library_path
- if file_path.suffix == JSON_EXT and file == JSON_FILENAME:
- self._copy_and_fix_json_file(src_file, dst_file)
- else:
- self._copy_plugin_file(src_file, dst_file)
-
- self.log.info(f"Copied {file} to {dst_file}")
-
- def _copy_and_fix_json_file(self, src_file: Path, dst_file: Path) -> None:
- """Copy JSON file and fix the library_path to use relative path
-
- Args:
- src_file: Source JSON file path
- dst_file: Destination JSON file path
- """
+ temp_root = Path(temp_dir)
+ for member in archive.getmembers():
+ if not member.isfile():
+ continue
+ filename = Path(member.name).name
+ destination = destinations.get(filename)
+ if destination is None:
+ continue
+ source = archive.extractfile(member)
+ if source is None:
+ continue
+ temporary_file = temp_root / filename
+ with source, temporary_file.open("wb") as output:
+ shutil.copyfileobj(source, output)
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ if filename == JSON_FILENAME:
+ self._copy_and_fix_json_file(temporary_file, destination)
+ else:
+ self._copy_plugin_file(temporary_file, destination)
+ found.add(filename)
+ self.log.info("Installed %s to %s", filename, destination)
+
+ missing = [name for name in destinations if name not in found]
+ if missing:
+ raise OSError("Archive did not contain required lsfg-vk files: " + ", ".join(missing))
+
+ def _copy_and_fix_json_file(self, source: Path, destination: Path) -> None:
+ """Point the manifest at ~/.local/lib after Decky installs it."""
try:
- # Read the JSON file
- with open(src_file, 'r') as f:
- json_data = json.load(f)
-
- # Fix the library_path from "liblsfg-vk.so" to "../../../lib/liblsfg-vk.so"
- if 'layer' in json_data and 'library_path' in json_data['layer']:
- current_path = json_data['layer']['library_path']
- if current_path == "liblsfg-vk.so":
- json_data['layer']['library_path'] = "../../../lib/liblsfg-vk.so"
- self.log.info(f"Fixed library_path from '{current_path}' to '../../../lib/liblsfg-vk.so'")
-
- # Write the modified JSON file
- with open(dst_file, 'w') as f:
- json.dump(json_data, f, indent=2)
-
- except (json.JSONDecodeError, KeyError, OSError) as e:
- self.log.error(f"Error fixing JSON file {src_file}: {e}")
- # Fallback to simple copy if JSON modification fails
- self._copy_plugin_file(src_file, dst_file)
-
- def _create_config_file(self) -> None:
- """Create or update the TOML config file in ~/.config/lsfg-vk with default configuration and detected DLL path
-
- If a config file already exists, preserve existing profiles and only update global settings like DLL path.
- """
- # Import here to avoid circular imports
+ data = json.loads(source.read_text(encoding="utf-8"))
+ data["layer"]["library_path"] = "../../../lib/liblsfg-vk-layer.so"
+ destination.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
+ destination.chmod(0o644)
+ except (json.JSONDecodeError, KeyError, OSError) as error:
+ self.log.error("Error fixing JSON file %s: %s", source, error)
+ self._copy_plugin_file(source, destination)
+
+ def _create_config_file(self) -> ProfileData:
+ """Migrate v1 once, normalize v2, and retain Decky launch workarounds."""
from .dll_detection import DllDetectionService
-
- # Try to detect DLL path
+
dll_service = DllDetectionService(self.log)
-
- # Check if config file already exists
+ profile_data: ProfileData
+ was_legacy = False
if self.config_file_path.exists():
+ content = self.config_file_path.read_text(encoding="utf-8")
+ was_legacy = ConfigurationManager.is_legacy_v1(content)
try:
- # Read existing config to preserve user profiles
- content = self.config_file_path.read_text(encoding='utf-8')
- existing_profile_data = ConfigurationManager.parse_toml_content_multi_profile(content)
- self.log.info(f"Found existing config file, preserving user profiles")
-
- # Create merged profile data that preserves user settings but adds any new fields
- merged_profile_data = self._merge_config_with_defaults(existing_profile_data, dll_service)
-
- # Generate TOML content with merged profiles
- toml_content = ConfigurationManager.generate_toml_content_multi_profile(merged_profile_data)
-
- except Exception as e:
- self.log.warning(f"Failed to parse existing config file: {str(e)}, creating new one")
- # Fall back to creating a new config file
- config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
- toml_content = ConfigurationManager.generate_toml_content(config)
+ profile_data = ConfigurationManager.parse_toml_content_multi_profile(content)
+ except (ValueError, KeyError, TypeError) as error:
+ self.log.warning("Could not parse existing config, using defaults: %s", error)
+ default = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
+ profile_data = ProfileData(
+ current_profile="decky-lsfg-vk",
+ profiles={"decky-lsfg-vk": default},
+ global_config={"dll": default["dll"], "allow_fp16": default["allow_fp16"]},
+ )
+ if was_legacy:
+ self._backup_legacy_config(content)
else:
- # No existing config file, create a new one with defaults
- config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
- toml_content = ConfigurationManager.generate_toml_content(config)
- self.log.info(f"Creating new config file")
-
- # Write config file
- self._write_file(self.config_file_path, toml_content, 0o644)
- self.log.info(f"Created config file at {self.config_file_path}")
-
- # Log detected DLL path if found - USE GENERATED CONSTANTS
- from .config_schema_generated import DLL
+ default = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
+ profile_data = ProfileData(
+ current_profile="decky-lsfg-vk",
+ profiles={"decky-lsfg-vk": default},
+ global_config={"dll": default["dll"], "allow_fp16": default["allow_fp16"]},
+ )
+
+ profile_data = self._merge_config_with_defaults(profile_data, dll_service)
+ script_values = self._read_script_values()
+ current_profile = profile_data["current_profile"]
+ profile_data["profiles"][current_profile] = ConfigurationManager.merge_config_with_script(
+ profile_data["profiles"][current_profile], script_values
+ )
+ self._write_file(
+ self.config_file_path,
+ ConfigurationManager.generate_toml_content_multi_profile(profile_data),
+ 0o644,
+ )
+ return profile_data
+
+ def _backup_legacy_config(self, content: str) -> None:
+ backup_path = self.config_file_path.with_name(f"{self.config_file_path.name}.v1.bak")
+ if backup_path.exists():
+ return
+ self._write_file(backup_path, content, 0o644)
+ self.log.info("Backed up v1 configuration to %s", backup_path)
+
+ def _read_script_values(self) -> Dict[str, Any]:
+ if not self.lsfg_launch_script_path.exists():
+ return {}
try:
- # Try to parse the written content to get the DLL path
- final_content = self.config_file_path.read_text(encoding='utf-8')
- final_config = ConfigurationManager.parse_toml_content(final_content)
- if final_config.get(DLL):
- self.log.info(f"Configured DLL path: {final_config[DLL]}")
- except (OSError, IOError, ValueError, KeyError) as e:
- # Don't fail installation if we can't log the DLL path
- self.log.debug(f"Could not log DLL path: {e}")
-
- def _create_lsfg_launch_script(self) -> None:
- """Create the ~/lsfg launch script for easier game setup"""
- # Use the default configuration for the initial script
- from .config_schema import ConfigurationManager
- default_config = ConfigurationManager.get_defaults()
-
- # Create configuration service to generate the script
+ return ConfigurationManager.parse_script_content(
+ self.lsfg_launch_script_path.read_text(encoding="utf-8")
+ )
+ except OSError as error:
+ self.log.warning("Could not preserve launch-script settings: %s", error)
+ return {}
+
+ def _create_lsfg_launch_script(self, profile_data: ProfileData = None) -> None:
+ """Write the v2 launcher after config migration has completed."""
from .configuration import ConfigurationService
- config_service = ConfigurationService(logger=self.log)
- config_service.user_home = self.user_home
- config_service.lsfg_script_path = self.lsfg_launch_script_path
-
- # Generate script content with default configuration
- script_content = config_service._generate_script_content(default_config)
-
- # Write the script file
- self._write_file(self.lsfg_launch_script_path, script_content, 0o755)
- self.log.info(f"Created lsfg launch script at {self.lsfg_launch_script_path}")
-
+
+ if profile_data is None:
+ profile_data = ConfigurationManager.parse_toml_content_multi_profile(
+ self.config_file_path.read_text(encoding="utf-8")
+ )
+ configuration_service = ConfigurationService(logger=self.log)
+ configuration_service.user_home = self.user_home
+ configuration_service.config_dir = self.config_dir
+ configuration_service.config_file_path = self.config_file_path
+ configuration_service.lsfg_script_path = self.lsfg_launch_script_path
+ script = configuration_service._generate_script_content_for_profile(profile_data)
+ self._write_file(self.lsfg_launch_script_path, script, 0o755)
+
+ def _merge_config_with_defaults(self, existing: ProfileData, dll_service) -> ProfileData:
+ defaults = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
+ global_config = dict(existing.get("global_config", {}))
+ global_config.setdefault("dll", defaults["dll"])
+ global_config.setdefault("allow_fp16", defaults["allow_fp16"])
+
+ profiles: Dict[str, Any] = {}
+ for name, existing_config in existing.get("profiles", {}).items():
+ profiles[name] = ConfigurationManager.validate_config({
+ **defaults,
+ **existing_config,
+ **global_config,
+ })
+ if not profiles:
+ profiles["decky-lsfg-vk"] = ConfigurationManager.validate_config(defaults)
+
+ current_profile = existing.get("current_profile", "decky-lsfg-vk")
+ if current_profile not in profiles:
+ current_profile = "decky-lsfg-vk" if "decky-lsfg-vk" in profiles else next(iter(profiles))
+ return ProfileData(
+ current_profile=current_profile,
+ profiles=profiles,
+ global_config=global_config,
+ )
+
+ def _remove_legacy_layer_files(self) -> None:
+ for path in (self.legacy_lib_file, self.legacy_json_file):
+ if self._remove_if_exists(path):
+ self.log.info("Removed superseded v1 layer file %s", path)
+
def get_launch_script_path(self) -> str:
- """Get the path to the lsfg launch script
-
- Returns:
- String path to the launch script file
- """
return str(self.lsfg_launch_script_path)
def check_installation(self) -> InstallationCheckResponse:
- """Check if lsfg-vk is already installed
-
- Returns:
- InstallationCheckResponse with installation status and file paths
- """
try:
lib_exists = self.lib_file.exists()
json_exists = self.json_file.exists()
- config_exists = self.config_file_path.exists()
-
- self.log.info(f"Installation check: lib={lib_exists}, json={json_exists}, config={config_exists}")
-
+ legacy_installed = self.legacy_lib_file.exists() or self.legacy_json_file.exists()
return {
"installed": lib_exists and json_exists,
+ "legacy_installed": legacy_installed,
"lib_exists": lib_exists,
"json_exists": json_exists,
- "script_exists": config_exists, # Keep script_exists for backward compatibility
+ "script_exists": self.config_file_path.exists(),
"lib_path": str(self.lib_file),
"json_path": str(self.json_file),
- "script_path": str(self.config_file_path), # Keep script_path for backward compatibility
- "error": None
+ "script_path": str(self.config_file_path),
+ "error": None,
}
-
- except Exception as e:
- error_msg = f"Error checking lsfg-vk installation: {str(e)}"
- self.log.error(error_msg)
+ except OSError as error:
return {
"installed": False,
+ "legacy_installed": False,
"lib_exists": False,
"json_exists": False,
"script_exists": False,
"lib_path": str(self.lib_file),
"json_path": str(self.json_file),
"script_path": str(self.config_file_path),
- "error": str(e)
+ "error": str(error),
}
-
+
def uninstall(self) -> UninstallationResponse:
- """Uninstall lsfg-vk by removing the installed files
-
- Note: The config file (conf.toml) is preserved to maintain user's custom profiles
-
- Returns:
- UninstallationResponse with success status and removed files list
- """
try:
- removed_files = []
- # Remove core lsfg-vk files, but preserve config file to maintain user's custom profiles
- files_to_remove = [self.lib_file, self.json_file, self.lsfg_launch_script_path]
-
- for file_path in files_to_remove:
- if self._remove_if_exists(file_path):
- removed_files.append(str(file_path))
-
- # Also try to remove the old script file if it exists (for backward compatibility)
- if self._remove_if_exists(self.lsfg_script_path):
- removed_files.append(str(self.lsfg_script_path))
-
- # Don't remove config directory since we're preserving the config file
-
- if not removed_files:
- return self._success_response(UninstallationResponse,
- "No lsfg-vk files found to remove",
- removed_files=None)
-
- self.log.info("lsfg-vk uninstalled successfully")
- return self._success_response(UninstallationResponse,
- f"lsfg-vk uninstalled successfully. Removed {len(removed_files)} files.",
- removed_files=removed_files)
-
- except OSError as e:
- error_msg = f"Error uninstalling lsfg-vk: {str(e)}"
- self.log.error(error_msg)
- return self._error_response(UninstallationResponse, str(e),
- message="", removed_files=None)
-
+ removed = []
+ for path in (
+ self.lib_file,
+ self.json_file,
+ self.legacy_lib_file,
+ self.legacy_json_file,
+ self.lsfg_launch_script_path,
+ ):
+ if self._remove_if_exists(path):
+ removed.append(str(path))
+ if not removed:
+ return self._success_response(UninstallationResponse, "No lsfg-vk files found to remove", removed_files=None)
+ return self._success_response(
+ UninstallationResponse,
+ f"lsfg-vk uninstalled successfully. Removed {len(removed)} files.",
+ removed_files=removed,
+ )
+ except OSError as error:
+ return self._error_response(UninstallationResponse, str(error), message="", removed_files=None)
+
def cleanup_on_uninstall(self) -> None:
- """Clean up lsfg-vk files when the plugin is uninstalled
-
- Note: The config file (conf.toml) is preserved to maintain user's custom profiles
- """
try:
- self.log.info("Checking for lsfg-vk files to clean up:")
- self.log.info(f" Library file: {self.lib_file}")
- self.log.info(f" JSON file: {self.json_file}")
- self.log.info(f" Config file: {self.config_file_path} (preserved)")
- self.log.info(f" Launch script: {self.lsfg_launch_script_path}")
- self.log.info(f" Old script file: {self.lsfg_script_path}")
-
- removed_files = []
- # Remove core lsfg-vk files, but preserve config file to maintain user's custom profiles
- files_to_remove = [self.lib_file, self.json_file, self.lsfg_launch_script_path, self.lsfg_script_path]
-
- for file_path in files_to_remove:
- try:
- if self._remove_if_exists(file_path):
- removed_files.append(str(file_path))
- except OSError as e:
- self.log.error(f"Failed to remove {file_path}: {e}")
-
- # Don't remove config directory since we're preserving the config file
-
- if removed_files:
- self.log.info(f"Cleaned up {len(removed_files)} lsfg-vk files during plugin uninstall: {removed_files}")
- else:
- self.log.info("No lsfg-vk files found to clean up during plugin uninstall")
-
- except Exception as e:
- self.log.error(f"Error cleaning up lsfg-vk files during uninstall: {str(e)}")
- self.log.error(f"Traceback: {traceback.format_exc()}")
-
- def _merge_config_with_defaults(self, existing_profile_data, dll_service):
- """Merge existing user config with current schema defaults
-
- This ensures that:
- 1. User's custom profiles and values are preserved
- 2. Any new fields added to the schema get their default values
- 3. Global settings like DLL path are updated as needed
-
- Args:
- existing_profile_data: The user's existing ProfileData
- dll_service: DLL detection service for updating DLL path
-
- Returns:
- ProfileData with merged configuration
- """
- from .config_schema import ProfileData
-
- # Get current schema defaults
- default_config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
- default_global_config = {
- "dll": default_config.get("dll", ""),
- "no_fp16": False
- }
-
- # Start with existing data
- merged_data: ProfileData = {
- "current_profile": existing_profile_data.get("current_profile", "decky-lsfg-vk"),
- "global_config": existing_profile_data.get("global_config", {}).copy(),
- "profiles": {}
- }
-
- # Merge global config: preserve user values, add missing fields, update DLL
- for key, default_value in default_global_config.items():
- if key not in merged_data["global_config"]:
- merged_data["global_config"][key] = default_value
- self.log.info(f"Added missing global field '{key}' with default value: {default_value}")
-
- # Update DLL path if detected
- dll_result = dll_service.check_lossless_scaling_dll()
- if dll_result.get("detected") and dll_result.get("path"):
- old_dll = merged_data["global_config"].get("dll")
- merged_data["global_config"]["dll"] = dll_result["path"]
- if old_dll != dll_result["path"]:
- self.log.info(f"Updated DLL path from '{old_dll}' to: {dll_result['path']}")
-
- # Merge each profile: preserve user values, add missing fields
- existing_profiles = existing_profile_data.get("profiles", {})
-
- for profile_name, existing_profile_config in existing_profiles.items():
- merged_profile_config = existing_profile_config.copy()
-
- # Add any missing fields from current schema with default values
- added_fields = []
- for key, default_value in default_config.items():
- if key not in merged_profile_config and key not in ["dll", "no_fp16"]: # Skip global fields
- merged_profile_config[key] = default_value
- added_fields.append(key)
-
- if added_fields:
- self.log.info(f"Profile '{profile_name}': Added missing fields {added_fields}")
-
- merged_data["profiles"][profile_name] = merged_profile_config
-
- # If no profiles exist, create the default one
- if not merged_data["profiles"]:
- merged_data["profiles"]["decky-lsfg-vk"] = {
- k: v for k, v in default_config.items()
- if k not in ["dll", "no_fp16"] # Exclude global fields
- }
- merged_data["current_profile"] = "decky-lsfg-vk"
- self.log.info("No existing profiles found, created default profile")
-
- return merged_data
+ self.uninstall()
+ except Exception as error:
+ self.log.error("Error cleaning up lsfg-vk files during uninstall: %s", error)
+ self.log.error("Traceback: %s", traceback.format_exc())