mirror of https://github.com/xemu-project/xemu.git
198 lines
4.9 KiB
Python
198 lines
4.9 KiB
Python
#!/usr/bin/env python
|
|
# /// script
|
|
# dependencies = ["requests"]
|
|
# ///
|
|
"""
|
|
Update Meson wrap file `revision` fields to point to latest release.
|
|
"""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import configparser
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from dataclasses import dataclass, asdict
|
|
|
|
import requests
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
SEMVER_RE = re.compile(
|
|
r"""
|
|
^v?
|
|
(?P<major>0|[1-9]\d*)\.
|
|
(?P<minor>0|[1-9]\d*)\.
|
|
(?P<patch>0|[1-9]\d*)
|
|
$""",
|
|
re.VERBOSE,
|
|
)
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
WRAP_DIR = ROOT / "subprojects"
|
|
SESSION = requests.Session()
|
|
GH_TOKEN = os.getenv("GH_TOKEN", "")
|
|
if GH_TOKEN:
|
|
SESSION.headers["Authorization"] = f"Bearer {GH_TOKEN}"
|
|
SESSION.headers["Accept"] = "application/vnd.github+json"
|
|
|
|
|
|
def gh_sha_for_tag(owner: str, repo: str, tag: str) -> str:
|
|
data = SESSION.get(
|
|
f"https://api.github.com/repos/{owner}/{repo}/git/ref/tags/{tag}", timeout=30
|
|
).json()
|
|
|
|
# First level: get the object it points to
|
|
obj_type = data["object"]["type"]
|
|
obj_sha = data["object"]["sha"]
|
|
|
|
if obj_type == "commit":
|
|
# Lightweight tag
|
|
return obj_sha
|
|
elif obj_type == "tag":
|
|
# Annotated tag: need to dereference
|
|
tag_obj_url = data["object"]["url"]
|
|
tag_data = requests.get(tag_obj_url).json()
|
|
return tag_data["object"]["sha"]
|
|
else:
|
|
raise Exception(f"Unknown object type: {obj_type}")
|
|
|
|
|
|
def gh_latest_release(
|
|
owner: str, repo: str, pattern: re.Pattern
|
|
) -> None | tuple[str, str]:
|
|
"""
|
|
Return (tag_name, commit_sha) for the most recent matching release.
|
|
"""
|
|
releases = SESSION.get(
|
|
f"https://api.github.com/repos/{owner}/{repo}/releases", timeout=30
|
|
).json()
|
|
viable = [t for t in releases if pattern.match(t["tag_name"])]
|
|
|
|
if not viable:
|
|
return None
|
|
|
|
tag_name = viable[0]["tag_name"]
|
|
sha = gh_sha_for_tag(owner, repo, tag_name)
|
|
|
|
return tag_name, sha
|
|
|
|
|
|
def gh_latest_tag(owner: str, repo: str, pattern: re.Pattern) -> tuple[str, str]:
|
|
"""
|
|
Return (tag_name, commit_sha) for the most recent matching tag.
|
|
"""
|
|
tags = SESSION.get(
|
|
f"https://api.github.com/repos/{owner}/{repo}/tags", timeout=30
|
|
).json()
|
|
viable = [t for t in tags if pattern.match(t["name"])]
|
|
|
|
if not viable:
|
|
return None
|
|
|
|
return viable[0]["name"], viable[0]["commit"]["sha"]
|
|
|
|
|
|
@dataclass
|
|
class UpdatedWrap:
|
|
path: str
|
|
owner: str
|
|
repo: str
|
|
old_rev: str
|
|
new_rev: str
|
|
new_tag: str
|
|
|
|
|
|
def update_wrap(path: Path) -> None | UpdatedWrap:
|
|
"""
|
|
Return (tag_name, commit_sha) if updated, otherwise None.
|
|
"""
|
|
cp = configparser.ConfigParser(interpolation=None)
|
|
cp.read(path, encoding="utf-8")
|
|
|
|
if "wrap-git" not in cp:
|
|
# FIXME: Support wrap-file from wrapdb
|
|
return None
|
|
|
|
w = cp["wrap-git"]
|
|
url = w.get("url", "")
|
|
rev = w.get("revision", "").strip()
|
|
m = re.match(r".*github\.com[:/](?P<owner>[^/]+)/(?P<repo>[^/.]+)(?:\.git)?", url)
|
|
if not (m and rev):
|
|
return None
|
|
|
|
owner, repo = m.group("owner"), m.group("repo")
|
|
try:
|
|
pattern = cp.get("update", "tag_regex", fallback=None)
|
|
pattern = re.compile(pattern) if pattern else SEMVER_RE
|
|
|
|
latest = gh_latest_release(owner, repo, pattern)
|
|
if latest is None:
|
|
log.info("Couldn't find latest release for %s/%s", owner, repo)
|
|
log.info("Searching for tags directly...")
|
|
latest = gh_latest_tag(owner, repo, pattern)
|
|
if latest is None:
|
|
log.info("Couldn't find latest tag for %s/%s", owner, repo)
|
|
return None
|
|
tag, sha = latest
|
|
except Exception as e:
|
|
log.exception(e)
|
|
return None
|
|
|
|
if sha.startswith(rev):
|
|
log.info("%s already at %s (%s)", path.name, tag, sha)
|
|
return None
|
|
|
|
log.info("%s updated to %s (%s)", path.name, tag, sha)
|
|
|
|
w["revision"] = sha
|
|
|
|
with open(path, "w", encoding="utf-8") as file:
|
|
cp.write(file)
|
|
|
|
# XXX: ConfigParser writes two extra newlines. Trim the last one.
|
|
file.seek(file.tell() - 1, 0)
|
|
file.truncate()
|
|
|
|
return UpdatedWrap(str(path), owner, repo, rev, sha, tag)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument(
|
|
"--manifest",
|
|
"-m",
|
|
action="store_true",
|
|
default=False,
|
|
help="Print JSON-formatted updated manifest",
|
|
)
|
|
ap.add_argument(
|
|
"wraps", nargs="*", help="Which wraps to update, or all if unspecified"
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
wraps = args.wraps
|
|
if wraps:
|
|
wraps = [Path(p) for p in wraps]
|
|
else:
|
|
wraps = WRAP_DIR.glob("*.wrap")
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
updated = []
|
|
for wrap in wraps:
|
|
info = update_wrap(wrap)
|
|
if info:
|
|
updated.append(asdict(info))
|
|
|
|
if args.manifest:
|
|
json.dump(updated, sys.stdout, indent=2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|