Files
libretro/scripts/version_basis.py
T
Abdessamad Derraz 6aa236440a feat: read core_version from its recorded basis
version_basis says where a profile's version is read: a pattern in a
file, the nearest tag and its distance, an alias, a package, none, or
unverified with the reason. profile_sync --verify-version reads it again
at the pin and exits 1 on a difference, --set-version writes what the
code states. A pin advance reads the version at the new pin in the same
write and is refused when it cannot.

upstream.describe counts the commits after the nearest tag in a treeless
clone kept under the cache, and graph_ancestry answers a pair GitHub
refuses to compare.
2026-10-27 18:48:11 +01:00

277 lines
12 KiB
Python

"""Read a profile's core_version from the source its version_basis records.
A profile states the version of the code it was read at. Copied from a
libretro .info or left behind by a pin advance, it named another release:
snes9x said 1.61 at a pin whose snes9x.h states 1.63, and four bulk advances
moved 295 pins without touching one version. version_basis records where the
version is read, so a tool can read it again at any pin:
- ``file``: a pattern in one file of a declared repository at its pin, the
version the program states (a macro, a build file, a package file).
- ``tag``: the nearest tag in the pin's history and the commits after it,
for code that takes its version from git at build time. core_version is
the tag at distance 0, else ``<tag>-<distance>-g<pin7>``, git describe's
form.
- ``alias``: the same binary as another profile, whose version it carries.
- ``package``: an artefact read outside any source tree (an Android package,
a closed binary, a release archive): a version no pin can be asked for.
A pattern reads it from the artefact's name when the name states it.
- ``none``: the code at the pin states no version and no tag reaches it.
core_version is then null.
- ``unverified``: no reading could be made (``why``: no pin, a host the
tools cannot read, a forge gone, no build named as read, no source).
core_version stays as written and is reported unconfirmed.
A profile whose builds live in different repositories keys version_basis by
build mode, the way it keys core_version.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
import upstream
KINDS = ("file", "tag", "alias", "package", "none", "unverified")
# Values copied from a .info or a branch name, which name no version.
PLACEHOLDERS = frozenset({"git", "svn", "unknown", "master", "main", "latest", "dev", "head"})
# Pin field of each repository field a basis may name. A mirror carries the
# source's history, so source_commit pins it too.
PIN_FIELDS = {"source": "source_commit", "source_mirror": "source_commit",
"upstream": "upstream_commit"}
MATCH = "MATCH"
DRIFT = "DRIFT"
UNREADABLE = "UNREADABLE"
NO_BASIS = "NO_BASIS"
UNCHECKED = "UNCHECKED"
FAILING = (DRIFT, UNREADABLE, NO_BASIS)
_FIELD = re.compile(r"\{(\d+)(?::([^}]*))?\}")
class VersionUnreadable(Exception):
"""The basis yields no version at the pin it is asked about."""
@dataclass(frozen=True)
class Reading:
mode: str
kind: str
declared: str | None
read: str | None
where: str
status: str
# What a write sets beside core_version: the tag and distance of a tag basis.
updates: tuple[tuple[str, object], ...] = ()
def bases(profile: dict) -> dict[str, dict]:
"""The basis of each build mode, "" when one covers every build."""
basis = profile.get("version_basis")
if not isinstance(basis, dict):
return {}
if "kind" in basis:
return {"": basis}
return {str(mode): value for mode, value in basis.items() if isinstance(value, dict)}
def declared(profile: dict, mode: str) -> str | None:
"""core_version for a build mode, as written."""
value = profile.get("core_version")
if isinstance(value, dict):
value = value.get(mode)
return None if value is None else str(value)
def _by_mode(profile: dict, field: str, mode: str) -> str | None:
value = profile.get(field)
if isinstance(value, dict):
value = value.get(mode)
return str(value) if value else None
def is_placeholder(value: object) -> bool:
return isinstance(value, str) and value.strip().casefold() in PLACEHOLDERS
def format_version(match: re.Match, template: str | None) -> str:
"""The groups of a match laid out by template, `{1}.{2}` or `{4:02d}`.
A format spec applies to the group read as an integer: FBNeo writes its
alpha number on two digits (VER_ALPHA 3 is 1.0.0.03).
"""
if not template:
return match.group(1).strip()
def field(found: re.Match) -> str:
text = match.group(int(found.group(1))) or ""
spec = found.group(2)
return format(int(text, 0), spec) if spec else text.strip()
return _FIELD.sub(field, template)
def read_text_version(text: str, pattern: str, template: str | None) -> tuple[str, int] | None:
"""The version a pattern reads in a file's text, and its line number."""
found = re.search(pattern, text, re.MULTILINE)
if not found:
return None
return format_version(found, template), text.count("\n", 0, found.start()) + 1
def describe_form(tag: str, distance: int, pin: str) -> str:
"""core_version of a tag basis: the tag, or git describe's long form."""
return tag if distance == 0 else f"{tag}-{distance}-g{pin[:7]}"
def _repo_and_pin(
profile: dict, basis: dict, mode: str, cache_dir: str, offline: bool,
) -> tuple[upstream.Repo, str]:
"""The repository a basis reads and the commit it is read at."""
if basis.get("url"):
repo = upstream.parse_repo(str(basis["url"]))
pin = str(basis.get("commit") or "")
where = str(basis["url"])
else:
field = str(basis.get("repo") or "source")
url = _by_mode(profile, field, mode) or ""
repo = upstream.parse_repo(url)
pin = _by_mode(profile, PIN_FIELDS.get(field, ""), mode) or ""
where = url or field
if repo is not None and not pin and field == "upstream" and profile.get("profiled_date"):
# The pin of a second repository falls back on profiled_date,
# as profile_sync resolves it.
pin = upstream.resolve_commit_at(
repo, str(profile["profiled_date"]), cache_dir, offline) or ""
if repo is None:
raise VersionUnreadable(f"{where}: not a forge repository")
if not pin:
raise VersionUnreadable(f"{repo.slug}: no pinned commit")
return repo, pin
def _ancestry(
repo: upstream.Repo, base: str, head: str, cache_dir: str, offline: bool,
) -> tuple[int, int] | None:
"""The forge's compare, else the commit graph: GitHub refuses with 422
a compare whose diff is too large (aps3e's tag and pin)."""
try:
relation = upstream.ancestry(repo, base, head, cache_dir, offline)
except upstream.RateLimitError:
raise
except upstream.UpstreamError:
relation = None
if relation is None:
try:
relation = upstream.graph_ancestry(repo, base, head, cache_dir, offline)
except upstream.NotCachedError:
return None
return relation
def read(
profile: dict, mode: str, basis: dict, cache_dir: str, offline: bool,
profiles: dict | None = None, recompute: bool = False,
) -> Reading:
"""The version the basis yields at the profile's pin, against core_version.
``recompute`` asks git for the nearest tag again, the way a pin advance
has to: a recorded tag only proves its own distance, and a tag released
between the two pins would go unseen.
"""
stated = declared(profile, mode)
kind = str(basis.get("kind") or "")
def reading(value: str | None, where: str, updates: tuple = ()) -> Reading:
status = MATCH if value == stated else DRIFT
return Reading(mode, kind, stated, value, where, status, updates)
def unreadable(reason: str) -> Reading:
return Reading(mode, kind, stated, None, reason, UNREADABLE)
try:
if kind == "none":
if recompute:
# A release tagged since the basis was written gives the
# code a version: the advance has to name it.
repo, pin = _repo_and_pin(profile, basis, mode, cache_dir, offline)
described = upstream.describe(repo, pin, cache_dir, offline)
if described is not None:
return Reading(mode, kind, stated, describe_form(*described, pin),
f"{repo.slug}: tag {described[0]} is now in "
f"{pin[:7]}'s history", DRIFT)
return reading(None, "the code states no version")
if kind == "unverified":
return Reading(mode, kind, stated, None, f"not verified: {basis.get('why')}",
UNCHECKED)
if kind == "package":
artifact = str(basis.get("artifact") or "")
if basis.get("pattern"):
# An artefact named after its version says it in its name:
# advancemame-0.94.0.tar.gz.
found = read_text_version(artifact, str(basis["pattern"]), basis.get("format"))
if found is None:
return unreadable(f"{artifact}: pattern not found")
return reading(found[0], f"named by {artifact}")
return Reading(mode, kind, stated, None, f"read from {artifact}", UNCHECKED)
if kind == "alias":
other = (profiles or {}).get(str(basis.get("of") or ""))
if other is None:
return unreadable(f"alias of {basis.get('of')}: no such profile")
return reading(declared(other, mode) if isinstance(other.get("core_version"), dict)
else declared(other, ""), f"alias of {basis['of']}")
repo, pin = _repo_and_pin(profile, basis, mode, cache_dir, offline)
if kind == "file":
path = str(basis.get("path") or "")
lines = upstream.fetch_file(repo, pin, path, cache_dir, offline)
if lines is None:
return unreadable(f"{repo.slug}@{pin[:7]}:{path}: absent")
found = read_text_version("\n".join(lines), str(basis.get("pattern") or ""),
basis.get("format"))
if found is None:
return unreadable(f"{repo.slug}@{pin[:7]}:{path}: pattern not found")
value, line = found
return reading(value, f"{repo.slug}@{pin[:7]}:{path}:{line}")
if kind == "tag":
if recompute:
described = upstream.describe(repo, pin, cache_dir, offline, basis.get("match"))
if described is None:
return unreadable(f"{repo.slug}@{pin[:7]}: no tag in its history")
tag, distance = described
else:
tag, distance = str(basis.get("tag") or ""), basis.get("distance")
commit = upstream.tag_commit(repo, tag, cache_dir, offline)
if commit is None:
return unreadable(f"{repo.slug}: no tag {tag}")
relation = (0, 0) if commit == pin else _ancestry(
repo, commit, pin, cache_dir, offline)
if relation is None:
return unreadable(f"{repo.slug}: {tag}...{pin[:7]} not comparable")
if relation[1]:
return unreadable(f"{repo.slug}: tag {tag} is not in {pin[:7]}'s history")
if relation[0] != distance:
return Reading(mode, kind, stated, describe_form(tag, relation[0], pin),
f"{repo.slug}: {pin[:7]} is {relation[0]} commits after "
f"{tag}, the basis says {distance}", DRIFT,
(("distance", relation[0]),))
return reading(describe_form(tag, distance, pin),
f"{repo.slug}: tag {tag} + {distance}",
(("tag", tag), ("distance", distance)))
except (upstream.UpstreamError, VersionUnreadable) as exc:
return unreadable(str(exc))
return unreadable(f"unknown kind {kind!r}")
def check(
profile: dict, cache_dir: str, offline: bool, profiles: dict | None = None,
recompute: bool = False,
) -> list[Reading]:
"""One reading per build mode, or a NO_BASIS reading."""
found = bases(profile)
if not found:
return [Reading("", "", declared(profile, ""), None, "no version_basis", NO_BASIS)]
return [read(profile, mode, basis, cache_dir, offline, profiles, recompute)
for mode, basis in found.items()]