#!/usr/bin/env python3 """ Audit codebase builds. Iterates through every namespace in the project and runs 'bild'. For every build failure encountered, it automatically creates a new task. """ # : out bild-audit import argparse import re import shutil import subprocess import sys from pathlib import Path # Extensions supported by bild (from Omni/Bild.hs and Omni/Namespace.hs) EXTENSIONS = {".c", ".hs", ".lisp", ".nix", ".py", ".scm", ".rs", ".toml"} MAX_TITLE_LENGTH = 50 def strip_ansi(text: str) -> str: """Strip ANSI escape codes from text.""" ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") return ansi_escape.sub("", text) def is_ignored(path: Path) -> bool: """Check if a file is ignored by git.""" res = subprocess.run( ["git", "check-ignore", str(path)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, ) return res.returncode == 0 def get_buildable_files(root_dir: str = ".") -> list[str]: """Find all files that bild can build.""" targets: list[str] = [] root = Path(root_dir) if not root.exists(): return [] for path in root.rglob("*"): # Skip directories if path.is_dir(): continue # Skip hidden files/dirs and '_' dirs parts = path.parts if any(p.startswith(".") or p == "_" for p in parts): continue if path.suffix in EXTENSIONS: # Clean up path: keep it relative to cwd if possible try: # We want the path as a string, relative to current directory # if possible p_str = ( str(path.relative_to(Path.cwd())) if path.is_absolute() else str(path) ) except ValueError: p_str = str(path) if not is_ignored(Path(p_str)): targets.append(p_str) return targets def run_bild(target: str) -> subprocess.CompletedProcess[str]: """Run bild on the target.""" # --time 0 disables timeout # --loud enables output (which we capture) cmd = ["bild", "--time", "0", "--loud", target] return subprocess.run(cmd, capture_output=True, text=True, check=False) def create_task( target: str, result: subprocess.CompletedProcess[str], parent_id: str | None = None, ) -> None: """Create a task for a build failure.""" # Construct a descriptive title # Try to get the last meaningful line of error output lines = (result.stdout + result.stderr).strip().split("\n") last_line = lines[-1] if lines else "Unknown error" last_line = strip_ansi(last_line).strip() if len(last_line) > MAX_TITLE_LENGTH: last_line = last_line[: MAX_TITLE_LENGTH - 3] + "..." title = f"Build failed: {target} - {last_line}" cmd = ["task", "create", title, "--priority", "2", "--json"] if parent_id: cmd.append(f"--discovered-from={parent_id}") # Try to infer namespace # e.g. Omni/Bild.hs -> Omni/Bild ns = Path(target).parent if str(ns) != ".": cmd.append(f"--namespace={ns}") print(f"Creating task for {target}...") # noqa: T201 proc = subprocess.run(cmd, capture_output=True, text=True, check=False) if proc.returncode != 0: print(f"Error creating task: {proc.stderr}", file=sys.stderr) # noqa: T201 else: # task create --json returns the created task json print(f"Task created: {proc.stdout.strip()}") # noqa: T201 def main() -> None: """Run the build audit.""" parser = argparse.ArgumentParser(description="Audit codebase builds.") parser.add_argument( "--parent", help="Parent task ID to link discovered tasks to", ) parser.add_argument( "paths", nargs="*", default=["."], help="Paths to search for targets", ) args = parser.parse_args() # Check if bild is available if not shutil.which("bild"): print( # noqa: T201 "Warning: 'bild' command not found. Ensure it is in PATH.", file=sys.stderr, ) print(f"Scanning for targets in {args.paths}...") # noqa: T201 targets: list[str] = [] for path_str in args.paths: path = Path(path_str) if path.is_file(): targets.append(str(path)) else: targets.extend(get_buildable_files(path_str)) # Remove duplicates targets = sorted(set(targets)) print(f"Found {len(targets)} targets.") # noqa: T201 failures = 0 for target in targets: res = run_bild(target) if res.returncode == 0: print("OK") # noqa: T201 else: print("FAIL") # noqa: T201 failures += 1 create_task(target, res, args.parent) print(f"\nAudit complete. {failures} failures found.") # noqa: T201 if failures > 0: sys.exit(1) else: sys.exit(0) if __name__ == "__main__": main()