Files
MetaCrate/tools/extract_public_api.py

128 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Build pinned LibreMetaverse metadata twice and emit a deterministic API catalog."""
from __future__ import annotations
import argparse
import filecmp
import shutil
import subprocess
import tempfile
from pathlib import Path
UPSTREAM_COMMIT = "2aa70bb68513b39795da5d13c88f31b86e85a3ba"
TARGET_FRAMEWORK = "net10.0"
PROJECTS = (
"LibreMetaverse.Types/LibreMetaverse.Types.csproj",
"LibreMetaverse.StructuredData/LibreMetaverse.StructuredData.csproj",
"LibreMetaverse.Imaging.Abstractions/LibreMetaverse.Imaging.Abstractions.csproj",
"LibreMetaverse.Imaging.Skia/LibreMetaverse.Imaging.Skia.csproj",
"PrimMesher/LibreMetaverse.PrimMesher.csproj",
"LibreMetaverse/LibreMetaverse.csproj",
"LibreMetaverse.Rendering.Simple/LibreMetaverse.Rendering.Simple.csproj",
"LibreMetaverse.Rendering.MeshFoundry/LibreMetaverse.Rendering.MeshFoundry.csproj",
"LibreMetaverse.LslTools/LibreMetaverse.LslTools.csproj",
"LibreMetaverse.RLV/LibreMetaverse.RLV.csproj",
"LibreMetaverse.Utilities/LibreMetaverse.Utilities.csproj",
"LibreMetaverse.Voice.Vivox/LibreMetaverse.Voice.Vivox.csproj",
"LibreMetaverse.Voice.WebRTC/LibreMetaverse.Voice.WebRTC.csproj",
)
ASSEMBLIES = tuple(Path(project).stem for project in PROJECTS)
def run(*command: str, cwd: Path | None = None) -> None:
subprocess.run(command, cwd=cwd, check=True)
def output(command: list[str], cwd: Path) -> str:
return subprocess.run(command, cwd=cwd, check=True, text=True, stdout=subprocess.PIPE).stdout.strip()
def validate_upstream(upstream: Path) -> None:
actual = output(["git", "rev-parse", "HEAD"], upstream)
if actual != UPSTREAM_COMMIT:
raise RuntimeError(f"expected upstream {UPSTREAM_COMMIT}, found {actual}")
status = output(["git", "status", "--porcelain"], upstream)
if status:
raise RuntimeError("the pinned upstream checkout must be clean")
def build_tool(root: Path, temporary: Path) -> Path:
artifacts = temporary / "tool-artifacts"
run(
"dotnet", "build", str(root / "tools/api-catalog/ApiCatalog.csproj"),
"-c", "Release", "--artifacts-path", str(artifacts),
)
candidates = sorted((artifacts / "bin").rglob("ApiCatalog.dll"))
if len(candidates) != 1:
raise RuntimeError(f"expected one extractor assembly, found {candidates}")
run("dotnet", str(candidates[0]), "--self-test")
return candidates[0]
def build_upstream(upstream: Path, temporary: Path) -> tuple[list[Path], Path]:
artifacts = temporary / "upstream-artifacts"
binaries = temporary / "bin"
generated = temporary / "generated"
for project in PROJECTS:
command = [
"dotnet", "build", str(upstream / project), "-c", "Release", "-f", TARGET_FRAMEWORK,
"--artifacts-path", str(artifacts), "-p:ContinuousIntegrationBuild=true",
f"-p:OutputPath={binaries}/", "-p:CopyLocalLockFileAssemblies=true",
]
if project == "LibreMetaverse/LibreMetaverse.csproj":
command.extend([
"--no-incremental", "-p:EmitCompilerGeneratedFiles=true",
f"-p:CompilerGeneratedFilesOutputPath={generated}",
])
run(*command)
paths = []
for name in ASSEMBLIES:
path = binaries / f"{name}.dll"
if not path.is_file():
raise RuntimeError(f"missing built assembly {path}")
paths.append(path)
return paths, generated
def extract(tool: Path, assemblies: list[Path], generated: Path, destination: Path) -> None:
run(
"dotnet", str(tool), "--output", str(destination),
"--upstream-commit", UPSTREAM_COMMIT,
"--target-framework", TARGET_FRAMEWORK,
"--generated-root", str(generated),
*(str(path) for path in assemblies),
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--upstream", type=Path, default=Path("../libremetaverse"))
parser.add_argument("--output", type=Path, default=Path("api/public-api.json"))
args = parser.parse_args()
root = Path(__file__).resolve().parent.parent
upstream = args.upstream.resolve()
destination = args.output.resolve()
validate_upstream(upstream)
with tempfile.TemporaryDirectory(prefix="metacrate-api-catalog-") as directory:
temporary = Path(directory)
tool = build_tool(root, temporary)
assemblies, generated = build_upstream(upstream, temporary / "upstream")
outputs = []
for pass_number in (1, 2):
catalog = temporary / f"public-api-{pass_number}.json"
extract(tool, assemblies, generated, catalog)
outputs.append(catalog)
if not filecmp.cmp(outputs[0], outputs[1], shallow=False):
raise RuntimeError("two clean extraction passes produced different bytes")
validate_upstream(upstream)
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(outputs[0], destination)
print(f"wrote deterministic API catalog to {destination}")
if __name__ == "__main__":
main()