2024-04-01 14:24:15 +00:00
|
|
|
#!/usr/bin/env nix-shell
|
|
|
|
#!nix-shell -i python3 -p git "python3.withPackages (ps: with ps; [ gitpython packaging beautifulsoup4 pandas lxml ])"
|
|
|
|
|
|
|
|
import bs4
|
|
|
|
import git
|
|
|
|
import io
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import packaging.version
|
|
|
|
import pandas
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
import tempfile
|
|
|
|
import typing
|
|
|
|
import urllib.request
|
|
|
|
|
|
|
|
_QUERY_VERSION_PATTERN = re.compile('^([A-Z]+)="(.+)"$')
|
2024-04-30 23:14:30 +00:00
|
|
|
_RELEASE_PATCH_PATTERN = re.compile('^RELEASE-p([0-9]+)$')
|
2024-04-01 14:24:15 +00:00
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
MIN_VERSION = packaging.version.Version("13.0.0")
|
|
|
|
MAIN_BRANCH = "main"
|
|
|
|
TAG_PATTERN = re.compile(
|
|
|
|
f"^release/({packaging.version.VERSION_PATTERN})$", re.IGNORECASE | re.VERBOSE
|
|
|
|
)
|
|
|
|
REMOTE = "origin"
|
|
|
|
BRANCH_PATTERN = re.compile(
|
|
|
|
f"^{REMOTE}/((stable|releng)/({packaging.version.VERSION_PATTERN}))$",
|
|
|
|
re.IGNORECASE | re.VERBOSE,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def request_supported_refs() -> list[str]:
|
|
|
|
# Looks pretty shady but I think this should work with every version of the page in the last 20 years
|
|
|
|
r = re.compile("^h\d$", re.IGNORECASE)
|
|
|
|
soup = bs4.BeautifulSoup(
|
|
|
|
urllib.request.urlopen("https://www.freebsd.org/security"), features="lxml"
|
|
|
|
)
|
|
|
|
header = soup.find(
|
|
|
|
lambda tag: r.match(tag.name) is not None
|
|
|
|
and tag.text.lower() == "supported freebsd releases"
|
|
|
|
)
|
|
|
|
table = header.find_next("table")
|
|
|
|
df = pandas.read_html(io.StringIO(table.prettify()))[0]
|
|
|
|
return list(df["Branch"])
|
|
|
|
|
|
|
|
|
|
|
|
def query_version(repo: git.Repo) -> dict[str, typing.Any]:
|
|
|
|
# This only works on FreeBSD 13 and later
|
|
|
|
text = (
|
|
|
|
subprocess.check_output(
|
|
|
|
["bash", os.path.join(repo.working_dir, "sys", "conf", "newvers.sh"), "-v"]
|
|
|
|
)
|
|
|
|
.decode("utf-8")
|
|
|
|
.strip()
|
|
|
|
)
|
|
|
|
fields = dict()
|
|
|
|
for line in text.splitlines():
|
|
|
|
m = _QUERY_VERSION_PATTERN.match(line)
|
|
|
|
if m is None:
|
|
|
|
continue
|
|
|
|
fields[m[1].lower()] = m[2]
|
|
|
|
|
2024-04-30 23:14:30 +00:00
|
|
|
parsed = packaging.version.parse(fields["revision"])
|
|
|
|
fields["major"] = parsed.major
|
|
|
|
fields["minor"] = parsed.minor
|
|
|
|
|
|
|
|
# Extract the patch number from `RELAESE-p<patch>`, which is used
|
|
|
|
# e.g. in the "releng" branches.
|
|
|
|
m = _RELEASE_PATCH_PATTERN.match(fields["branch"])
|
|
|
|
if m is not None:
|
|
|
|
fields["patch"] = m[1]
|
|
|
|
|
2024-04-01 14:24:15 +00:00
|
|
|
return fields
|
|
|
|
|
|
|
|
|
|
|
|
def handle_commit(
|
|
|
|
repo: git.Repo,
|
|
|
|
rev: git.objects.commit.Commit,
|
|
|
|
ref_name: str,
|
|
|
|
ref_type: str,
|
|
|
|
supported_refs: list[str],
|
|
|
|
old_versions: dict[str, typing.Any],
|
|
|
|
) -> dict[str, typing.Any]:
|
|
|
|
if old_versions.get(ref_name, {}).get("rev", None) == rev.hexsha:
|
|
|
|
print(f"{ref_name}: revision still {rev.hexsha}, skipping")
|
|
|
|
return old_versions[ref_name]
|
|
|
|
|
|
|
|
repo.git.checkout(rev)
|
|
|
|
print(f"{ref_name}: checked out {rev.hexsha}")
|
|
|
|
|
|
|
|
full_hash = (
|
|
|
|
subprocess.check_output(["nix", "hash", "path", "--sri", repo.working_dir])
|
|
|
|
.decode("utf-8")
|
|
|
|
.strip()
|
|
|
|
)
|
|
|
|
print(f"{ref_name}: hash is {full_hash}")
|
|
|
|
|
|
|
|
version = query_version(repo)
|
|
|
|
print(f"{ref_name}: version is {version['version']}")
|
|
|
|
|
|
|
|
return {
|
|
|
|
"rev": rev.hexsha,
|
|
|
|
"hash": full_hash,
|
|
|
|
"ref": ref_name,
|
|
|
|
"refType": ref_type,
|
|
|
|
"supported": ref_name in supported_refs,
|
2024-04-30 23:14:30 +00:00
|
|
|
"version": version,
|
2024-04-01 14:24:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
# Normally uses /run/user/*, which is on a tmpfs and too small
|
|
|
|
temp_dir = tempfile.TemporaryDirectory(dir="/tmp")
|
|
|
|
print(f"Selected temporary directory {temp_dir.name}")
|
|
|
|
|
|
|
|
if len(sys.argv) >= 2:
|
|
|
|
orig_repo = git.Repo(sys.argv[1])
|
|
|
|
print(f"Fetching updates on {orig_repo.git_dir}")
|
|
|
|
orig_repo.remote("origin").fetch()
|
|
|
|
else:
|
|
|
|
print("Cloning source repo")
|
|
|
|
orig_repo = git.Repo.clone_from(
|
|
|
|
"https://git.FreeBSD.org/src.git", to_path=os.path.join(temp_dir.name, "orig")
|
|
|
|
)
|
|
|
|
|
|
|
|
supported_refs = request_supported_refs()
|
|
|
|
print(f"Supported refs are: {' '.join(supported_refs)}")
|
|
|
|
|
|
|
|
print("Doing git crimes, do not run `git worktree prune` until after script finishes!")
|
|
|
|
workdir = os.path.join(temp_dir.name, "work")
|
|
|
|
git.cmd.Git(orig_repo.git_dir).worktree("add", "--orphan", workdir)
|
|
|
|
|
|
|
|
# Have to create object before removing .git otherwise it will complain
|
|
|
|
repo = git.Repo(workdir)
|
|
|
|
repo.git.set_persistent_git_options(git_dir=repo.git_dir)
|
|
|
|
# Remove so that nix hash doesn't see the file
|
|
|
|
os.remove(os.path.join(workdir, ".git"))
|
|
|
|
|
|
|
|
print(f"Working in directory {repo.working_dir} with git directory {repo.git_dir}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
with open(os.path.join(BASE_DIR, "versions.json"), "r") as f:
|
|
|
|
old_versions = json.load(f)
|
|
|
|
except FileNotFoundError:
|
|
|
|
old_versions = dict()
|
|
|
|
|
|
|
|
versions = dict()
|
|
|
|
for tag in repo.tags:
|
|
|
|
m = TAG_PATTERN.match(tag.name)
|
|
|
|
if m is None:
|
|
|
|
continue
|
|
|
|
version = packaging.version.parse(m[1])
|
|
|
|
if version < MIN_VERSION:
|
|
|
|
print(f"Skipping old tag {tag.name} ({version})")
|
|
|
|
continue
|
|
|
|
|
|
|
|
print(f"Trying tag {tag.name} ({version})")
|
|
|
|
|
|
|
|
result = handle_commit(
|
|
|
|
repo, tag.commit, tag.name, "tag", supported_refs, old_versions
|
|
|
|
)
|
2024-04-30 23:14:30 +00:00
|
|
|
|
|
|
|
# Hack in the patch version from parsing the tag, if we didn't
|
|
|
|
# get one from the "branch" field (from newvers). This is
|
|
|
|
# probably 0.
|
|
|
|
versionObj = result["version"]
|
|
|
|
if "patch" not in versionObj:
|
|
|
|
versionObj["patch"] = version.micro
|
|
|
|
|
2024-04-01 14:24:15 +00:00
|
|
|
versions[tag.name] = result
|
|
|
|
|
|
|
|
for branch in repo.remote("origin").refs:
|
|
|
|
m = BRANCH_PATTERN.match(branch.name)
|
|
|
|
if m is not None:
|
|
|
|
fullname = m[1]
|
|
|
|
version = packaging.version.parse(m[3])
|
|
|
|
if version < MIN_VERSION:
|
|
|
|
print(f"Skipping old branch {fullname} ({version})")
|
|
|
|
continue
|
|
|
|
print(f"Trying branch {fullname} ({version})")
|
|
|
|
elif branch.name == f"{REMOTE}/{MAIN_BRANCH}":
|
|
|
|
fullname = MAIN_BRANCH
|
|
|
|
print(f"Trying development branch {fullname}")
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
|
|
|
|
result = handle_commit(
|
|
|
|
repo, branch.commit, fullname, "branch", supported_refs, old_versions
|
|
|
|
)
|
|
|
|
versions[fullname] = result
|
|
|
|
|
|
|
|
|
|
|
|
with open(os.path.join(BASE_DIR, "versions.json"), "w") as out:
|
|
|
|
json.dump(versions, out, sort_keys=True, indent=2)
|
|
|
|
out.write("\n")
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|