Ticket #72678: find-rust-failures.py

File find-rust-failures.py, 4.3 KB (added by azhuchkov (Andrey Zhuchkov), 5 months ago)

Script to find ports failed due to rust dependency

Line 
1#!/usr/bin/env python3
2"""
3Print ports whose latest Monterey x86_64 build failed in the
4install-dependencies step because Rust failed to build.
5
6Strategy:
71. Collect every port that lists 'rust' in any dependency.
82. For each port:
9   • open /port/<name>/builds/ and grab the newest build number for
10     builder “12” or “12_x86_64”;
11   • pull that build from Buildbot JSON and check whether the
12     install-dependencies step failed with the canonical
13     “Failed to build rust …” message.
143. Print the port name as soon as the failure is confirmed.
15"""
16
17import html
18import json
19import re
20import subprocess
21import sys
22import urllib.request
23from urllib.error import HTTPError
24
25# ---------------------------------------------------------------------------
26ROOT_BB   = "https://build.macports.org"
27PORT_SITE = "https://ports.macports.org"
28BUILDER   = "ports-12_x86_64-builder"           # Buildbot builder name
29UA_HDRS   = {"User-Agent": "mp-rust-check/1.1"}
30TIMEOUT   = 15                                  # seconds
31RUST_FAIL = re.compile(r"Dependency &#39;rust&#39;.+has previously failed", re.I)
32
33# --------------------------------------------------------------------------1-
34def fetch(url: str) -> str:
35    """Return response body decoded as UTF-8 (errors ignored)."""
36    req = urllib.request.Request(url, headers=UA_HDRS)
37    with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
38        return r.read().decode(errors="ignore")
39
40def jget(url: str) -> dict:
41    req = urllib.request.Request(url, headers=UA_HDRS)
42    with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
43        return json.load(r)
44
45# ---------- 1. Ports that depend on Rust -----------------------------------
46def rust_dependents() -> set[str]:
47    """Return the set of ports that declare any dependency on 'rust'."""
48    try:
49        out = subprocess.run(
50            ["port", "-q", "echo", "depends:rust"],
51            check=True, capture_output=True, text=True
52        ).stdout
53        return {p for p in out.split() if p}
54    except (FileNotFoundError, subprocess.CalledProcessError):
55        # Fallback: parse rust/details HTML
56        page  = fetch(f"{PORT_SITE}/port/rust/details")
57        block = re.search(
58            r'Ports that depend on "rust".*?Port Health:',
59            page, re.S | re.I
60        )
61        if not block:
62            return set()
63        ports = re.findall(r'>([A-Za-z0-9_.+-]+)</a>', block.group(0))
64        return {html.unescape(p) for p in ports}
65
66# ---------- 2. Latest Monterey build number --------------------------------
67def latest_monterey_build(port: str) -> int | None:
68    """
69    Return the newest build number for Monterey x86_64
70    (“ports-12_x86_64-builder”) or None if the port has never been
71    built on that builder.
72    """
73    try:
74        page = fetch(f"{PORT_SITE}/port/{port}/builds/")
75    except HTTPError:
76        return None
77
78    # Find every link like:
79    # https://build.macports.org/builders/ports-12_x86_64-builder/builds/134458
80    numbers = re.findall(
81        rf'/builders/{BUILDER}/builds/(\d+)',
82        page,
83        re.I,
84    )
85    if not numbers:
86        return None
87    # take the largest build number = latest build
88    return int(max(numbers, key=int))
89
90# ---------- 3. Did install-dependencies fail because of Rust? --------------
91def rust_broke_build(build_no: int) -> bool:
92    """True if install-dependencies failed due to Rust."""
93    try:
94        data = jget(f"{ROOT_BB}/json/builders/{BUILDER}/builds/{build_no}")
95    except HTTPError:
96        return False
97
98    if data.get("results") == 0:
99        return False
100
101    for step in data.get("steps", []):
102        if "install-dependencies" not in step["name"] or step["results"] == 0:
103            continue
104        for _title, rel in step.get("logs", []):
105            url = rel if rel.startswith("http") else f"{ROOT_BB}{rel}"
106            url = f"{url}?as_text=1&filter=0"
107            log = fetch(url)
108            if RUST_FAIL.search(log):
109                return True
110    return False
111
112# ---------- 4. Main --------------------------------------------------------
113def main() -> None:
114    for port in rust_dependents():
115        build_no = latest_monterey_build(port)
116        if not build_no:
117            continue
118        if rust_broke_build(build_no):
119            print(port, flush=True)  # immediate output
120
121if __name__ == "__main__":
122    try:
123        main()
124    except KeyboardInterrupt:
125        sys.exit("Interrupted")