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. Extracted from PluginManager to improve separation of concerns.
""" """
import hashlib
import json import json
import importlib import importlib
import importlib.util import importlib.util
@@ -164,11 +165,15 @@ class PluginLoader:
if not requirements_file.exists(): if not requirements_file.exists():
return True # No dependencies needed return True # No dependencies needed
marker_path = plugin_dir_resolved / ".dependencies_installed" 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(): if marker_path.exists():
self.logger.debug("Dependencies already installed for %s", plugin_id) 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 return True
self.logger.info("Requirements changed for %s, reinstalling dependencies", plugin_id)
try: try:
self.logger.info("Installing dependencies for plugin %s...", plugin_id) self.logger.info("Installing dependencies for plugin %s...", plugin_id)
@@ -181,9 +186,7 @@ class PluginLoader:
) )
if result.returncode == 0: if result.returncode == 0:
# Mark as installed marker_path.write_text(current_hash, encoding='utf-8')
marker_path.touch()
# Set proper file permissions after creating marker
ensure_file_permissions(marker_path, get_plugin_file_mode()) ensure_file_permissions(marker_path, get_plugin_file_mode())
self.logger.info("Dependencies installed successfully for %s", plugin_id) self.logger.info("Dependencies installed successfully for %s", plugin_id)
return True return True
@@ -199,7 +202,7 @@ class PluginLoader:
"Assuming they are satisfied: %s", "Assuming they are satisfied: %s",
plugin_id, stderr.strip() 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()) ensure_file_permissions(marker_path, get_plugin_file_mode())
return True return True
self.logger.warning( 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. from both the official registry and custom GitHub repositories.
""" """
import hashlib
import os import os
import json import json
import stat import stat
@@ -1755,6 +1756,12 @@ class PluginStoreManager:
timeout=300 timeout=300
) )
self.logger.info(f"Dependencies installed successfully for {plugin_path.name}") 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 return True
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e: