mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-05 04:07:44 +00:00
202 lines
9.1 KiB
Python
202 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the canonical Windows TH component before the full Windows outputs.
|
|
|
|
Only its direct derivation inputs are prepared under the normal build budget.
|
|
The component then has a short budget; its failure remains a CI failure even
|
|
when the separate strace diagnostic succeeds.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
STORE = "/nix/store/"
|
|
DRV_NAME = re.compile(r"[0-9abcdfghijklmnpqrsvwxyz]{32}-[A-Za-z0-9+._?=-]+\.drv\Z")
|
|
OUTPUT = re.compile(r"[A-Za-z_][A-Za-z0-9_+-]*\Z")
|
|
COMPONENT = "wai-app-static-lib-wai-app-static-x86_64-w64-mingw32-3.1.9.1"
|
|
|
|
|
|
def drv_path(value, *, basename=False):
|
|
if not isinstance(value, str):
|
|
raise ValueError("derivation path must be a string")
|
|
name = value[len(STORE):] if value.startswith(STORE) else value if basename else ""
|
|
if not DRV_NAME.fullmatch(name):
|
|
raise ValueError(f"invalid derivation path: {value!r}")
|
|
return STORE + name
|
|
|
|
|
|
def direct_inputs(document, expected):
|
|
"""Return explicit drv^output goals from supported Nix JSON formats."""
|
|
expected = drv_path(expected)
|
|
if not isinstance(document, dict):
|
|
raise ValueError("derivation JSON must be an object")
|
|
if "derivations" in document or "version" in document:
|
|
if (set(document) != {"derivations", "version"} or
|
|
type(document["version"]) is not int or document["version"] != 4):
|
|
raise ValueError("unsupported derivation JSON version")
|
|
rows = document["derivations"]
|
|
modern = True
|
|
else:
|
|
rows = document
|
|
modern = False
|
|
if not isinstance(rows, dict) or len(rows) != 1:
|
|
raise ValueError("expected exactly the selected component derivation")
|
|
key, row = next(iter(rows.items()))
|
|
if drv_path(key, basename=modern) != expected or not isinstance(row, dict):
|
|
raise ValueError("selected component derivation does not match")
|
|
if (row.get("system") != "x86_64-linux" or not isinstance(row.get("env"), dict) or
|
|
row["env"].get("name") != COMPONENT):
|
|
raise ValueError("expected the native-Linux-built Windows wai-app-static component")
|
|
if not isinstance(row.get("outputs"), dict) or set(row["outputs"]) != {"out"}:
|
|
raise ValueError("expected the component's single out output")
|
|
if modern:
|
|
if (type(row.get("version")) is not int or row["version"] != 4 or
|
|
not isinstance(row.get("inputs"), dict) or set(row["inputs"]) != {"drvs", "srcs"} or
|
|
not isinstance(row["inputs"]["srcs"], list)):
|
|
raise ValueError("unsupported derivation input schema")
|
|
inputs = row["inputs"]["drvs"]
|
|
else:
|
|
if "inputs" in row or "version" in row or not isinstance(row.get("inputDrvs"), dict):
|
|
raise ValueError("unsupported legacy derivation input schema")
|
|
inputs = row["inputDrvs"]
|
|
if not isinstance(inputs, dict):
|
|
raise ValueError("derivation inputs must be an object")
|
|
goals = []
|
|
seen = set()
|
|
for key, value in sorted(inputs.items()):
|
|
path = drv_path(key, basename=modern)
|
|
if path == expected or path in seen:
|
|
raise ValueError("duplicate or self-referencing input derivation")
|
|
seen.add(path)
|
|
if modern:
|
|
if not isinstance(value, dict) or set(value) != {"outputs", "dynamicOutputs"}:
|
|
raise ValueError("unsupported input derivation schema")
|
|
if value["dynamicOutputs"] != {}:
|
|
raise ValueError("dynamic derivation outputs are not supported")
|
|
outputs = value["outputs"]
|
|
else:
|
|
outputs = value
|
|
if (not isinstance(outputs, list) or not outputs or
|
|
any(not isinstance(o, str) or not OUTPUT.fullmatch(o) for o in outputs) or
|
|
len(set(outputs)) != len(outputs)):
|
|
raise ValueError("expected explicit, unique named input outputs")
|
|
goals.append(path + "^" + ",".join(outputs))
|
|
return goals
|
|
|
|
|
|
def call(argv, *, capture=False, timeout=600):
|
|
print("+ " + " ".join(argv), flush=True)
|
|
result = subprocess.run(argv, text=True, stdout=subprocess.PIPE if capture else None,
|
|
check=True, timeout=timeout)
|
|
return result.stdout if capture else None
|
|
|
|
|
|
def flake_context():
|
|
root = Path(call(["git", "rev-parse", "--show-toplevel"], capture=True).strip())
|
|
revision = call(["git", "rev-parse", "HEAD"], capture=True).strip()
|
|
if not re.fullmatch(r"[0-9a-f]{40}", revision):
|
|
raise ValueError("expected a committed checkout")
|
|
ref = root.as_uri().replace("file:", "git+file:", 1) + "?rev=" + revision
|
|
original = (f'(builtins.getFlake {json.dumps(ref)}).hydraJobs.x86_64-linux.'
|
|
'"x86_64-windows:exe:simplex-chat".project.hsPkgs.wai-app-static.components.library')
|
|
return revision, ref, original
|
|
|
|
|
|
def read_state(path):
|
|
state = json.loads(path.read_text())
|
|
revision, ref, original = flake_context()
|
|
if (state["revision"], state["flakeRef"], state["original"]) != (revision, ref, original):
|
|
raise ValueError("gate state belongs to a different checkout")
|
|
drv_path(state["drv"])
|
|
if direct_inputs(state["derivation"], state["drv"]) != state["inputGoals"]:
|
|
raise ValueError("gate input goals do not match the saved derivation")
|
|
return state
|
|
|
|
|
|
def prepare(path):
|
|
# Evaluation can realize planning/compiler IFDs. Give those the same normal
|
|
# budget as the direct inputs; only the ready component gets 300 seconds.
|
|
deadline = time.monotonic() + 14400
|
|
|
|
def remaining():
|
|
seconds = deadline - time.monotonic()
|
|
if seconds <= 0:
|
|
raise subprocess.TimeoutExpired("Windows TH input preparation", 14400)
|
|
return seconds
|
|
|
|
revision, ref, original = flake_context()
|
|
drv = call(["nix", "eval", "--impure", "--raw", "--expr", f"({original}).drvPath",
|
|
"-L", "--keep-failed", "--max-jobs", "1", "--timeout", "7200"],
|
|
capture=True, timeout=remaining()).strip()
|
|
drv_path(drv)
|
|
document = json.loads(call(["nix", "derivation", "show", drv], capture=True,
|
|
timeout=min(600, remaining())))
|
|
goals = direct_inputs(document, drv)
|
|
state = dict(revision=revision, flakeRef=ref, original=original, drv=drv,
|
|
derivation=document, inputGoals=goals, inputsReady=False)
|
|
path.write_text(json.dumps(state, indent=2) + "\n")
|
|
# No bare derivations or ^*: preserve only outputs actually requested by
|
|
# this canonical component. Empty goals must never build the default flake.
|
|
if goals:
|
|
call(["nix", "build", *goals, "--no-link", "-L", "--keep-failed",
|
|
"--max-jobs", "1", "--timeout", "7200"], timeout=remaining())
|
|
state["inputsReady"] = True
|
|
path.write_text(json.dumps(state, indent=2) + "\n")
|
|
|
|
|
|
def build(path):
|
|
state = read_state(path)
|
|
if state.get("inputsReady") is not True:
|
|
raise ValueError("canonical component inputs are not ready")
|
|
if output := os.environ.get("GITHUB_OUTPUT"):
|
|
with open(output, "a") as handle:
|
|
handle.write("attempted=true\n")
|
|
state["canonicalResult"] = "running"
|
|
path.write_text(json.dumps(state, indent=2) + "\n")
|
|
try:
|
|
call(["nix", "build", state["drv"] + "^out", "--no-link", "-L", "--keep-failed",
|
|
"--max-jobs", "1", "--timeout", "300"], timeout=330)
|
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
state["canonicalResult"] = "failure"
|
|
path.write_text(json.dumps(state, indent=2) + "\n")
|
|
raise
|
|
state["canonicalResult"] = "success"
|
|
path.write_text(json.dumps(state, indent=2) + "\n")
|
|
|
|
|
|
def trace(path):
|
|
state = read_state(path)
|
|
if state.get("inputsReady") is not True:
|
|
raise ValueError("canonical component inputs are not ready")
|
|
if state.get("canonicalResult") != "failure":
|
|
raise ValueError("diagnostic requires a failed canonical component attempt")
|
|
tool = f'(import ((builtins.getFlake {json.dumps(state["flakeRef"])}).inputs.nixpkgs) {{ system = "x86_64-linux"; }}).strace'
|
|
# A cache miss must not turn the diagnostic into a toolchain build.
|
|
call(["nix", "build", "--impure", "--expr", tool, "--no-link", "--max-jobs", "0",
|
|
"--builders", "", "--option", "external-builders", "[]"], timeout=120)
|
|
call(["nix", "build", "--impure", "--file", "scripts/nix/trace-windows-th.nix",
|
|
"--arg", "original", state["original"], "--arg", "strace", tool,
|
|
"--argstr", "expectedDrv", state["drv"], "--no-link", "-L", "--keep-failed",
|
|
"--max-jobs", "1", "--timeout", "300"], timeout=600)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("action", choices=["prepare", "build", "trace"])
|
|
parser.add_argument("--state", type=Path, default=Path("windows-th-gate.json"))
|
|
args = parser.parse_args()
|
|
try:
|
|
{"prepare": prepare, "build": build, "trace": trace}[args.action](args.state)
|
|
except (ValueError, KeyError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
|
|
print(f"Windows TH {args.action} failed: {error}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|