| 1 | #!/usr/bin/env python3 |
|---|
| 2 | """ |
|---|
| 3 | Print ports whose latest Monterey x86_64 build failed in the |
|---|
| 4 | install-dependencies step because Rust failed to build. |
|---|
| 5 | |
|---|
| 6 | Strategy: |
|---|
| 7 | 1. Collect every port that lists 'rust' in any dependency. |
|---|
| 8 | 2. 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. |
|---|
| 14 | 3. Print the port name as soon as the failure is confirmed. |
|---|
| 15 | """ |
|---|
| 16 | |
|---|
| 17 | import html |
|---|
| 18 | import json |
|---|
| 19 | import re |
|---|
| 20 | import subprocess |
|---|
| 21 | import sys |
|---|
| 22 | import urllib.request |
|---|
| 23 | from urllib.error import HTTPError |
|---|
| 24 | |
|---|
| 25 | # --------------------------------------------------------------------------- |
|---|
| 26 | ROOT_BB = "https://build.macports.org" |
|---|
| 27 | PORT_SITE = "https://ports.macports.org" |
|---|
| 28 | BUILDER = "ports-12_x86_64-builder" # Buildbot builder name |
|---|
| 29 | UA_HDRS = {"User-Agent": "mp-rust-check/1.1"} |
|---|
| 30 | TIMEOUT = 15 # seconds |
|---|
| 31 | RUST_FAIL = re.compile(r"Dependency 'rust'.+has previously failed", re.I) |
|---|
| 32 | |
|---|
| 33 | # --------------------------------------------------------------------------1- |
|---|
| 34 | def 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 | |
|---|
| 40 | def 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 ----------------------------------- |
|---|
| 46 | def 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 -------------------------------- |
|---|
| 67 | def 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? -------------- |
|---|
| 91 | def 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 -------------------------------------------------------- |
|---|
| 113 | def 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 | |
|---|
| 121 | if __name__ == "__main__": |
|---|
| 122 | try: |
|---|
| 123 | main() |
|---|
| 124 | except KeyboardInterrupt: |
|---|
| 125 | sys.exit("Interrupted") |
|---|