Blacksite/scripts/editor/build_progress.py

201 lines
5.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Build the Blacksite editor while publishing exact crate progress."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import subprocess
from typing import Iterable
def write_status(
path: Path,
phase: str,
detail: str,
progress: int,
state: str = "running",
crate_count: str = "",
) -> None:
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(
f"{phase}\n{detail}\n{progress}\n{state}\n{crate_count}\n",
encoding="utf-8",
)
os.replace(temporary, path)
def unique_crates(tree_output: str) -> set[str]:
return {line.strip() for line in tree_output.splitlines() if line.strip()}
def completed_crate(
message: object, completed_package_ids: set[str]
) -> tuple[bool, str]:
if not isinstance(message, dict) or message.get("reason") != "compiler-artifact":
return False, ""
package_id = message.get("package_id")
if not isinstance(package_id, str) or package_id in completed_package_ids:
return False, ""
completed_package_ids.add(package_id)
target = message.get("target")
if isinstance(target, dict) and isinstance(target.get("name"), str):
return True, target["name"]
return True, package_id.rsplit("#", 1)[-1]
def cargo_tree(cargo: str) -> list[str]:
return [
cargo,
"tree",
"-p",
"editor",
"--features",
"dev",
"--edges",
"normal,build",
"--prefix",
"none",
"--format",
"{p}",
"--no-dedupe",
"--color",
"never",
]
def cargo_build(cargo: str) -> list[str]:
return [
cargo,
"build",
"-p",
"editor",
"--bins",
"--features",
"dev",
"--message-format=json-render-diagnostics",
"--color",
"never",
]
def rendered_lines(message: object, raw_line: str) -> Iterable[str]:
if not isinstance(message, dict):
yield raw_line
return
if message.get("reason") == "compiler-message":
diagnostic = message.get("message")
if isinstance(diagnostic, dict):
rendered = diagnostic.get("rendered")
if isinstance(rendered, str) and rendered:
yield rendered.rstrip("\n")
def run(status_path: Path, log_path: Path, cargo: str) -> int:
write_status(
status_path,
"Preparing build",
"Resolving editor crate graph",
8,
)
tree = subprocess.run(
cargo_tree(cargo),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
crates = unique_crates(tree.stdout) if tree.returncode == 0 else set()
if not crates:
with log_path.open("a", encoding="utf-8") as log:
log.write(tree.stdout)
log.write(tree.stderr)
write_status(
status_path,
"Build graph failed",
"Open the launch log for Cargo details",
8,
"error",
)
return tree.returncode or 1
total = len(crates)
write_status(
status_path,
"Compiling project",
"Preparing editor crates",
12,
crate_count=f"0/{total}",
)
process = subprocess.Popen(
cargo_build(cargo),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
)
assert process.stdout is not None
completed_package_ids: set[str] = set()
with log_path.open("a", encoding="utf-8") as log:
for raw_line in process.stdout:
stripped = raw_line.rstrip("\n")
try:
message: object = json.loads(stripped)
except json.JSONDecodeError:
message = stripped
for rendered in rendered_lines(message, stripped):
log.write(rendered + "\n")
log.flush()
changed, crate_name = completed_crate(message, completed_package_ids)
if not changed:
continue
completed = min(len(completed_package_ids), total)
progress = min(78, 12 + (66 * completed // total))
write_status(
status_path,
"Compiling project",
f"{crate_name} ready",
progress,
crate_count=f"{completed}/{total}",
)
return_code = process.wait()
completed = min(len(completed_package_ids), total)
if return_code != 0:
write_status(
status_path,
"Build failed",
"Open the launch log for compiler details",
max(12, min(78, 12 + (66 * completed // total))),
"error",
f"{completed}/{total}",
)
return return_code
write_status(
status_path,
"Build complete",
"Preparing engine process",
78,
crate_count=f"{total}/{total}",
)
return 0
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("status_path", type=Path)
parser.add_argument("log_path", type=Path)
parser.add_argument("--cargo", default="/usr/bin/cargo")
arguments = parser.parse_args()
return run(arguments.status_path, arguments.log_path, arguments.cargo)
if __name__ == "__main__":
raise SystemExit(main())