128 lines
5.0 KiB
Python
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()
|