File indexing completed on 2026-09-05 08:17:13
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017 """
0018 Run every code generator listed in codegen/manifest.json and write the results
0019 into a directory laid out the way cmake/ActsCodegen.cmake looks them up, i.e.
0020 <output>/<key> for each key in the manifest.
0021
0022 Pointing ACTS_CODEGEN_PREBUILT_DIR at the result -- or shipping it as
0023 `prebuilt-codegen/` next to the top-level CMakeLists.txt, which is what the
0024 release source archive does -- lets a build skip the generators, and with them
0025 the uv, Python and package downloads they need.
0026
0027 This needs uv and nothing else: no CMake, no compiler, and none of the C++
0028 dependencies a real ACTS configure requires.
0029 """
0030
0031 import concurrent.futures
0032 import json
0033 import os
0034 import shutil
0035 import subprocess
0036 import sys
0037 from pathlib import Path
0038 from typing import Annotated
0039
0040 import typer
0041
0042 app = typer.Typer(add_completion=False)
0043
0044 SOURCE_ROOT = Path(__file__).resolve().parent.parent
0045 MANIFEST = SOURCE_ROOT / "codegen" / "manifest.json"
0046
0047
0048 def generate(key: str, unit: dict, output_dir: Path, uv: str) -> tuple[str, str]:
0049 """Run one generator and return (key, error message or empty string)."""
0050
0051 destination = output_dir / key
0052 destination.parent.mkdir(parents=True, exist_ok=True)
0053
0054 command = [
0055 uv,
0056 "run",
0057 "--quiet",
0058 "--python",
0059 unit["python_version"],
0060 "--no-project",
0061 ]
0062 if unit["isolated"]:
0063 command.append("--isolated")
0064 for requirement in unit["with_requirements"]:
0065 command += ["--with-requirements", str(SOURCE_ROOT / requirement)]
0066 for package in unit["with"]:
0067 command += ["--with", str(SOURCE_ROOT / package)]
0068 command += [str(SOURCE_ROOT / unit["script"]), str(destination)]
0069
0070
0071
0072
0073
0074
0075 result = subprocess.run(
0076 command,
0077 capture_output=True,
0078 text=True,
0079 )
0080 if result.returncode != 0:
0081 return key, result.stderr.strip() or f"exited with {result.returncode}"
0082 if not destination.exists():
0083 return key, "generator succeeded but wrote no output"
0084 return key, ""
0085
0086
0087 @app.command()
0088 def main(
0089 output: Annotated[
0090 Path,
0091 typer.Option("--output", "-o", help="Directory to write generated code into"),
0092 ] = Path("prebuilt-codegen"),
0093 jobs: Annotated[
0094 int,
0095 typer.Option("--jobs", "-j", help="Number of generators to run in parallel"),
0096 ] = 0,
0097 clean: Annotated[
0098 bool,
0099 typer.Option("--clean/--no-clean", help="Remove the output directory first"),
0100 ] = True,
0101 ) -> None:
0102 uv = shutil.which("uv")
0103 if uv is None:
0104 typer.echo("error: uv not found on PATH", err=True)
0105 raise typer.Exit(1)
0106
0107 units = json.loads(MANIFEST.read_text())["units"]
0108
0109 if clean and output.exists():
0110 shutil.rmtree(output)
0111 output.mkdir(parents=True, exist_ok=True)
0112
0113 typer.echo(f"Generating {len(units)} units into {output}")
0114
0115 failures = []
0116 with concurrent.futures.ThreadPoolExecutor(
0117 max_workers=jobs or min(len(units), (os.cpu_count() or 1))
0118 ) as pool:
0119 futures = [
0120 pool.submit(generate, key, unit, output, uv) for key, unit in units.items()
0121 ]
0122 for future in concurrent.futures.as_completed(futures):
0123 key, error = future.result()
0124 if error:
0125 failures.append((key, error))
0126 typer.echo(f" FAILED {key}", err=True)
0127 else:
0128 typer.echo(f" ok {key}")
0129
0130 if failures:
0131 typer.echo("", err=True)
0132 for key, error in failures:
0133 typer.echo(f"{key}:\n{error}\n", err=True)
0134 raise typer.Exit(1)
0135
0136 typer.echo(f"Wrote {len(units)} files to {output}")
0137
0138
0139 if __name__ == "__main__":
0140 sys.exit(app())