Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-05 08:17:13

0001 #!/usr/bin/env python3
0002 # /// script
0003 # requires-python = ">=3.11"
0004 # dependencies = [
0005 #     "typer",
0006 # ]
0007 # ///
0008 
0009 # This file is part of the ACTS project.
0010 #
0011 # Copyright (C) 2016 CERN for the benefit of the ACTS project
0012 #
0013 # This Source Code Form is subject to the terms of the Mozilla Public
0014 # License, v. 2.0. If a copy of the MPL was not distributed with this
0015 # file, You can obtain one at https://mozilla.org/MPL/2.0/.
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     # The CMake path scrubs the environment before invoking uv, because a
0071     # configure can happen inside an LCG/Spack shell whose PYTHONPATH,
0072     # VIRTUAL_ENV etc. would otherwise leak into the generator. This script only
0073     # ever runs in CI, which starts from a clean environment to begin with, so
0074     # there is nothing to guard against here: just inherit it.
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())