Sync from development - prepare for v0.2.1

This commit is contained in:
Omni
2026-01-12 22:15:19 +00:00
parent 9b5310c2f9
commit 29e1800074
75 changed files with 3007 additions and 523 deletions

View File

@@ -2885,8 +2885,9 @@ echo Prefix creation complete.
logger.info(f"Replacing existing shortcut: {shortcut_name}")
# First, remove the existing shortcut using STL
if getattr(sys, 'frozen', False):
stl_path = Path(sys._MEIPASS) / "steamtinkerlaunch"
appdir = os.environ.get('APPDIR')
if appdir:
stl_path = Path(appdir) / "opt" / "jackify" / "steamtinkerlaunch"
else:
project_root = Path(__file__).parent.parent.parent.parent.parent
stl_path = project_root / "external_repos/steamtinkerlaunch/steamtinkerlaunch"
@@ -3045,7 +3046,25 @@ echo Prefix creation complete.
in_target_section = False
path_updated = False
wine_path = new_path.replace('/', '\\\\')
# Determine Wine drive letter based on SD card detection
from jackify.backend.handlers.filesystem_handler import FileSystemHandler
from jackify.backend.handlers.path_handler import PathHandler
linux_path = Path(new_path)
if FileSystemHandler.is_sd_card(linux_path):
# SD card paths use D: drive
# Strip SD card prefix using the same method as other handlers
relative_sd_path_str = PathHandler._strip_sdcard_path_prefix(linux_path)
wine_path = relative_sd_path_str.replace('/', '\\')
wine_drive = "D:"
logger.debug(f"SD card path detected: {new_path} -> D:\\{wine_path}")
else:
# Regular paths use Z: drive with full path
wine_path = new_path.strip('/').replace('/', '\\')
wine_drive = "Z:"
logger.debug(f"Regular path: {new_path} -> Z:\\{wine_path}")
# Update existing path if found
for i, line in enumerate(lines):
@@ -3055,14 +3074,14 @@ echo Prefix creation complete.
elif stripped_line.startswith('[') and in_target_section:
in_target_section = False
elif in_target_section and f'"{path_key}"' in line:
lines[i] = f'"{path_key}"="Z:\\\\{wine_path}\\\\"\n' # Add trailing backslashes
lines[i] = f'"{path_key}"="{wine_drive}\\\\{wine_path}\\\\"\n' # Add trailing backslashes
path_updated = True
break
# Add new section if path wasn't updated
if not path_updated:
lines.append(f'\n{section_name}\n')
lines.append(f'"{path_key}"="Z:\\\\{wine_path}\\\\"\n') # Add trailing backslashes
lines.append(f'"{path_key}"="{wine_drive}\\\\{wine_path}\\\\"\n') # Add trailing backslashes
# Write updated content
with open(system_reg_path, 'w', encoding='utf-8') as f:

View File

@@ -275,8 +275,16 @@ class ModlistService:
actual_download_path = Path(download_dir_context)
download_dir_str = str(actual_download_path)
api_key = context['nexus_api_key']
oauth_info = context.get('nexus_oauth_info')
# CRITICAL: Re-check authentication right before launching engine
# This ensures we use current auth state, not stale cached values from context
# (e.g., if user revoked OAuth after context was created)
from ..services.nexus_auth_service import NexusAuthService
auth_service = NexusAuthService()
current_api_key, current_oauth_info = auth_service.get_auth_for_engine()
# Use current auth state, fallback to context values only if current check failed
api_key = current_api_key or context.get('nexus_api_key')
oauth_info = current_oauth_info or context.get('nexus_oauth_info')
# Path to the engine binary (copied from working code)
engine_path = get_jackify_engine_path()
@@ -311,6 +319,10 @@ class ModlistService:
# Environment setup - prefer NEXUS_OAUTH_INFO (supports auto-refresh) over NEXUS_API_KEY
if oauth_info:
os.environ['NEXUS_OAUTH_INFO'] = oauth_info
# CRITICAL: Set client_id so engine can refresh tokens with correct client_id
# Engine's RefreshToken method reads this to use our "jackify" client_id instead of hardcoded "wabbajack"
from jackify.backend.services.nexus_oauth_service import NexusOAuthService
os.environ['NEXUS_OAUTH_CLIENT_ID'] = NexusOAuthService.CLIENT_ID
# Also set NEXUS_API_KEY for backward compatibility
if api_key:
os.environ['NEXUS_API_KEY'] = api_key
@@ -549,18 +561,42 @@ class ModlistService:
success = modlist_menu.run_modlist_configuration_phase(config_context)
debug_callback(f"Configuration phase result: {success}")
# Restore stdout before calling completion callback
# Restore stdout before ENB detection and completion callback
if original_stdout:
sys.stdout = original_stdout
original_stdout = None
# Configure ENB for Linux compatibility (non-blocking)
# Do this BEFORE completion callback so we can pass detection status
enb_detected = False
try:
from ..handlers.enb_handler import ENBHandler
enb_handler = ENBHandler()
enb_success, enb_message, enb_detected = enb_handler.configure_enb_for_linux(context.install_dir)
if enb_message:
if enb_success:
logger.info(enb_message)
if progress_callback:
progress_callback(enb_message)
else:
logger.warning(enb_message)
# Non-blocking: continue workflow even if ENB config fails
except Exception as e:
logger.warning(f"ENB configuration skipped due to error: {e}")
# Continue workflow - ENB config is optional
# Store ENB detection status in context for GUI to use
context.enb_detected = enb_detected
if completion_callback:
if success:
debug_callback("Configuration completed successfully, calling completion callback")
completion_callback(True, "Configuration completed successfully!", context.name)
# Pass ENB detection status through callback
completion_callback(True, "Configuration completed successfully!", context.name, enb_detected)
else:
debug_callback("Configuration failed, calling completion callback with failure")
completion_callback(False, "Configuration failed", context.name)
completion_callback(False, "Configuration failed", context.name, False)
return success

View File

@@ -569,4 +569,57 @@ class NativeSteamService:
except Exception as e:
logger.error(f"Error removing shortcut: {e}")
return False
def create_steam_library_symlinks(self, app_id: int) -> bool:
"""
Create symlink to libraryfolders.vdf in Wine prefix for game detection.
This allows Wabbajack running in the prefix to detect Steam games.
Based on Wabbajack-Proton-AuCu implementation.
Args:
app_id: Steam AppID (unsigned)
Returns:
True if successful
"""
# Ensure Steam user detection is completed first
if not self.steam_path:
if not self.find_steam_user():
logger.error("Cannot create symlinks: Steam user detection failed")
return False
# Find libraryfolders.vdf
libraryfolders_vdf = self.steam_path / "config" / "libraryfolders.vdf"
if not libraryfolders_vdf.exists():
logger.error(f"libraryfolders.vdf not found at: {libraryfolders_vdf}")
return False
# Get compatdata path for this AppID
compat_data = self.steam_path / f"steamapps/compatdata/{app_id}"
if not compat_data.exists():
logger.error(f"Compatdata directory not found: {compat_data}")
return False
# Target directory in Wine prefix
prefix_config_dir = compat_data / "pfx/drive_c/Program Files (x86)/Steam/config"
prefix_config_dir.mkdir(parents=True, exist_ok=True)
# Symlink target
symlink_target = prefix_config_dir / "libraryfolders.vdf"
try:
# Remove existing symlink/file if it exists
if symlink_target.exists() or symlink_target.is_symlink():
symlink_target.unlink()
# Create symlink
symlink_target.symlink_to(libraryfolders_vdf)
logger.info(f"Created symlink: {symlink_target} -> {libraryfolders_vdf}")
return True
except Exception as e:
logger.error(f"Error creating symlink: {e}")
return False

View File

@@ -102,7 +102,6 @@ class NexusOAuthService:
# Determine executable path (DEV mode vs AppImage)
# Check multiple indicators for AppImage execution
is_appimage = (
getattr(sys, 'frozen', False) or # PyInstaller frozen
'APPIMAGE' in env or # AppImage environment variable
'APPDIR' in env or # AppImage directory variable
(sys.argv[0] and sys.argv[0].endswith('.AppImage')) # Executable name
@@ -127,7 +126,8 @@ class NexusOAuthService:
# Running from source (DEV mode)
# Need to ensure we run from the correct directory
src_dir = Path(__file__).parent.parent.parent.parent # Go up to src/
exec_path = f"cd {src_dir} && {sys.executable} -m jackify.frontends.gui"
# Use bash -c with proper quoting for paths with spaces
exec_path = f'bash -c \'cd "{src_dir}" && "{sys.executable}" -m jackify.frontends.gui "$@"\' --'
logger.info(f"DEV mode exec path: {exec_path}")
logger.info(f"Source directory: {src_dir}")
@@ -139,29 +139,43 @@ class NexusOAuthService:
else:
# Check if Exec path matches current mode
current_content = desktop_file.read_text()
if f"Exec={exec_path} %u" not in current_content:
# Check for both quoted (AppImage) and unquoted (DEV mode with bash -c) formats
if is_appimage:
expected_exec = f'Exec="{exec_path}" %u'
else:
expected_exec = f"Exec={exec_path} %u"
if expected_exec not in current_content:
needs_update = True
logger.info(f"Updating desktop file with new Exec path: {exec_path}")
# Explicitly detect and fix malformed entries (unquoted paths with spaces)
# Check if any Exec line exists without quotes but contains spaces
if is_appimage and ' ' in exec_path:
import re
# Look for Exec=<path with spaces> without quotes
if re.search(r'Exec=[^"]\S*\s+\S*\.AppImage', current_content):
needs_update = True
logger.info("Fixing malformed desktop file (unquoted path with spaces)")
if needs_update:
desktop_file.parent.mkdir(parents=True, exist_ok=True)
# Build desktop file content with proper working directory
if is_appimage:
# AppImage doesn't need working directory
# AppImage - quote path to handle spaces
desktop_content = f"""[Desktop Entry]
Type=Application
Name=Jackify
Comment=Wabbajack modlist manager for Linux
Exec={exec_path} %u
Exec="{exec_path}" %u
Icon=com.jackify.app
Terminal=false
Categories=Game;Utility;
MimeType=x-scheme-handler/jackify;
"""
else:
# DEV mode needs working directory set to src/
# exec_path already contains the correct format: "cd {src_dir} && {sys.executable} -m jackify.frontends.gui"
# DEV mode - exec_path already contains bash -c with proper quoting
src_dir = Path(__file__).parent.parent.parent.parent # Go up to src/
desktop_content = f"""[Desktop Entry]
Type=Application