Add authoritative public API catalog
This commit is contained in:
127
tools/extract_public_api.py
Normal file
127
tools/extract_public_api.py
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user