201 lines
5.3 KiB
Python
Executable File
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())
|