39 lines
1.5 KiB
Python
39 lines
1.5 KiB
Python
import importlib.util
|
|
import os
|
|
|
|
from .engine import SelectedPlugin
|
|
|
|
|
|
def discover_all(plugins_dir):
|
|
"""Return {name: (plugin_dir, get_manifest_callable)} for every plugin
|
|
directory that has a plugin.py exposing get_manifest(ctx)."""
|
|
found = {}
|
|
for name in sorted(os.listdir(plugins_dir)):
|
|
plugin_dir = os.path.join(plugins_dir, name)
|
|
manifest_path = os.path.join(plugin_dir, "plugin.py")
|
|
if not os.path.isdir(plugin_dir) or not os.path.exists(manifest_path):
|
|
continue
|
|
spec = importlib.util.spec_from_file_location(f"xteink_plugin_{name}", manifest_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
if not hasattr(module, "get_manifest"):
|
|
continue
|
|
found[name] = (plugin_dir, module.get_manifest)
|
|
return found
|
|
|
|
|
|
def build_selected(names, plugins_dir, ctx):
|
|
"""Build a deterministic, sorted-by-name list of SelectedPlugin for the
|
|
given plugin names. Order doesn't affect correctness (the engine derives
|
|
shared files from the whole set at once) but keeping it stable makes
|
|
output/diffs reproducible."""
|
|
found = discover_all(plugins_dir)
|
|
selected = []
|
|
for name in sorted(names):
|
|
if name not in found:
|
|
raise KeyError(f"Unknown plugin: {name} (available: {', '.join(sorted(found))})")
|
|
plugin_dir, get_manifest = found[name]
|
|
manifest = get_manifest(ctx)
|
|
selected.append(SelectedPlugin(name=name, plugin_dir=plugin_dir, manifest=manifest))
|
|
return selected
|