feat: Auto-install plugin dependencies when enabling plugins

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!
This commit is contained in:
LemonNexus 2026-02-14 23:46:01 +00:00
parent 6931c4b039
commit 706a5710a9
4 changed files with 267 additions and 2 deletions

View File

@ -1303,7 +1303,7 @@ class OverlayWindow(QMainWindow):
return tab return tab
def _save_settings(self, dialog): def _save_settings(self, dialog):
"""Save plugin settings.""" """Save plugin settings with dependency checking."""
if not self.plugin_manager: if not self.plugin_manager:
dialog.accept() dialog.accept()
return return
@ -1312,13 +1312,79 @@ class OverlayWindow(QMainWindow):
self._refresh_ui() self._refresh_ui()
self.setStyleSheet(get_global_stylesheet()) 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(): for plugin_id, cb in self.settings_checkboxes.items():
if cb.isChecked(): 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) self.plugin_manager.enable_plugin(plugin_id)
else: else:
self.plugin_manager.disable_plugin(plugin_id) 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 # Reload plugins
self._reload_plugins() self._reload_plugins()
dialog.accept() dialog.accept()

View File

@ -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

View File

@ -28,6 +28,10 @@ class BasePlugin(ABC):
hotkey: Optional[str] = None # e.g., "ctrl+shift+n" hotkey: Optional[str] = None # e.g., "ctrl+shift+n"
enabled: bool = True 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]): def __init__(self, overlay_window: 'OverlayWindow', config: Dict[str, Any]):
self.overlay = overlay_window self.overlay = overlay_window
self.config = config self.config = config

View File

@ -133,6 +133,16 @@ class GameReaderTestPlugin(BasePlugin):
author = "EU-Utility" author = "EU-Utility"
description = "Debug tool for testing OCR and screen reading" 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): def __init__(self, overlay_window, config):
super().__init__(overlay_window, config) super().__init__(overlay_window, config)
self.test_history = [] self.test_history = []