#!/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 os import sys import subprocess import argparse import shutil import re # Extensions supported by bild (from Omni/Bild.hs and Omni/Namespace.hs) EXTENSIONS = {'.c', '.hs', '.lisp', '.nix', '.py', '.scm', '.rs', '.toml'} def strip_ansi(text): """Strip ANSI escape codes from text.""" ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') return ansi_escape.sub('', text) def is_ignored(path): """Check if a file is ignored by git.""" res = subprocess.run( ['git', 'check-ignore', path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) return res.returncode == 0 def get_buildable_files(root_dir='.'): """Find all files that bild can build.""" targets = [] for root, dirs, files in os.walk(root_dir): # Skip hidden directories, build artifacts (_), and task db (.tasks) dirs[:] = [d for d in dirs if not d.startswith('.') and d != '_'] for f in files: ext = os.path.splitext(f)[1] if ext in EXTENSIONS: path = os.path.join(root, f) # Clean up path if path.startswith('./'): path = path[2:] if not is_ignored(path): targets.append(path) return targets def run_bild(target): """Run bild on the target.""" # --time 0 disables timeout # --loud enables output (which we capture) cmd = ['bild', '--time', '0', '--loud', target] result = subprocess.run(cmd, capture_output=True, text=True) return result def create_task(target, result, parent_id=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) > 50: last_line = last_line[:47] + "..." 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 parts = target.split('/') if len(parts) > 1: # e.g. Omni/Bild.hs -> Omni/Bild # Omni/Bild/Audit.py -> Omni/Bild ns = os.path.dirname(target) if ns: cmd.append(f'--namespace={ns}') print(f"Creating task for {target}...") proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode != 0: print(f"Error creating task: {proc.stderr}", file=sys.stderr) else: # task create --json returns the created task json print(f"Task created: {proc.stdout.strip()}") def main(): 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("Warning: 'bild' command not found. Ensure it is in PATH.", file=sys.stderr) print(f"Scanning for targets in {args.paths}...") targets = [] for path in args.paths: if os.path.isfile(path): targets.append(path) else: targets.extend(get_buildable_files(path)) # Remove duplicates targets = sorted(list(set(targets))) print(f"Found {len(targets)} targets.") failures = 0 for i, target in enumerate(targets): print(f"[{i+1}/{len(targets)}] Building {target} ... ", end='', flush=True) res = run_bild(target) if res.returncode == 0: print("OK") else: print("FAIL") failures += 1 create_task(target, res, args.parent) print(f"\nAudit complete. {failures} failures found.") if failures > 0: sys.exit(1) else: sys.exit(0) if __name__ == '__main__': main()