#!/usr/bin/env python3 """Build a deterministic frontend app-pack source repository from stack.lock.json.""" from __future__ import annotations import argparse import json import os import re import shutil import stat import subprocess import sys from pathlib import Path PACK_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") FIXED_GIT_ENV = { "GIT_AUTHOR_NAME": "NexusCRM Stack", "GIT_AUTHOR_EMAIL": "nexuscrm-stack@example.invalid", "GIT_AUTHOR_DATE": "2000-01-01T00:00:00+00:00", "GIT_COMMITTER_NAME": "NexusCRM Stack", "GIT_COMMITTER_EMAIL": "nexuscrm-stack@example.invalid", "GIT_COMMITTER_DATE": "2000-01-01T00:00:00+00:00", } class MaterializeError(RuntimeError): pass def _is_relative_to(path: Path, parent: Path) -> bool: try: path.relative_to(parent) return True except ValueError: return False def _load_json_object(path: Path, label: str) -> dict[str, object]: if path.is_symlink(): raise MaterializeError(f"{label} must not be a symlink: {path}") try: if not stat.S_ISREG(path.lstat().st_mode): raise MaterializeError(f"{label} must be a regular file: {path}") payload = json.loads(path.read_text()) except FileNotFoundError as exc: raise MaterializeError(f"{label} not found: {path}") from exc except json.JSONDecodeError as exc: raise MaterializeError(f"{label} is not valid JSON: {path}: {exc}") from exc if not isinstance(payload, dict): raise MaterializeError(f"{label} must be a JSON object: {path}") return payload def _require_canonical_directory(path: Path, owner_root: Path, label: str) -> Path: raw = path.absolute() try: resolved = path.resolve(strict=True) except FileNotFoundError as exc: raise MaterializeError(f"{label} not found: {path}") from exc if raw != resolved or not _is_relative_to(resolved, owner_root): raise MaterializeError(f"{label} must be a canonical directory under its source root") if not stat.S_ISDIR(path.lstat().st_mode): raise MaterializeError(f"{label} must be a directory: {path}") return resolved def _require_regular_tree(root: Path, label: str) -> None: _require_canonical_directory(root, root, label) for directory, directory_names, file_names in os.walk(root, followlinks=False): parent = Path(directory) for name in directory_names: path = parent / name if not stat.S_ISDIR(path.lstat().st_mode): raise MaterializeError(f"{label} contains a symlink or special entry: {path}") for name in file_names: path = parent / name if not stat.S_ISREG(path.lstat().st_mode): raise MaterializeError(f"{label} contains a symlink or special entry: {path}") def _safe_pack_name(value: object, label: str) -> str: name = str(value or "").strip() if not PACK_NAME_RE.fullmatch(name) or name in {".", ".."}: raise MaterializeError(f"unsafe {label}: {name!r}") return name def _resolve_local_pack(downstream_root: Path, entry: str) -> tuple[str, Path]: relative = Path(entry) if relative.is_absolute() or ".." in relative.parts: raise MaterializeError(f"unsafe local pack path: {entry!r}") pack_dir = _require_canonical_directory( downstream_root / relative, downstream_root, f"local pack {entry!r}", ) manifest = _load_json_object(pack_dir / "pack.json", "pack manifest") return _safe_pack_name(manifest.get("name"), f"pack name for {entry}"), pack_dir def _resolve_pack( downstream_root: Path, official_app_packs_root: Path, entry: str, ) -> tuple[str, Path]: if entry.startswith("official_repo:"): name = _safe_pack_name( entry.removeprefix("official_repo:"), "official pack name" ) pack_dir = _require_canonical_directory( official_app_packs_root / name, official_app_packs_root, f"official pack {name}", ) return name, pack_dir return _resolve_local_pack(downstream_root, entry) def _verify_shadowed_official_pack_lineage( *, pack_name: str, pack_dir: Path, official_app_packs_root: Path, official_ref: str, ) -> None: """Reject a generated downstream pack built from another official source. A local final pack may intentionally overlay an official pack with the same name. Its frontend is then the source consumed by the host build, so the final-pack lineage must bind it to the exact official checkout selected by the frontend BOM. Checking only the two Git checkouts lets a stale generated frontend silently win over the newly selected official source. """ if not (official_app_packs_root / pack_name).is_dir(): return lineage_path = pack_dir / ".build-lineage.json" if not lineage_path.is_file(): raise MaterializeError( f"local pack {pack_name} shadows an official pack without build lineage" ) lineage = _load_json_object(lineage_path, f"{pack_name} build lineage") official_pack = lineage.get("officialPack") if not isinstance(official_pack, dict): raise MaterializeError( f"local pack {pack_name} build lineage has no officialPack authority" ) lineage_ref = str(official_pack.get("ref") or "").strip() if lineage_ref != official_ref: raise MaterializeError( f"local pack {pack_name} was built from official app-packs {lineage_ref or ''}, " f"frontend BOM selects {official_ref}" ) def _assert_safe_target(target_root: Path, input_roots: tuple[Path, ...]) -> None: if target_root == Path(target_root.anchor): raise MaterializeError(f"refusing unsafe target root: {target_root}") if target_root.is_symlink(): raise MaterializeError(f"target root must not be a symlink: {target_root}") for input_root in input_roots: if target_root == input_root or _is_relative_to(input_root, target_root): raise MaterializeError( f"target root must not contain an input root: target={target_root} input={input_root}" ) def _run_git(target_root: Path, *args: str, capture: bool = False) -> str: env = os.environ.copy() env.update(FIXED_GIT_ENV) result = subprocess.run( ["git", "-C", str(target_root), *args], env=env, text=True, capture_output=True, check=False, ) if result.returncode != 0: raise MaterializeError( f"git {' '.join(args)} failed ({result.returncode}): {result.stderr.strip()}" ) return result.stdout.strip() if capture else "" def materialize_frontend_app_packs_source( *, downstream_root: Path, official_app_packs_root: Path, target_root: Path, ) -> dict[str, str]: downstream_root = downstream_root.expanduser().resolve() official_app_packs_root = official_app_packs_root.expanduser().resolve() target_root = target_root.expanduser() if target_root.is_symlink(): raise MaterializeError(f"target root must not be a symlink: {target_root}") target_root = target_root.resolve(strict=False) if not downstream_root.is_dir(): raise MaterializeError(f"downstream root not found: {downstream_root}") if not official_app_packs_root.is_dir(): raise MaterializeError( f"official app-packs root not found: {official_app_packs_root}" ) _assert_safe_target(target_root, (downstream_root, official_app_packs_root)) stack_lock = _load_json_object(downstream_root / "stack.lock.json", "stack lock") pack_entries = stack_lock.get("packs") if not isinstance(pack_entries, list) or not all( isinstance(entry, str) for entry in pack_entries ): raise MaterializeError("stack lock packs must be an array of strings") resolved_packs: list[tuple[str, Path]] = [] seen_names: set[str] = set() for entry in pack_entries: pack_name, pack_dir = _resolve_pack( downstream_root, official_app_packs_root, entry ) if not entry.startswith("official_repo:"): if (official_app_packs_root / pack_name).is_dir(): _verify_shadowed_official_pack_lineage( pack_name=pack_name, pack_dir=pack_dir, official_app_packs_root=official_app_packs_root, official_ref=_run_git( official_app_packs_root, "rev-parse", "HEAD", capture=True ), ) source_frontend = pack_dir / "frontend" if not (source_frontend / "source-manifest.json").is_file(): continue _require_regular_tree(source_frontend, f"frontend source for {pack_name}") if pack_name in seen_names: raise MaterializeError(f"duplicate frontend pack name: {pack_name}") if _is_relative_to(target_root, pack_dir): raise MaterializeError( f"target root must not be inside a source pack: target={target_root} pack={pack_dir}" ) seen_names.add(pack_name) resolved_packs.append((pack_name, source_frontend)) if not resolved_packs: raise MaterializeError( "no frontend app-package manifests found in configured packs" ) if target_root.exists(): shutil.rmtree(target_root) (target_root / "frontend").mkdir(parents=True) source_index_entries: list[dict[str, str]] = [] for pack_name, source_frontend in resolved_packs: target_frontend = target_root / pack_name / "frontend" shutil.copytree(source_frontend, target_frontend) source_report_types = source_frontend.parent / "report_types" if source_report_types.is_dir(): _require_regular_tree( source_report_types, f"report type source for {pack_name}", ) shutil.copytree( source_report_types, target_root / pack_name / "report_types", ) for generated_name in ("node_modules", "dist", ".vite"): generated_path = target_frontend / generated_name if generated_path.is_dir(): shutil.rmtree(generated_path) elif generated_path.exists() or generated_path.is_symlink(): generated_path.unlink() source_index_entries.append( { "pack": pack_name, "manifest": f"{pack_name}/frontend/source-manifest.json", } ) source_index = { "schemaVersion": "nexuscrm.appPacksFrontendSourceIndex.v1", "owner": "nexuscrm-app-packs", "appFrontendManifests": source_index_entries, } (target_root / "frontend" / "source-index.json").write_text( json.dumps(source_index, indent=2) + "\n" ) _run_git(target_root, "init", "-q", "--object-format=sha1") _run_git(target_root, "add", "--force", "--all") _run_git( target_root, "-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", "commit", "-q", "-m", "Materialize runtime frontend app packages", ) commit = _run_git(target_root, "rev-parse", "HEAD", capture=True) return {"root": str(target_root), "ref": commit} def parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--downstream-root", type=Path, required=True) parser.add_argument("--official-app-packs-root", type=Path, required=True) parser.add_argument("--target-root", type=Path, required=True) return parser.parse_args(argv) def main(argv: list[str] | None = None) -> int: args = parse_args(argv or sys.argv[1:]) try: result = materialize_frontend_app_packs_source( downstream_root=args.downstream_root, official_app_packs_root=args.official_app_packs_root, target_root=args.target_root, ) except MaterializeError as exc: print( f"frontend app-pack source materialization failed: {exc}", file=sys.stderr ) return 1 print(json.dumps(result, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())