From 6c4700583bbbab666ea0a877f510b924f0f922c4 Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 29 May 2026 13:38:19 -0400 Subject: [PATCH] fix(plugin-loader): detect new deps via requirements.txt hash instead of empty marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The .dependencies_installed marker was an empty file, so adding a new package to requirements.txt (e.g. astral in ledmatrix-weather v2.3.0) never triggered a pip re-install on existing installs — the file existed so the check returned early. The marker now stores a SHA-256 hash of requirements.txt. On every plugin load, the loader compares the current hash to the stored one; a mismatch (or missing marker) triggers pip install and writes the new hash. store_manager._install_dependencies() also writes the hash marker after a store install/update so the loader skips a redundant pip run on next boot. Co-Authored-By: Claude Sonnet 4.6 --- src/plugin_system/plugin_loader.py | 17 ++++++++++------- src/plugin_system/store_manager.py | 7 +++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/plugin_system/plugin_loader.py b/src/plugin_system/plugin_loader.py index 75628bb0..f486fac9 100644 --- a/src/plugin_system/plugin_loader.py +++ b/src/plugin_system/plugin_loader.py @@ -5,6 +5,7 @@ Handles plugin module imports, dependency installation, and class instantiation. Extracted from PluginManager to improve separation of concerns. """ +import hashlib import json import importlib import importlib.util @@ -164,11 +165,15 @@ class PluginLoader: if not requirements_file.exists(): return True # No dependencies needed marker_path = plugin_dir_resolved / ".dependencies_installed" + current_hash = hashlib.sha256(requirements_file.read_bytes()).hexdigest() - # Check if already installed + # Skip if requirements.txt hasn't changed since last install if marker_path.exists(): - self.logger.debug("Dependencies already installed for %s", plugin_id) - return True + stored_hash = marker_path.read_text(encoding='utf-8').strip() + if stored_hash == current_hash: + self.logger.debug("Dependencies already installed for %s (requirements unchanged)", plugin_id) + return True + self.logger.info("Requirements changed for %s, reinstalling dependencies", plugin_id) try: self.logger.info("Installing dependencies for plugin %s...", plugin_id) @@ -181,9 +186,7 @@ class PluginLoader: ) if result.returncode == 0: - # Mark as installed - marker_path.touch() - # Set proper file permissions after creating marker + marker_path.write_text(current_hash, encoding='utf-8') ensure_file_permissions(marker_path, get_plugin_file_mode()) self.logger.info("Dependencies installed successfully for %s", plugin_id) return True @@ -199,7 +202,7 @@ class PluginLoader: "Assuming they are satisfied: %s", plugin_id, stderr.strip() ) - marker_path.touch() + marker_path.write_text(current_hash, encoding='utf-8') ensure_file_permissions(marker_path, get_plugin_file_mode()) return True self.logger.warning( diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index 5a774056..2f2140cf 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -5,6 +5,7 @@ Handles plugin discovery, installation, updates, and uninstallation from both the official registry and custom GitHub repositories. """ +import hashlib import os import json import stat @@ -1755,6 +1756,12 @@ class PluginStoreManager: timeout=300 ) self.logger.info(f"Dependencies installed successfully for {plugin_path.name}") + # Write hash marker so plugin_loader skips redundant pip run on next startup + try: + current_hash = hashlib.sha256(requirements_file.read_bytes()).hexdigest() + (plugin_path / ".dependencies_installed").write_text(current_hash, encoding='utf-8') + except OSError as marker_err: + self.logger.debug("Could not write dependency marker for %s: %s", plugin_path.name, marker_err) return True except subprocess.CalledProcessError as e: