#!/usr/bin/env python3 """Validate that all source trees required by a build config exist.""" import argparse import sys import tomllib from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] META_PACKAGES = {"libgcc", "libstdcxx"} def build_lookup(): lookup = {} for root in (PROJECT_ROOT / "recipes", PROJECT_ROOT / "local/recipes"): for recipe_toml in root.rglob("recipe.toml"): parts = recipe_toml.parts if "source" in parts or "target" in parts: continue package_name = recipe_toml.parent.name if package_name not in lookup: lookup[package_name] = recipe_toml.parent return lookup def resolve_config(config_path: Path, visited=None): if visited is None: visited = set() config_path = config_path.resolve() if config_path in visited: return {} visited.add(config_path) with open(config_path, "rb") as config_file: config = tomllib.load(config_file) packages = dict(config.get("packages", {})) for include in config.get("include", []): include_path = config_path.parent / include if include_path.exists(): included_packages = resolve_config(include_path, visited) for package_name, package_value in packages.items(): included_packages[package_name] = package_value packages = included_packages return packages def recipe_restore_path(recipe_dir: Path): recipes_root = PROJECT_ROOT / "recipes" try: return recipe_dir.relative_to(recipes_root).as_posix() except ValueError: return None def same_as_source_dir(recipe_dir: Path): recipe_file = recipe_dir / "recipe.toml" if not recipe_file.exists(): return None with open(recipe_file, "rb") as handle: recipe = tomllib.load(handle) source = recipe.get("source") if not isinstance(source, dict): return None same_as = source.get("same_as") if not isinstance(same_as, str) or not same_as: return None return (recipe_dir / same_as).resolve() / "source" def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("config", nargs="?", default="redbear-full") parser.add_argument("--extra-package", action="append", default=[]) parser.add_argument("--missing-paths-only", action="store_true") parser.add_argument("--release", default="") return parser.parse_args() def main(): args = parse_args() config_path = PROJECT_ROOT / "config" / f"{args.config}.toml" if not config_path.exists(): print(f"Config not found: {config_path}", file=sys.stderr) return 1 lookup = build_lookup() packages = resolve_config(config_path) requested_packages = dict(packages) for package_name in args.extra_package: requested_packages.setdefault(package_name, {}) missing_recipe_paths = [] missing_entries = [] present = 0 for package_name, package_conf in sorted(requested_packages.items()): if str(package_conf) == "ignore" or package_name in META_PACKAGES: continue recipe_dir = lookup.get(package_name) if recipe_dir is None: missing_entries.append((package_name, None)) continue source_dir = recipe_dir / "source" if source_dir.is_dir() and any(source_dir.iterdir()): present += 1 continue alias_source_dir = same_as_source_dir(recipe_dir) if alias_source_dir is not None and alias_source_dir.is_dir() and any(alias_source_dir.iterdir()): present += 1 continue missing_entries.append((package_name, recipe_dir)) restore_path = recipe_restore_path(recipe_dir) if restore_path is not None: missing_recipe_paths.append(restore_path) if args.missing_paths_only: seen = set() for recipe_path in missing_recipe_paths: if recipe_path not in seen: print(recipe_path) seen.add(recipe_path) return 1 if missing_entries else 0 print(f"=== Validating source trees for config: {args.config} ===") for package_name, recipe_dir in missing_entries: if recipe_dir is None: print(f" NOT FOUND: {package_name}") else: print(f" MISSING: {recipe_dir.relative_to(PROJECT_ROOT)}") total = present + len(missing_entries) print(f"\n Total (config): {total}") print(f" Present: {present}") print(f" Missing: {len(missing_entries)}") if missing_entries: release = args.release or "" print(f"\nTo restore: ./local/scripts/restore-sources.sh --release={release}") return 1 print("All source trees present.") return 0 if __name__ == "__main__": sys.exit(main())