Add Discord-native hybrid front-end for Jarvis (bot + bridge)
Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Transform isair/jarvis into a Discord-controlled voice assistant running on the Ubuntu VNC desktop, keeping the mature ~39k-line Python brain intact. - bot/ (Node + bun, discord.js): /자비스 slash commands (ephemeral), voice channel join + voice receive/playback, pluggable VNC screen broadcast (selfbot live / noVNC / screenshot) - bridge/ (Python, Flask): wraps jarvis STT + run_reply_engine + Piper TTS behind a thin localhost HTTP API - .env.example, scripts/ (start_bridge/start_bot/dev), README rewrite, docs/language-comparison.md and docs/vnc-xfce-setup.md Language decision: hybrid (Python brain + Node/bun Discord layer) because Discord blocks bot video; native screen broadcast only works via a Node selfbot library.
This commit is contained in:
570
jarvis_desktop.spec
Normal file
570
jarvis_desktop.spec
Normal file
@@ -0,0 +1,570 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
"""
|
||||
PyInstaller spec file for Jarvis Desktop App
|
||||
Builds a standalone executable for Windows, macOS, and Linux
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||
|
||||
block_cipher = None
|
||||
|
||||
# Get the project root directory
|
||||
project_root = Path('.').absolute()
|
||||
src_path = project_root / 'src'
|
||||
|
||||
# Create qt.conf for macOS to help Qt find plugins correctly
|
||||
if sys.platform == 'darwin':
|
||||
qt_conf_path = project_root / 'qt.conf'
|
||||
qt_conf_path.write_text("""[Paths]
|
||||
Prefix = .
|
||||
Plugins = PyQt6/Qt6/plugins
|
||||
""")
|
||||
print(f"Created qt.conf at {qt_conf_path}")
|
||||
|
||||
# Collect all necessary data files
|
||||
# Note: Let PyInstaller's built-in hooks handle sounddevice, ctranslate2, and Qt WebEngine
|
||||
# Manual collection can conflict with hooks and cause crashes
|
||||
datas = [
|
||||
(str(src_path / 'desktop_app' / 'desktop_assets' / '*.png'), 'desktop_app/desktop_assets'),
|
||||
]
|
||||
|
||||
# Collect Piper TTS data files (espeak-ng-data is required for phonemization)
|
||||
try:
|
||||
import piper
|
||||
piper_path = Path(piper.__file__).parent
|
||||
# espeak-ng-data contains phoneme data needed for TTS
|
||||
espeak_data = piper_path / 'espeak-ng-data'
|
||||
if espeak_data.exists():
|
||||
datas.append((str(espeak_data), 'piper/espeak-ng-data'))
|
||||
print(f"Bundling Piper espeak-ng-data from {espeak_data}")
|
||||
# tashkeel contains Arabic diacritization data
|
||||
tashkeel_data = piper_path / 'tashkeel'
|
||||
if tashkeel_data.exists():
|
||||
datas.append((str(tashkeel_data), 'piper/tashkeel'))
|
||||
print(f"Bundling Piper tashkeel from {tashkeel_data}")
|
||||
except ImportError:
|
||||
print("Warning: piper not installed, TTS may not work in bundle")
|
||||
|
||||
# Bundle tzdata on Windows so zoneinfo can resolve IANA zones (Windows has no
|
||||
# system zoneinfo database). macOS/Linux read /usr/share/zoneinfo at runtime
|
||||
# and do not need the pip package.
|
||||
if sys.platform == 'win32':
|
||||
try:
|
||||
datas += collect_data_files('tzdata')
|
||||
print("Bundling tzdata for zoneinfo support on Windows")
|
||||
except Exception as e:
|
||||
print(f"Warning: could not collect tzdata: {e}")
|
||||
|
||||
# Add qt.conf for macOS
|
||||
if sys.platform == 'darwin':
|
||||
datas.append((str(project_root / 'qt.conf'), '.'))
|
||||
|
||||
# Collect Qt plugins for system tray functionality
|
||||
try:
|
||||
import PyQt6
|
||||
qt_path = Path(PyQt6.__file__).parent
|
||||
# Add Qt plugins for platform integration (needed for system tray on macOS)
|
||||
# Only add directories that actually exist (e.g., 'styles' may not exist on Linux)
|
||||
qt_plugin_dirs = [
|
||||
('platforms', 'PyQt6/Qt6/plugins/platforms'),
|
||||
('styles', 'PyQt6/Qt6/plugins/styles'),
|
||||
]
|
||||
for plugin_name, dest_path in qt_plugin_dirs:
|
||||
plugin_path = qt_path / 'Qt6' / 'plugins' / plugin_name
|
||||
if plugin_path.exists():
|
||||
datas.append((str(plugin_path), dest_path))
|
||||
else:
|
||||
print(f"Info: Qt plugin directory '{plugin_name}' not found, skipping")
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not collect Qt plugins: {e}")
|
||||
|
||||
# Note: Qt WebEngine resources are handled by PyInstaller's hook-PyQt6.QtWebEngineWidgets.py
|
||||
# Manual collection can conflict with the hook and cause crashes
|
||||
|
||||
# Hidden imports that PyInstaller might miss
|
||||
hiddenimports = [
|
||||
# Jarvis core modules
|
||||
'jarvis',
|
||||
'jarvis._version',
|
||||
'jarvis.daemon',
|
||||
'jarvis.config',
|
||||
'jarvis.debug',
|
||||
'jarvis.llm',
|
||||
'jarvis.main',
|
||||
# Desktop app modules
|
||||
'desktop_app',
|
||||
'desktop_app.app',
|
||||
'desktop_app.splash_screen',
|
||||
'desktop_app.setup_wizard',
|
||||
'desktop_app.updater',
|
||||
'desktop_app.update_dialog',
|
||||
'desktop_app.themes',
|
||||
'desktop_app.face_widget',
|
||||
'desktop_app.diary_dialog',
|
||||
'desktop_app.memory_viewer',
|
||||
# Listening modules
|
||||
'jarvis.listening',
|
||||
'jarvis.listening.echo_detection',
|
||||
'jarvis.listening.listener',
|
||||
'jarvis.listening.state_manager',
|
||||
'jarvis.listening.wake_detection',
|
||||
'jarvis.listening.transcript_buffer',
|
||||
'jarvis.listening.intent_judge',
|
||||
# Memory modules
|
||||
'jarvis.memory',
|
||||
'jarvis.memory.conversation',
|
||||
'jarvis.memory.db',
|
||||
'jarvis.memory.embeddings',
|
||||
# Output modules
|
||||
'jarvis.output',
|
||||
'jarvis.output.tts',
|
||||
'jarvis.output.tune_player',
|
||||
# Piper TTS (local neural TTS)
|
||||
'piper',
|
||||
'piper.voice',
|
||||
'piper.config',
|
||||
'piper.download',
|
||||
'piper.download_voices',
|
||||
'piper.phonemize_espeak',
|
||||
'piper.phoneme_ids',
|
||||
# ONNX Runtime (required by Piper for model inference)
|
||||
'onnxruntime',
|
||||
'onnxruntime.capi',
|
||||
'onnxruntime.capi._pybind_state',
|
||||
# Profile modules
|
||||
'jarvis.profile',
|
||||
'jarvis.profile.profiles',
|
||||
# Reply modules
|
||||
'jarvis.reply',
|
||||
'jarvis.reply.engine',
|
||||
'jarvis.reply.enrichment',
|
||||
# Tools modules
|
||||
'jarvis.tools',
|
||||
'jarvis.tools.base',
|
||||
'jarvis.tools.registry',
|
||||
'jarvis.tools.types',
|
||||
'jarvis.tools.builtin',
|
||||
'jarvis.tools.builtin.fetch_web_page',
|
||||
'jarvis.tools.builtin.local_files',
|
||||
'jarvis.tools.builtin.nutrition',
|
||||
'jarvis.tools.builtin.nutrition.delete_meal',
|
||||
'jarvis.tools.builtin.nutrition.fetch_meals',
|
||||
'jarvis.tools.builtin.nutrition.log_meal',
|
||||
'jarvis.tools.builtin.recall_conversation',
|
||||
'jarvis.tools.builtin.refresh_mcp_tools',
|
||||
'jarvis.tools.builtin.screenshot',
|
||||
'jarvis.tools.builtin.web_search',
|
||||
'jarvis.tools.external',
|
||||
'jarvis.tools.external.mcp_client',
|
||||
# Utils modules
|
||||
'jarvis.utils',
|
||||
'jarvis.utils.fast_vector_store',
|
||||
'jarvis.utils.fuzzy_search',
|
||||
'jarvis.utils.location',
|
||||
'jarvis.utils.redact',
|
||||
'jarvis.utils.vector_store',
|
||||
# PyQt6
|
||||
'PyQt6.QtCore',
|
||||
'PyQt6.QtGui',
|
||||
'PyQt6.QtWidgets',
|
||||
'PyQt6.sip',
|
||||
# PyQt6 WebEngine (for embedded memory viewer)
|
||||
'PyQt6.QtWebEngineWidgets',
|
||||
'PyQt6.QtWebEngineCore',
|
||||
'PyQt6.QtWebChannel',
|
||||
# Audio dependencies (critical for voice input)
|
||||
'sounddevice',
|
||||
'_sounddevice_data',
|
||||
'_sounddevice_data.portaudio-binaries',
|
||||
'webrtcvad',
|
||||
# Speech recognition (faster-whisper backend)
|
||||
'faster_whisper',
|
||||
'ctranslate2',
|
||||
'huggingface_hub',
|
||||
'huggingface_hub.file_download',
|
||||
'huggingface_hub.hf_api',
|
||||
'huggingface_hub.utils',
|
||||
'tokenizers',
|
||||
# Third-party dependencies
|
||||
'dotenv',
|
||||
'psutil',
|
||||
'requests',
|
||||
'numpy',
|
||||
'PIL',
|
||||
'PIL.Image',
|
||||
'rapidfuzz',
|
||||
'rapidfuzz.fuzz',
|
||||
'bs4',
|
||||
'lxml',
|
||||
'html2text',
|
||||
'faiss',
|
||||
'sqlite3',
|
||||
'json',
|
||||
'asyncio',
|
||||
'threading',
|
||||
'subprocess',
|
||||
'geoip2',
|
||||
'geoip2.database',
|
||||
'miniupnpc',
|
||||
# zoneinfo support on Windows (macOS/Linux use /usr/share/zoneinfo)
|
||||
'tzdata',
|
||||
'zoneinfo',
|
||||
# Flask for memory viewer
|
||||
'flask',
|
||||
'flask.json',
|
||||
'werkzeug',
|
||||
'werkzeug.serving',
|
||||
'werkzeug.routing',
|
||||
'werkzeug.utils',
|
||||
'werkzeug.datastructures',
|
||||
'werkzeug.wrappers',
|
||||
'werkzeug.exceptions',
|
||||
'jinja2',
|
||||
'markupsafe',
|
||||
'itsdangerous',
|
||||
'click',
|
||||
'blinker',
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
['src/desktop_app/app.py'],
|
||||
pathex=[str(src_path)],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=['src/desktop_app/rthook_onnxruntime.py'],
|
||||
excludes=[
|
||||
# Exclude heavy packages to keep bundle size reasonable
|
||||
'psycopg2', # Not used and causes OpenSSL conflicts
|
||||
'torch', # PyTorch is 1.5-2GB - chatterbox TTS is optional
|
||||
'torchaudio',
|
||||
'torchvision',
|
||||
'chatterbox', # Optional TTS engine (uses PyTorch)
|
||||
'transformers', # Heavy ML library (not needed, faster_whisper uses ctranslate2)
|
||||
'safetensors',
|
||||
'accelerate',
|
||||
'cv2', # OpenCV - not needed for core functionality
|
||||
'opencv-python',
|
||||
'matplotlib', # Not needed for core app
|
||||
'notebook',
|
||||
'jupyter',
|
||||
'IPython',
|
||||
'scipy', # Large, only used by optional features
|
||||
'sklearn',
|
||||
'scikit-learn',
|
||||
# Note: Keep huggingface_hub - needed by faster_whisper for model downloads
|
||||
],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
# Filter out heavy binaries on all platforms to reduce bundle size
|
||||
# Note: Be careful not to exclude libs needed by numpy/faster-whisper
|
||||
excluded_binary_patterns = [
|
||||
'torch', 'libtorch', 'libcaffe2', # PyTorch (~1.5GB)
|
||||
'torchaudio', 'torchvision',
|
||||
'cv2', 'opencv', 'libopencv', # OpenCV (~500MB)
|
||||
'sklearn', 'scikit', # scikit-learn
|
||||
'transformers', # Heavy ML library
|
||||
'chatterbox',
|
||||
'matplotlib',
|
||||
# Note: Keep huggingface_hub (needed by faster_whisper for model downloads)
|
||||
# Note: Keep libopenblas (needed by numpy) and libfreetype (needed by av/ffmpeg)
|
||||
]
|
||||
|
||||
# Exclude VC++ runtime DLLs from the bundle entirely. Different packages
|
||||
# (PyQt6, conda, etc.) ship conflicting versions that cause access-violation
|
||||
# crashes in onnxruntime. Instead of trying to pick the "right" version we
|
||||
# rely on the system-installed Microsoft Visual C++ Redistributable which
|
||||
# users are asked to install (see README). Also exclude other system DLLs
|
||||
# that PyInstaller picks up from non-system locations (e.g. Oculus).
|
||||
excluded_system_dlls = {
|
||||
'vcruntime140.dll', 'vcruntime140_1.dll',
|
||||
'msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll',
|
||||
'ucrtbase.dll', # Universal CRT — must come from Windows System32
|
||||
'dbghelp.dll', # Must come from Windows System32
|
||||
}
|
||||
|
||||
filtered_binaries = []
|
||||
for binary in a.binaries:
|
||||
name = binary[0].lower()
|
||||
binary_path = str(binary[1]).lower() if len(binary) > 1 else ''
|
||||
|
||||
# Check if this binary should be excluded
|
||||
should_exclude = False
|
||||
base_name = name.rsplit('\\', 1)[-1].rsplit('/', 1)[-1]
|
||||
|
||||
# Exclude all VC runtime and system DLLs — use system-installed versions
|
||||
if base_name in excluded_system_dlls:
|
||||
print(f"Excluding system DLL (use VC++ Redistributable): {binary[0]}")
|
||||
should_exclude = True
|
||||
|
||||
# Pattern-based exclusions (heavy libraries)
|
||||
if not should_exclude:
|
||||
for pattern in excluded_binary_patterns:
|
||||
if pattern in name or pattern in binary_path:
|
||||
print(f"Excluding heavy binary: {binary[0]}")
|
||||
should_exclude = True
|
||||
break
|
||||
|
||||
if not should_exclude:
|
||||
filtered_binaries.append(binary)
|
||||
|
||||
a.binaries = filtered_binaries
|
||||
|
||||
# Note: VC++ runtime DLL handling on Windows is managed by PyInstaller 6.13.0+
|
||||
# which has built-in pre-loading of system VC runtime DLLs
|
||||
|
||||
# On macOS, ensure OpenSSL libraries are bundled properly
|
||||
if sys.platform == 'darwin':
|
||||
# Remove any psycopg2 binaries and OpenCV's bundled OpenSSL (should be excluded already, but be safe)
|
||||
filtered_binaries = []
|
||||
for binary in a.binaries:
|
||||
name = binary[0]
|
||||
# Exclude psycopg2 entirely
|
||||
if 'psycopg2' in name.lower():
|
||||
print(f"Excluding psycopg2: {name}")
|
||||
continue
|
||||
filtered_binaries.append(binary)
|
||||
|
||||
# Find and bundle OpenSSL libraries from Python's dependencies
|
||||
# Python's SSL module needs these, and they should come from Python's installation
|
||||
python_executable = sys.executable
|
||||
python_lib_dir = Path(python_executable).parent.parent / 'lib'
|
||||
|
||||
# Try to find OpenSSL in Python's lib directory or common locations
|
||||
openssl_candidates = [
|
||||
# Check Python's lib directory (pyenv, virtualenv, etc.)
|
||||
python_lib_dir / 'libssl.3.dylib',
|
||||
python_lib_dir / 'libcrypto.3.dylib',
|
||||
# Check Homebrew locations (will bundle these into the app)
|
||||
Path('/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib'),
|
||||
Path('/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib'),
|
||||
Path('/opt/homebrew/lib/libssl.3.dylib'),
|
||||
Path('/opt/homebrew/lib/libcrypto.3.dylib'),
|
||||
# Check system locations
|
||||
Path('/usr/local/lib/libssl.3.dylib'),
|
||||
Path('/usr/local/lib/libcrypto.3.dylib'),
|
||||
]
|
||||
|
||||
openssl_libs = {
|
||||
'libssl.3.dylib': None,
|
||||
'libcrypto.3.dylib': None,
|
||||
}
|
||||
|
||||
# Find existing OpenSSL libraries
|
||||
for candidate in openssl_candidates:
|
||||
lib_name = candidate.name
|
||||
if lib_name in openssl_libs and candidate.exists() and openssl_libs[lib_name] is None:
|
||||
openssl_libs[lib_name] = candidate
|
||||
print(f"Found OpenSSL library: {candidate}")
|
||||
|
||||
# Remove any existing libssl/libcrypto entries first
|
||||
filtered_binaries = [b for b in filtered_binaries
|
||||
if not (b[0] == 'libssl.3.dylib' or b[0] == 'libcrypto.3.dylib')]
|
||||
|
||||
# Add found OpenSSL libraries
|
||||
for lib_name, lib_path in openssl_libs.items():
|
||||
if lib_path and lib_path.exists():
|
||||
print(f"Bundling OpenSSL: {lib_path} as {lib_name}")
|
||||
filtered_binaries.append((lib_name, str(lib_path), 'BINARY'))
|
||||
else:
|
||||
print(f"Warning: OpenSSL library {lib_name} not found - SSL may not work!")
|
||||
|
||||
a.binaries = filtered_binaries
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
# Platform-specific configurations
|
||||
if sys.platform == 'darwin':
|
||||
# macOS: Create .app bundle
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='Jarvis',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False, # No console for production
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=str(src_path / 'desktop_app' / 'desktop_assets' / 'icon_idle.png'),
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='Jarvis',
|
||||
)
|
||||
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name='Jarvis.app',
|
||||
icon=str(src_path / 'desktop_app' / 'desktop_assets' / 'icon_idle.png'),
|
||||
bundle_identifier='com.jarvis.assistant',
|
||||
info_plist={
|
||||
'NSHighResolutionCapable': 'True',
|
||||
'LSUIElement': '1', # Hide from dock
|
||||
'NSMicrophoneUsageDescription': 'Jarvis needs microphone access to listen for voice commands.',
|
||||
'NSScreenCaptureUsageDescription': 'Jarvis needs screen capture access to read text from your screen via OCR.',
|
||||
},
|
||||
)
|
||||
|
||||
# Post-build: Ensure OpenSSL libraries are correct and remove conflicting ones
|
||||
import shutil
|
||||
frameworks_dir = Path('dist/Jarvis.app/Contents/Frameworks')
|
||||
|
||||
# Remove OpenCV's bundled OpenSSL libraries (they conflict with Python's SSL)
|
||||
# Try both possible directory names
|
||||
for dylibs_dir_name in ['__dot__dylibs', '.dylibs']:
|
||||
cv2_dylibs_dir = frameworks_dir / 'cv2' / dylibs_dir_name
|
||||
if cv2_dylibs_dir.exists():
|
||||
for lib_name in ['libssl.3.dylib', 'libcrypto.3.dylib']:
|
||||
cv2_lib = cv2_dylibs_dir / lib_name
|
||||
if cv2_lib.exists():
|
||||
cv2_lib.unlink()
|
||||
print(f"Removed OpenCV bundled OpenSSL: {cv2_lib}")
|
||||
|
||||
# Also check Resources directory
|
||||
resources_dir = Path('dist/Jarvis.app/Contents/Resources')
|
||||
cv2_resources_dylibs = resources_dir / 'cv2' / '.dylibs'
|
||||
if cv2_resources_dylibs.exists():
|
||||
for lib_name in ['libssl.3.dylib', 'libcrypto.3.dylib']:
|
||||
cv2_lib = cv2_resources_dylibs / lib_name
|
||||
if cv2_lib.exists():
|
||||
cv2_lib.unlink()
|
||||
print(f"Removed OpenCV bundled OpenSSL from Resources: {cv2_lib}")
|
||||
|
||||
# Find OpenSSL libraries that were bundled (from the binaries we added)
|
||||
bundled_openssl = {}
|
||||
for binary in a.binaries:
|
||||
if binary[0] in ['libssl.3.dylib', 'libcrypto.3.dylib']:
|
||||
bundled_openssl[binary[0]] = Path(binary[1])
|
||||
|
||||
# Also check the source paths we used during build
|
||||
openssl_source_paths = {
|
||||
'libssl.3.dylib': Path('/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib'),
|
||||
'libcrypto.3.dylib': Path('/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib'),
|
||||
}
|
||||
# Fallback to homebrew lib if openssl@3 not found
|
||||
if not openssl_source_paths['libssl.3.dylib'].exists():
|
||||
openssl_source_paths = {
|
||||
'libssl.3.dylib': Path('/opt/homebrew/lib/libssl.3.dylib'),
|
||||
'libcrypto.3.dylib': Path('/opt/homebrew/lib/libcrypto.3.dylib'),
|
||||
}
|
||||
|
||||
# Fix any broken symlinks in Frameworks and ensure correct libraries are in place
|
||||
for lib_name in ['libssl.3.dylib', 'libcrypto.3.dylib']:
|
||||
lib_path = frameworks_dir / lib_name
|
||||
if lib_path.exists():
|
||||
if lib_path.is_symlink():
|
||||
# Check if symlink is broken
|
||||
try:
|
||||
lib_path.resolve(strict=True)
|
||||
# Symlink is valid, skip
|
||||
continue
|
||||
except (OSError, RuntimeError):
|
||||
# Broken symlink - remove it
|
||||
lib_path.unlink()
|
||||
print(f"Removed broken symlink: {lib_path}")
|
||||
else:
|
||||
# File exists and is not a symlink, check if it's valid
|
||||
if lib_path.stat().st_size > 0:
|
||||
# File looks valid, skip
|
||||
continue
|
||||
|
||||
# Library doesn't exist or was removed - copy from source
|
||||
source_lib = None
|
||||
if lib_name in bundled_openssl and bundled_openssl[lib_name].exists():
|
||||
source_lib = bundled_openssl[lib_name]
|
||||
elif lib_name in openssl_source_paths and openssl_source_paths[lib_name].exists():
|
||||
source_lib = openssl_source_paths[lib_name]
|
||||
|
||||
if source_lib and source_lib.exists():
|
||||
shutil.copy2(source_lib, lib_path)
|
||||
print(f"Fixed OpenSSL library: {source_lib} -> {lib_path}")
|
||||
else:
|
||||
print(f"Warning: Could not find source for {lib_name}")
|
||||
|
||||
elif sys.platform == 'win32':
|
||||
# Windows: Create onedir distribution (directory with EXE + DLLs alongside)
|
||||
# This avoids the VC++ runtime DLL conflicts that plague onefile mode and
|
||||
# enables packaging via Inno Setup installer.
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='Jarvis',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=str(src_path / 'desktop_app' / 'desktop_assets' / 'icon_idle.ico'),
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='Jarvis',
|
||||
)
|
||||
|
||||
else:
|
||||
# Linux: Create directory-based distribution (more reliable than one-file)
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='Jarvis',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
name='Jarvis',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user