fix(plugin-loader): detect new deps via requirements.txt hash instead of empty marker

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 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-05-29 13:38:19 -04:00
parent f96fdd9f24
commit 6c4700583b
2 changed files with 17 additions and 7 deletions

View File

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

View File

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