From 706a5710a90ae68cdd633ca1a05876caa1fc06a8 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sat, 14 Feb 2026 23:46:01 +0000 Subject: [PATCH] feat: Auto-install plugin dependencies when enabling plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NEW FEATURES: 1. Plugin Dependency Declaration (BasePlugin): - Added 'dependencies' class attribute - Format: {'pip': ['package1', 'package2'], 'optional': {...}} - Plugins can declare required pip packages 2. Plugin Dependency Manager (core/plugin_dependency_manager.py): - Checks if declared dependencies are installed - Installs missing packages via pip - Tracks installation status - Shows progress dialog during installation 3. Settings Dialog Integration: - When enabling a plugin with dependencies, shows dialog - Lists missing dependencies - Asks user if they want to install - Shows progress bar during installation - Handles installation failures gracefully 4. Example: Game Reader Test plugin: - Declares dependencies: pillow, numpy - Optional: easyocr, pytesseract, paddleocr - When enabled, prompts to install if missing WORKFLOW: 1. User enables a plugin in Settings → Plugins 2. System checks if plugin has dependencies 3. If dependencies missing, shows dialog 4. User clicks Yes to install 5. Progress dialog shows installation progress 6. Plugin loads after dependencies installed This eliminates manual pip install steps for plugins! --- core/overlay_window.py | 70 ++++++++++- core/plugin_dependency_manager.py | 185 +++++++++++++++++++++++++++++ plugins/base_plugin.py | 4 + plugins/game_reader_test/plugin.py | 10 ++ 4 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 core/plugin_dependency_manager.py diff --git a/core/overlay_window.py b/core/overlay_window.py index 47c94bf..8bbb946 100644 --- a/core/overlay_window.py +++ b/core/overlay_window.py @@ -1303,7 +1303,7 @@ class OverlayWindow(QMainWindow): return tab def _save_settings(self, dialog): - """Save plugin settings.""" + """Save plugin settings with dependency checking.""" if not self.plugin_manager: dialog.accept() return @@ -1312,13 +1312,79 @@ class OverlayWindow(QMainWindow): self._refresh_ui() self.setStyleSheet(get_global_stylesheet()) - # Save plugin enable/disable + # Check for newly enabled plugins with dependencies + from core.plugin_dependency_manager import get_dependency_manager + dep_manager = get_dependency_manager() + + plugins_to_enable = [] for plugin_id, cb in self.settings_checkboxes.items(): if cb.isChecked(): + # Check if this is a newly enabled plugin + if not self.plugin_manager.is_plugin_enabled(plugin_id): + plugins_to_enable.append(plugin_id) self.plugin_manager.enable_plugin(plugin_id) else: self.plugin_manager.disable_plugin(plugin_id) + # Check dependencies for newly enabled plugins + for plugin_id in plugins_to_enable: + # Get plugin class + all_plugins = self.plugin_manager.get_all_discovered_plugins() + if plugin_id in all_plugins: + plugin_class = all_plugins[plugin_id] + if dep_manager.has_dependencies(plugin_class): + installed, missing = dep_manager.check_all_dependencies(plugin_class) + if missing: + # Show dependency dialog + dep_text = dep_manager.get_missing_dependencies_text(plugin_class) + from PyQt6.QtWidgets import QMessageBox + + msg_box = QMessageBox(self) + msg_box.setWindowTitle("Install Dependencies?") + msg_box.setText(f"Plugin '{plugin_class.name}' requires additional dependencies:\n\n{dep_text}") + msg_box.setInformativeText("Would you like to install them now?") + msg_box.setStandardButtons( + QMessageBox.StandardButton.Yes | + QMessageBox.StandardButton.No + ) + msg_box.setDefaultButton(QMessageBox.StandardButton.Yes) + + reply = msg_box.exec() + + if reply == QMessageBox.StandardButton.Yes: + # Install dependencies + from PyQt6.QtWidgets import QProgressDialog + from PyQt6.QtCore import Qt + + progress = QProgressDialog( + f"Installing dependencies for {plugin_class.name}...", + "Cancel", + 0, + len(missing), + self + ) + progress.setWindowModality(Qt.WindowModality.WindowModal) + progress.setWindowTitle("Installing Dependencies") + + current = 0 + for dep in missing: + if progress.wasCanceled(): + break + + progress.setLabelText(f"Installing {dep.name}...") + progress.setValue(current) + + success, msg = dep_manager.install_dependency(dep.name) + if not success: + QMessageBox.warning( + self, + "Installation Failed", + f"Failed to install {dep.name}:\n{msg}" + ) + current += 1 + + progress.setValue(len(missing)) + # Reload plugins self._reload_plugins() dialog.accept() diff --git a/core/plugin_dependency_manager.py b/core/plugin_dependency_manager.py new file mode 100644 index 0000000..a46525d --- /dev/null +++ b/core/plugin_dependency_manager.py @@ -0,0 +1,185 @@ +""" +EU-Utility - Plugin Dependency Manager + +Manages plugin dependencies and auto-installs them when needed. +""" + +import sys +import subprocess +import importlib +from typing import Dict, List, Tuple, Optional, Any +from dataclasses import dataclass +from pathlib import Path +import json + + +@dataclass +class DependencyCheck: + """Result of a dependency check.""" + name: str + required: bool + installed: bool + version: Optional[str] = None + error: Optional[str] = None + + +class PluginDependencyManager: + """Manages plugin dependencies with auto-install capability.""" + + def __init__(self, plugin_manager=None): + self.plugin_manager = plugin_manager + self.install_status_file = Path("config/dependency_installs.json") + self._install_status = self._load_install_status() + + def _load_install_status(self) -> Dict: + """Load status of previously installed dependencies.""" + if self.install_status_file.exists(): + try: + with open(self.install_status_file, 'r') as f: + return json.load(f) + except: + pass + return {} + + def _save_install_status(self): + """Save status of installed dependencies.""" + try: + self.install_status_file.parent.mkdir(parents=True, exist_ok=True) + with open(self.install_status_file, 'w') as f: + json.dump(self._install_status, f, indent=2) + except Exception as e: + print(f"[DependencyManager] Failed to save status: {e}") + + def check_dependency(self, package_name: str) -> DependencyCheck: + """Check if a pip package is installed.""" + # Handle version specifiers (e.g., "package>=1.0") + base_name = package_name.split('[')[0].split('>')[0].split('<')[0].split('=')[0].strip() + + try: + module = importlib.import_module(base_name.replace('-', '_')) + version = getattr(module, '__version__', 'unknown') + return DependencyCheck( + name=package_name, + required=True, + installed=True, + version=version + ) + except ImportError: + return DependencyCheck( + name=package_name, + required=True, + installed=False + ) + except Exception as e: + return DependencyCheck( + name=package_name, + required=True, + installed=False, + error=str(e) + ) + + def check_all_dependencies(self, plugin_class) -> Tuple[List[DependencyCheck], List[DependencyCheck]]: + """Check all dependencies for a plugin. + + Returns: + Tuple of (installed_deps, missing_deps) + """ + deps = getattr(plugin_class, 'dependencies', {}) + pip_deps = deps.get('pip', []) + + installed = [] + missing = [] + + for dep in pip_deps: + check = self.check_dependency(dep) + if check.installed: + installed.append(check) + else: + missing.append(check) + + return installed, missing + + def install_dependency(self, package_name: str) -> Tuple[bool, str]: + """Install a pip package. + + Returns: + (success, message) + """ + try: + print(f"[DependencyManager] Installing {package_name}...") + result = subprocess.run( + [sys.executable, '-m', 'pip', 'install', package_name], + capture_output=True, + text=True, + timeout=300 + ) + + if result.returncode == 0: + self._install_status[package_name] = { + 'installed': True, + 'output': result.stdout[-500:] if len(result.stdout) > 500 else result.stdout + } + self._save_install_status() + return True, f"Successfully installed {package_name}" + else: + return False, f"Failed to install {package_name}: {result.stderr}" + + except subprocess.TimeoutExpired: + return False, f"Installation of {package_name} timed out" + except Exception as e: + return False, f"Error installing {package_name}: {e}" + + def install_all_missing(self, plugin_class, progress_callback=None) -> Tuple[bool, List[str]]: + """Install all missing dependencies for a plugin. + + Returns: + (all_successful, messages) + """ + _, missing = self.check_all_dependencies(plugin_class) + + if not missing: + return True, ["All dependencies already installed"] + + messages = [] + all_success = True + + for dep in missing: + if progress_callback: + progress_callback(f"Installing {dep.name}...") + + success, msg = self.install_dependency(dep.name) + messages.append(msg) + + if not success: + all_success = False + + return all_success, messages + + def get_missing_dependencies_text(self, plugin_class) -> str: + """Get a formatted text of missing dependencies.""" + _, missing = self.check_all_dependencies(plugin_class) + + if not missing: + return "All dependencies are installed." + + lines = [f"Missing dependencies ({len(missing)}):"] + for dep in missing: + lines.append(f" • {dep.name}") + + return "\n".join(lines) + + def has_dependencies(self, plugin_class) -> bool: + """Check if a plugin has declared dependencies.""" + deps = getattr(plugin_class, 'dependencies', {}) + return bool(deps.get('pip', [])) + + +# Singleton instance +_dep_manager = None + +def get_dependency_manager(plugin_manager=None) -> PluginDependencyManager: + """Get the plugin dependency manager singleton.""" + global _dep_manager + if _dep_manager is None: + _dep_manager = PluginDependencyManager(plugin_manager) + return _dep_manager diff --git a/plugins/base_plugin.py b/plugins/base_plugin.py index 6c0e3bb..63708bd 100644 --- a/plugins/base_plugin.py +++ b/plugins/base_plugin.py @@ -28,6 +28,10 @@ class BasePlugin(ABC): hotkey: Optional[str] = None # e.g., "ctrl+shift+n" enabled: bool = True + # Dependencies - override in subclass + # Format: {'pip': ['package1', 'package2>=1.0'], 'optional': {'package3': 'description'}} + dependencies: Dict[str, Any] = {} + def __init__(self, overlay_window: 'OverlayWindow', config: Dict[str, Any]): self.overlay = overlay_window self.config = config diff --git a/plugins/game_reader_test/plugin.py b/plugins/game_reader_test/plugin.py index deeb1e2..648f71c 100644 --- a/plugins/game_reader_test/plugin.py +++ b/plugins/game_reader_test/plugin.py @@ -133,6 +133,16 @@ class GameReaderTestPlugin(BasePlugin): author = "EU-Utility" description = "Debug tool for testing OCR and screen reading" + # Dependencies for OCR functionality + dependencies = { + 'pip': ['pillow', 'numpy'], + 'optional': { + 'easyocr': 'Best OCR accuracy, auto-downloads models', + 'pytesseract': 'Alternative OCR engine', + 'paddleocr': 'Advanced OCR with layout detection' + } + } + def __init__(self, overlay_window, config): super().__init__(overlay_window, config) self.test_history = []