← Files LuciaARCHIVED FILE

scripts/check_dependencies.py

6.13 KB · Nov 11, 2026 · 18:09 UTC

↓ Download file

See the change to this file →

#!/usr/bin/env python3
"""Check Lucia module availability and delegate dependency checks."""

from __future__ import annotations

import argparse
import json
import logging
import os
import subprocess
import sys
from pathlib import Path

from managed_python_runtime import ensure_runtime, run_verified_helper
from verified_execution import (
    prepare_execution_root,
    release_execution_root,
    report_preparation_failure,
)

__all__ = ["main"]

LOGGER = logging.getLogger(__name__)
PLUGIN_ROOT = Path(__file__).resolve().parents[1]


def _registered_components() -> tuple[str, ...]:
    """Read the canonical component registry used to build Lucia packages."""

    payload = json.loads((PLUGIN_ROOT / "components.json").read_text(encoding="utf-8"))
    components = payload.get("plugins")
    if (
        not isinstance(components, list)
        or not components
        or any(not isinstance(name, str) or not name for name in components)
    ):
        raise RuntimeError("Lucia components.json has no valid plugins registry.")
    return tuple(components)


COMPONENTS = _registered_components()


def _component_root(name: str) -> Path:
    """Return the packaged or repo-source root for one component."""

    packaged = PLUGIN_ROOT / "modules" / name
    if packaged.is_dir():
        return packaged
    return PLUGIN_ROOT.parent / name


def main(argv: list[str] | None = None) -> int:
    """Validate all components or run one component dependency checker."""

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--module", choices=COMPONENTS)
    parser.add_argument("--requirements", action="append")
    args, remaining = parser.parse_known_args(argv)

    missing = [name for name in COMPONENTS if not _component_root(name).is_dir()]
    if missing:
        LOGGER.error("Missing Lucia modules: %s", ", ".join(missing))
        report_preparation_failure(
            PLUGIN_ROOT,
            args.module,
            ValueError("Missing packaged Lucia components"),
            stage="dependency-setup",
            technical_code="runtime.missing-components",
            technical_type="MissingComponents",
        )
        return 1
    if args.module is None:
        LOGGER.info("All %s Lucia modules are available.", len(COMPONENTS))
        return 0

    try:
        execution_root = prepare_execution_root(PLUGIN_ROOT, args.module)
    except (OSError, ValueError) as error:
        LOGGER.error("Lucia installation verification failed: %s", error)
        report_preparation_failure(PLUGIN_ROOT, args.module, error)
        return 1
    try:
        component_root = (
            execution_root / "modules" / args.module
            if execution_root != PLUGIN_ROOT
            else _component_root(args.module)
        )
        checker = component_root / "scripts" / "check_dependencies.py"
        if not checker.exists():
            LOGGER.error(
                "Dependency checker not found for %s: %s", args.module, checker
            )
            report_preparation_failure(
                PLUGIN_ROOT,
                args.module,
                ValueError("Published dependency checker is missing"),
                stage="dependency-setup",
                technical_code="runtime.missing-checker",
                technical_type="MissingDependencyChecker",
            )
            return 1
        delegated_args = list(remaining)
        for requirement in args.requirements or []:
            delegated_args = ["--requirements", requirement, *delegated_args]
        ready, target, detail = ensure_runtime(
            execution_root,
            args.module,
            requirements=args.requirements,
        )
        if not ready and detail.startswith(
            "Shared runtime update requires the base Python interpreter"
        ):
            # An activated host hook may put the managed venv first on PATH.
            # Re-enter this same verified checker under its recorded base interpreter;
            # keep the shared runtime's prohibition on self-updating intact.
            base = Path(getattr(sys, "_base_executable", sys.executable))
            if base.is_file() and base.absolute() != Path(sys.executable).absolute():
                completed = subprocess.run(
                    [
                        str(base),
                        str(execution_root / "scripts/check_dependencies.py"),
                        *(sys.argv[1:] if argv is None else argv),
                    ],
                    cwd=execution_root,
                    check=False,
                )
                return completed.returncode
        if not ready:
            from operational_incidents import report_failure

            if not os.environ.get("MPARANZA_INCIDENT_RECOVERY_ACTIVE"):
                report_failure(
                    args.module,
                    "dependency-setup",
                    detail,
                    capture_state="complete",
                    technical_diagnostics=[
                        {
                            "component": "lucia-runtime",
                            "stage": "dependency-setup",
                            "error_type": "RuntimeSetupError",
                            "error_code": "runtime.setup-failed",
                            "message": detail,
                            "evidence_source": "observed-tool-result",
                        }
                    ],
                )
            LOGGER.error("Lucia managed Python runtime setup failed: %s", detail)
            return 1
        # The actual selector resolves this relative path under the selected component,
        # not this top-level Lucia checker (covered by a real-process regression).
        runtime_args = ["--module", args.module]
        for requirement in args.requirements or []:
            runtime_args.extend(["--requirements", requirement])
        return run_verified_helper(
            execution_root,
            [*runtime_args, "run", "scripts/check_dependencies.py", *delegated_args],
            args.module,
        )
    finally:
        release_execution_root(execution_root)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    raise SystemExit(main())

SHA-256: da530d90dd0253f10239b18efa2c2c0ddad5926a7b13c20c57491f43243c4b58