289 lines
8.2 KiB
Python
Executable File
289 lines
8.2 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
|
|
import sys
|
|
from typing import Iterable
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
DEFAULT_CARGO_LANE = ROOT / "scripts/codex/cargo_lane.py"
|
|
|
|
|
|
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 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 managed_cargo(cargo_lane: Path, lane: str, *arguments: str) -> list[str]:
|
|
return [
|
|
sys.executable,
|
|
str(cargo_lane),
|
|
"exec",
|
|
lane,
|
|
"--",
|
|
"cargo",
|
|
*arguments,
|
|
]
|
|
|
|
|
|
def host_target() -> str:
|
|
completed = subprocess.run(
|
|
["rustc", "-vV"],
|
|
cwd=ROOT,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
check=False,
|
|
)
|
|
for line in completed.stdout.splitlines():
|
|
if line.startswith("host:"):
|
|
return line.partition(":")[2].strip()
|
|
return ""
|
|
|
|
|
|
def cargo_metadata(cargo_lane: Path, lane: str, target: str) -> list[str]:
|
|
arguments = ["metadata", "--format-version", "1"]
|
|
if target:
|
|
arguments.extend(["--filter-platform", target])
|
|
return managed_cargo(
|
|
cargo_lane,
|
|
lane,
|
|
*arguments,
|
|
)
|
|
|
|
|
|
def cargo_build(cargo_lane: Path, lane: str) -> list[str]:
|
|
return managed_cargo(
|
|
cargo_lane,
|
|
lane,
|
|
"build",
|
|
"-p",
|
|
"editor",
|
|
"--bins",
|
|
"--message-format=json-render-diagnostics",
|
|
"--color",
|
|
"never",
|
|
)
|
|
|
|
|
|
def editor_package_closure(metadata_output: str) -> set[str]:
|
|
document = json.loads(metadata_output)
|
|
packages = document.get("packages")
|
|
resolve = document.get("resolve")
|
|
if not isinstance(packages, list) or not isinstance(resolve, dict):
|
|
raise ValueError("Cargo metadata did not include packages and a resolve graph")
|
|
|
|
editor_ids = {
|
|
package.get("id")
|
|
for package in packages
|
|
if isinstance(package, dict)
|
|
and package.get("name") == "editor"
|
|
and isinstance(package.get("id"), str)
|
|
}
|
|
if len(editor_ids) != 1:
|
|
raise ValueError("Cargo metadata did not identify exactly one editor package")
|
|
root_id = next(iter(editor_ids))
|
|
|
|
nodes = resolve.get("nodes")
|
|
if not isinstance(nodes, list):
|
|
raise ValueError("Cargo metadata resolve graph did not include nodes")
|
|
nodes_by_id = {
|
|
node["id"]: node
|
|
for node in nodes
|
|
if isinstance(node, dict) and isinstance(node.get("id"), str)
|
|
}
|
|
|
|
closure: set[str] = set()
|
|
pending = [root_id]
|
|
while pending:
|
|
package_id = pending.pop()
|
|
if package_id in closure:
|
|
continue
|
|
closure.add(package_id)
|
|
node = nodes_by_id.get(package_id)
|
|
if not isinstance(node, dict):
|
|
continue
|
|
dependencies = node.get("deps")
|
|
if not isinstance(dependencies, list):
|
|
continue
|
|
for dependency in dependencies:
|
|
if not isinstance(dependency, dict):
|
|
continue
|
|
dependency_id = dependency.get("pkg")
|
|
kinds = dependency.get("dep_kinds")
|
|
if not isinstance(dependency_id, str) or not isinstance(kinds, list):
|
|
continue
|
|
if any(
|
|
isinstance(kind, dict) and kind.get("kind") in {None, "build"}
|
|
for kind in kinds
|
|
):
|
|
pending.append(dependency_id)
|
|
return closure
|
|
|
|
|
|
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_lane: Path, lane: str) -> int:
|
|
write_status(
|
|
status_path,
|
|
"Preparing build",
|
|
"Resolving editor crate graph",
|
|
8,
|
|
)
|
|
metadata = subprocess.run(
|
|
cargo_metadata(cargo_lane, lane, host_target()),
|
|
cwd=ROOT,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
check=False,
|
|
)
|
|
try:
|
|
crates = (
|
|
editor_package_closure(metadata.stdout)
|
|
if metadata.returncode == 0
|
|
else set()
|
|
)
|
|
except (json.JSONDecodeError, ValueError):
|
|
crates = set()
|
|
if not crates:
|
|
with log_path.open("a", encoding="utf-8") as log:
|
|
log.write(metadata.stdout)
|
|
log.write(metadata.stderr)
|
|
write_status(
|
|
status_path,
|
|
"Build graph failed",
|
|
"Open the launch log for Cargo details",
|
|
8,
|
|
"error",
|
|
)
|
|
return metadata.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_lane, lane),
|
|
cwd=ROOT,
|
|
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 = len(completed_package_ids.intersection(crates))
|
|
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 = len(completed_package_ids.intersection(crates))
|
|
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-lane", type=Path, default=DEFAULT_CARGO_LANE)
|
|
parser.add_argument("--lane", default="dev")
|
|
arguments = parser.parse_args()
|
|
return run(
|
|
arguments.status_path,
|
|
arguments.log_path,
|
|
arguments.cargo_lane,
|
|
arguments.lane,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|