Scaffold: agent starter kit (seeded from Ebb)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Version drift report for gradle/libs.versions.toml (Plan 09, D-056).
|
||||
|
||||
Reads every [versions] entry, finds an artifact that uses it in [libraries] or [plugins], and asks Google Maven and
|
||||
Maven Central for that artifact's maven-metadata.xml. Prints one table: current, latest stable, newest (only where a
|
||||
pre-release is newer than the latest stable), and how far the current version is behind.
|
||||
The Gradle wrapper version is compared with services.gradle.org. Nothing here runs from the build; run it by hand:
|
||||
|
||||
python3 tools/dependency-report/latest.py
|
||||
|
||||
Rule (CLAUDE.md › Build and verify): run it at the start of every plan. A component more than one stable minor
|
||||
behind gets a line in that plan's report; a toolchain component (Gradle, AGP, Kotlin, JDK LTS) more than one line
|
||||
behind triggers a Plan 09-shaped upgrade before the plan starts.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
REPOS = ["https://dl.google.com/android/maven2", "https://repo1.maven.org/maven2"]
|
||||
# Catalog entries that are SDK levels, not artifacts.
|
||||
NOT_ARTIFACTS = {"minSdk", "compileSdk", "targetSdk"}
|
||||
# The toolchain is looked up by its Gradle plugin, not by the first library that shares its version.
|
||||
PREFERRED = {
|
||||
"agp": ("com.android.tools.build", "gradle"),
|
||||
"kotlin": ("org.jetbrains.kotlin", "kotlin-gradle-plugin"),
|
||||
"ksp": ("com.google.devtools.ksp", "symbol-processing-gradle-plugin"),
|
||||
}
|
||||
STABLE = re.compile(r"^\d+(\.\d+)*$")
|
||||
|
||||
|
||||
def key(version):
|
||||
"""Numeric parts first; a pre-release (anything after the first '-') sorts before its release."""
|
||||
base, _, suffix = version.partition("-")
|
||||
numbers = [int(p) if p.isdigit() else -1 for p in base.split(".")]
|
||||
return numbers, (0, suffix) if suffix else (1, "")
|
||||
|
||||
|
||||
def fetch(url):
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=20) as response:
|
||||
return response.read()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def versions_of(group, name):
|
||||
"""Versions from every repository together: some artifacts (early KSP) also sit, stale, on Google Maven."""
|
||||
found = set()
|
||||
for repo in REPOS:
|
||||
body = fetch(f"{repo}/{group.replace('.', '/')}/{name}/maven-metadata.xml")
|
||||
if body:
|
||||
found.update(v.text for v in ET.fromstring(body).iter("version") if v.text)
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def plugin_marker(plugin_id):
|
||||
return plugin_id, f"{plugin_id}.gradle.plugin"
|
||||
|
||||
|
||||
def read_catalog(text):
|
||||
"""The subset of TOML the catalog uses (works on Python < 3.11, which has no tomllib): returns the [versions]
|
||||
entries in file order and, per version name, the first library or plugin that refers to it."""
|
||||
versions, artifacts, section = {}, {}, None
|
||||
for raw in text.splitlines():
|
||||
line = raw.split("#", 1)[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("["):
|
||||
section = line.strip("[]")
|
||||
continue
|
||||
if section == "versions":
|
||||
match = re.match(r'([\w-]+)\s*=\s*"([^"]+)"', line)
|
||||
if match:
|
||||
versions[match.group(1)] = match.group(2)
|
||||
continue
|
||||
ref = re.search(r'version\.ref\s*=\s*"([^"]+)"', line)
|
||||
if not ref or ref.group(1) in artifacts:
|
||||
continue
|
||||
module = re.search(r'module\s*=\s*"([^"]+)"', line)
|
||||
plugin = re.search(r'\bid\s*=\s*"([^"]+)"', line)
|
||||
if section == "libraries" and module:
|
||||
artifacts[ref.group(1)] = tuple(module.group(1).split(":"))
|
||||
elif section == "plugins" and plugin:
|
||||
artifacts[ref.group(1)] = plugin_marker(plugin.group(1))
|
||||
return versions, artifacts
|
||||
|
||||
|
||||
def behind(current, stable):
|
||||
if not stable or not STABLE.match(current):
|
||||
return ""
|
||||
c, s = [int(x) for x in current.split(".")], [int(x) for x in stable.split(".")]
|
||||
c += [0] * (3 - len(c))
|
||||
s += [0] * (3 - len(s))
|
||||
if c[0] < s[0]:
|
||||
return f"{s[0] - c[0]} major"
|
||||
if c[1] < s[1]:
|
||||
return f"{s[1] - c[1]} minor"
|
||||
if c[2] < s[2]:
|
||||
return "patch"
|
||||
return ""
|
||||
|
||||
|
||||
def row(name, coordinate, current, available):
|
||||
stable = sorted((v for v in available if STABLE.match(v)), key=key)
|
||||
everything = sorted(available, key=key)
|
||||
latest = stable[-1] if stable else "?"
|
||||
newest = everything[-1] if everything else "?"
|
||||
return [name, coordinate, current, latest, newest if newest not in (latest, current) else "", behind(current, latest if stable else "")]
|
||||
|
||||
|
||||
def gradle_row():
|
||||
props = (ROOT / "gradle/wrapper/gradle-wrapper.properties").read_text()
|
||||
match = re.search(r"gradle-([\d.]+)-(bin|all)\.zip", props)
|
||||
current = match.group(1) if match else "?"
|
||||
body = fetch("https://services.gradle.org/versions/all")
|
||||
available = []
|
||||
if body:
|
||||
available = [v["version"] for v in json.loads(body) if not v.get("snapshot") and not v.get("nightly")]
|
||||
return row("gradle (wrapper)", "services.gradle.org", current, available)
|
||||
|
||||
|
||||
def main():
|
||||
versions, artifacts = read_catalog((ROOT / "gradle/libs.versions.toml").read_text())
|
||||
rows = [gradle_row()]
|
||||
for name, current in versions.items():
|
||||
if name in NOT_ARTIFACTS:
|
||||
continue
|
||||
coordinate = PREFERRED.get(name) or artifacts.get(name)
|
||||
if not coordinate:
|
||||
rows.append([name, "(no artifact)", str(current), "", "", ""])
|
||||
continue
|
||||
rows.append(row(name, ":".join(coordinate), str(current), versions_of(*coordinate)))
|
||||
header = ["version", "artifact", "current", "latest stable", "newer pre-release", "behind"]
|
||||
widths = [max(len(str(r[i])) for r in rows + [header]) for i in range(len(header))]
|
||||
print(" | ".join(h.ljust(w) for h, w in zip(header, widths)))
|
||||
print("-+-".join("-" * w for w in widths))
|
||||
for r in rows:
|
||||
print(" | ".join(str(c).ljust(w) for c, w in zip(r, widths)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user