Sync from development - prepare for v0.2.0

This commit is contained in:
Omni
2025-12-06 20:09:55 +00:00
parent fe14e4ecfb
commit ce969eba1b
277 changed files with 14059 additions and 3899 deletions

View File

@@ -3,17 +3,119 @@ import signal
import subprocess
import time
import resource
import sys
import shutil
def get_safe_python_executable():
"""
Get a safe Python executable for subprocess calls.
When running as AppImage, returns system Python instead of AppImage path
to prevent recursive AppImage spawning.
Returns:
str: Path to Python executable safe for subprocess calls
"""
# Check if we're running as AppImage
is_appimage = (
'APPIMAGE' in os.environ or
'APPDIR' in os.environ or
(hasattr(sys, 'frozen') and sys.frozen) or
(sys.argv[0] and sys.argv[0].endswith('.AppImage'))
)
if is_appimage:
# Running as AppImage - use system Python to avoid recursive spawning
# Try to find system Python (same logic as AppRun)
for cmd in ['python3', 'python3.13', 'python3.12', 'python3.11', 'python3.10', 'python3.9', 'python3.8']:
python_path = shutil.which(cmd)
if python_path:
return python_path
# Fallback: if we can't find system Python, this is a problem
# But we'll still return sys.executable as last resort
return sys.executable
else:
# Not AppImage - sys.executable is safe
return sys.executable
def get_clean_subprocess_env(extra_env=None):
"""
Returns a copy of os.environ with PyInstaller and other problematic variables removed.
Returns a copy of os.environ with bundled-runtime variables and other problematic entries removed.
Optionally merges in extra_env dict.
Also ensures bundled tools (lz4, unzip, etc.) are in PATH when running as AppImage.
CRITICAL: Preserves system PATH to ensure system tools (like lz4) are available.
"""
from pathlib import Path
env = os.environ.copy()
# Remove PyInstaller-specific variables
# Remove AppImage-specific variables that can confuse subprocess calls
# These variables cause subprocesses to be interpreted as new AppImage launches
for key in ['APPIMAGE', 'APPDIR', 'ARGV0', 'OWD']:
env.pop(key, None)
# Remove bundle-specific variables
for k in list(env):
if k.startswith('_MEIPASS'):
del env[k]
# Get current PATH - ensure we preserve system paths
current_path = env.get('PATH', '')
# Ensure common system directories are in PATH if not already present
# This is critical for tools like lz4 that might be in /usr/bin, /usr/local/bin, etc.
system_paths = ['/usr/bin', '/usr/local/bin', '/bin', '/sbin', '/usr/sbin']
path_parts = current_path.split(':') if current_path else []
for sys_path in system_paths:
if sys_path not in path_parts and os.path.isdir(sys_path):
path_parts.append(sys_path)
# Add bundled tools directory to PATH if running as AppImage
# This ensures lz4, unzip, xz, etc. are available to subprocesses
appdir = env.get('APPDIR')
tools_dir = None
if appdir:
# Running as AppImage - use APPDIR
tools_dir = os.path.join(appdir, 'opt', 'jackify', 'tools')
# Verify the tools directory exists and contains lz4
if not os.path.isdir(tools_dir):
tools_dir = None
elif not os.path.exists(os.path.join(tools_dir, 'lz4')):
# Tools dir exists but lz4 not there - might be a different layout
tools_dir = None
elif getattr(sys, 'frozen', False):
# PyInstaller frozen - try to find tools relative to executable
exe_path = Path(sys.executable)
# In PyInstaller, sys.executable is the bundled executable
# Tools should be in the same directory or a tools subdirectory
possible_tools_dirs = [
exe_path.parent / 'tools',
exe_path.parent / 'opt' / 'jackify' / 'tools',
]
for possible_dir in possible_tools_dirs:
if possible_dir.is_dir() and (possible_dir / 'lz4').exists():
tools_dir = str(possible_dir)
break
# Build final PATH: bundled tools first (if any), then original PATH with system paths
final_path_parts = []
if tools_dir and os.path.isdir(tools_dir):
# Prepend tools directory so bundled tools take precedence
# This is critical - bundled lz4 must come before system lz4
final_path_parts.append(tools_dir)
# Add all other paths (preserving order, removing duplicates)
# Note: AppRun already sets PATH with tools directory, but we ensure it's first
seen = set()
if tools_dir:
seen.add(tools_dir) # Already added, don't add again
for path_part in path_parts:
if path_part and path_part not in seen:
final_path_parts.append(path_part)
seen.add(path_part)
env['PATH'] = ':'.join(final_path_parts)
# Optionally restore LD_LIBRARY_PATH to system default if needed
# (You can add more logic here if you know your system's default)
if extra_env:
@@ -59,7 +161,11 @@ class ProcessManager:
"""
def __init__(self, cmd, env=None, cwd=None, text=False, bufsize=0):
self.cmd = cmd
self.env = env
# Default to cleaned environment if None to prevent AppImage variable inheritance
if env is None:
self.env = get_clean_subprocess_env()
else:
self.env = env
self.cwd = cwd
self.text = text
self.bufsize = bufsize