From a293887145964d8ad0c87760d10a53fc2b3054b3 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:24:21 +0200 Subject: [PATCH 1/4] feat: place the pin against its version tag --check-version said only that core_version names a tag other than the pin. It now counts the pin against that tag (after it, before it, or off its history), and names the latest release when the pin already contains it. Of 52 warnings, 43 are builds between releases; ares, boytacean, nesoid and primehack name an older release than their code, vkquake and vkquake2 a later one. --- scripts/profile_sync.py | 77 +++++++++++++++++++++++++++++++++++--- scripts/upstream.py | 35 +++++++++++++++++ tests/test_profile_sync.py | 49 ++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 5 deletions(-) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 24681a3c1..c53efceaf 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -2216,15 +2216,41 @@ def page_version(profile: dict, cache_dir: str, offline: bool) -> str | None: return match.group(1) if match else None -def version_warning(report: VersionReport | None, pin: str | None) -> str | None: - """Flag a declared version that names a tag away from the resolved pin.""" +def version_warning( + report: VersionReport | None, + pin: str | None, + relation: tuple[int, int] | None = None, + passed_release: str | None = None, +) -> str | None: + """Flag a declared version that names a tag away from the resolved pin. + + ``relation`` is the pin's (ahead, behind) count against the tag commit. + A pin after its tag is a build between two releases; a pin before it + describes code the declared release had already replaced. + ``passed_release`` is a later release the pin already contains, which + core_version should name instead. + """ if report is None or not report.tag_commit or not pin: return None - if report.tag_commit == pin: + if report.tag_commit == pin and not passed_release: return None + if passed_release: + return ( + f"core_version {report.declared} is older than the code: the pin " + f"{pin[:7]} already contains release {passed_release}" + ) + where = "" + if relation is not None: + ahead, behind = relation + if ahead and not behind: + where = f", {ahead} commits after the tag" + elif behind and not ahead: + where = f", {behind} commits before the tag: core_version is later than the code" + elif ahead and behind: + where = f", off the tag's history ({ahead} ahead, {behind} behind)" return ( f"core_version {report.declared} names tag commit {report.tag_commit[:7]}, " - f"the pin resolved to {pin[:7]}" + f"the pin resolved to {pin[:7]}{where}" ) @@ -3937,7 +3963,48 @@ def _attach_version( except upstream.UpstreamError as exc: print(f"{report.name}: version check skipped: {exc}", file=sys.stderr) return - report.version_warning = version_warning(report.version, report.pin) + relation = None + passed = None + version = report.version + try: + if version and version.tag_commit and report.pin and version.tag_commit != report.pin: + relation = upstream.ancestry( + repo, version.tag_commit, report.pin, args.cache_dir, args.offline + ) + passed = _passed_release(args, repo, version, report.pin) + except upstream.RateLimitError: + raise + except upstream.UpstreamError as exc: + print(f"{report.name}: tag ancestry unread: {exc}", file=sys.stderr) + report.version_warning = version_warning(version, report.pin, relation, passed) + + +def _passed_release( + args: argparse.Namespace, + repo: upstream.Repo, + version: VersionReport | None, + pin: str | None, +) -> str | None: + """The latest release, when the pin already contains it and core_version + names another one. + + The latest tag is no witness: nightly, continuous and Pre-release tags + sort first on many forges. The latest release is a version the project + published as such. + """ + if not version or not pin or not version.latest_release: + return None + if version.latest_release == version.declared: + return None + commit = upstream.tag_commit(repo, version.latest_release, args.cache_dir, args.offline) + if not commit: + return None + if commit == pin: + return version.latest_release + relation = upstream.ancestry(repo, commit, pin, args.cache_dir, args.offline) + if relation and relation[1] == 0: + return version.latest_release + return None def _print_extras( diff --git a/scripts/upstream.py b/scripts/upstream.py index 487c9e3cf..c23eb2e38 100644 --- a/scripts/upstream.py +++ b/scripts/upstream.py @@ -742,6 +742,41 @@ def compare( return CompareResult(files, truncated, payload.get("status") == "diverged") +def _commits_ahead(payload: object, family: str) -> int | None: + """Commits the compare head carries that its base does not.""" + if not isinstance(payload, dict): + return None + if family == "github": + value = payload.get("ahead_by") + elif family == "gitlab": + value = len(payload.get("commits") or []) + else: + value = payload.get("total_commits") + return value if isinstance(value, int) else None + + +def ancestry( + repo: Repo, base: str, head: str, cache_dir: str, offline: bool = False +) -> tuple[int, int] | None: + """(ahead, behind): commits head has that base lacks, and the reverse. + + Two shas never change their history, so both answers are cached for + good. GitHub gives both counts in one answer, GitLab and Forgejo list + one direction only and are asked twice. + """ + forward = _api(_compare_url(repo, base, head), cache_dir, offline) + ahead = _commits_ahead(forward, repo.family) + if ahead is None: + return None + if repo.family == "github": + behind = forward.get("behind_by") + else: + behind = _commits_ahead( + _api(_compare_url(repo, head, base), cache_dir, offline), repo.family + ) + return (ahead, behind) if isinstance(behind, int) else None + + def find_renamed(result: CompareResult, path: str) -> str | None: """New path of a file the comparison reports as renamed.""" for change in result.files: diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index d5644cd09..f16e5f9f8 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -3171,6 +3171,55 @@ class TestCheckVersion(unittest.TestCase): result = check_version({"core_version": "v3.0.0"}, self.repo, self.dir, False) self.assertIsNone(version_warning(result, "tagsha")) + def test_warning_says_which_side_of_the_tag_the_pin_is(self): + # 52 warnings read alike: a build after its release and a pin older + # than the release it claims could not be told apart. + result = check_version({"core_version": "v3.0.0"}, self.repo, self.dir, False) + self.assertIn("4 commits after the tag", version_warning(result, "p", (4, 0))) + self.assertIn("core_version is later than the code", + version_warning(result, "p", (0, 9))) + self.assertIn("off the tag's history", version_warning(result, "p", (2, 3))) + + def test_warning_names_a_release_the_pin_already_contains(self): + result = check_version({"core_version": "v3.0.0"}, self.repo, self.dir, False) + warning = version_warning(result, "tagsha", None, "v3.1.0") + self.assertIn("already contains release v3.1.0", warning) + + +class TestAncestry(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.saved = profile_sync.upstream._http_json + + def tearDown(self): + profile_sync.upstream._http_json = self.saved + self.tmp.cleanup() + + def test_github_answers_both_directions_at_once(self): + calls = [] + + def answer(url): + calls.append(url) + return {"ahead_by": 3, "behind_by": 0} + + profile_sync.upstream._http_json = answer + repo = profile_sync.upstream.parse_repo("https://github.com/o/n") + self.assertEqual( + profile_sync.upstream.ancestry(repo, "tag", "pin", self.tmp.name), (3, 0) + ) + self.assertEqual(len(calls), 1) + + def test_gitlab_is_asked_both_ways(self): + def answer(url): + forward = "from=tag&to=pin" in url + return {"commits": [{}] * (0 if forward else 5)} + + profile_sync.upstream._http_json = answer + repo = profile_sync.upstream.parse_repo("https://gitlab.com/o/n") + self.assertEqual( + profile_sync.upstream.ancestry(repo, "tag", "pin", self.tmp.name), (0, 5) + ) + HASH_PROFILE = { "files": [ From de6f616bb9075695e38a7fa8359cb313dfb4b9c6 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:34:11 +0200 Subject: [PATCH 2/4] docs: split a docstring sentence in profile_sync --- scripts/profile_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index c53efceaf..338efbd6b 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -2225,7 +2225,7 @@ def version_warning( """Flag a declared version that names a tag away from the resolved pin. ``relation`` is the pin's (ahead, behind) count against the tag commit. - A pin after its tag is a build between two releases; a pin before it + A pin after its tag is a build between two releases. A pin before it describes code the declared release had already replaced. ``passed_release`` is a later release the pin already contains, which core_version should name instead. From 54b2d31fa3b48594366498b31ff483afcc0ffabc Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:34:33 +0200 Subject: [PATCH 3/4] docs: keep two installer pages within budget --- wiki/first-install.md | 10 +++------- wiki/troubleshooting.md | 5 ++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/wiki/first-install.md b/wiki/first-install.md index f6a62a281..e62a9596b 100644 --- a/wiki/first-install.md +++ b/wiki/first-install.md @@ -76,8 +76,7 @@ It shows your setup and its BIOS folder: ``` - **Right folder?** Go on. -- **Wrong folder?** Press Ctrl+C. Run the command again with your folder - after `--dest`: +- **Wrong folder?** Press Ctrl+C. Run it again with your folder: ```sh @@ -120,14 +119,12 @@ Add `--check` to the same command. It changes nothing. curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --check ``` -All good when every file is present and none is wrong: +All good when every file is present: ``` 109/109 present (109 verified, 0 wrong hash) ``` -An empty folder also shows `0 wrong hash`, with `0/109 present`. - ## Undo It keeps every file it replaces in a `retrobios-backup` folder, next to your @@ -138,8 +135,7 @@ BIOS folder. The end of the run prints the line that puts them back. ### No supported platform detected. At a terminal, a menu follows. Pick your setup by its number, then type its -BIOS folder, or press Enter for the usual one. A folder pasted with quotes -around it is fine. +BIOS folder, or press Enter for the usual one. With `--no-input`, or without a terminal, it stops and prints the command to use. diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md index fdbfaeb74..260544c0d 100644 --- a/wiki/troubleshooting.md +++ b/wiki/troubleshooting.md @@ -66,9 +66,8 @@ version. ## If the command fails { #installation-script-fails } -- **Network error:** try again once github.com opens. -- **`The clock reads ...`:** the date is wrong. Let it sync over Wi-Fi, or set - it, then run again. +- **Network error:** try again later. +- **`The clock reads ...`:** set the right date. - **Permission denied:** do what the message says. It names what to run. - **No setup found:** name it and its folder: From 18fc5ce53f0ce598c07dae0c3184d10c7326ee07 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:35:51 +0200 Subject: [PATCH 4/4] fix: leave platform-built pins to a reader bump_commit advanced any profile whose refs all anchored at HEAD. On a built_by profile the pin is the commit a platform compiles (mame0258, mame0278, gsplus-legacy at recalbox gsplus.mk:8-9), so HEAD is a build nobody ships under that name. The bump now refuses those profiles. --- scripts/profile_sync.py | 10 ++++++++++ tests/test_profile_sync.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 338efbd6b..97823280e 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -2927,6 +2927,16 @@ def bump_commit( if pending_recale(report, accept_changed, text): return False document = yaml.safe_load(text) + if document.get("built_by"): + # The pin is the commit a platform builds (recalbox gsplus.mk, the + # mame0258 its package fetches), not a point on a moving branch: + # HEAD is a build nobody ships under this name. + print( + f"{path.stem}: pinned to the build {', '.join(document['built_by'])} " + "ships, advanced by hand", + file=sys.stderr, + ) + return False if any(isinstance(document.get(f), dict) for f in ("source_commit", "upstream_commit")): # A pin per build mode names one revision per repository, and the # report heads only the primary one: rewriting str(dict) was a diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index f16e5f9f8..e98e8a0d0 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -2999,6 +2999,24 @@ class TestBumpCommit(unittest.TestCase): self.assertEqual(profile_sync.pending_recale(report), 0) self.assertTrue(bump_commit(self.path, report)) + def test_refused_on_a_build_a_platform_compiles(self): + # mame0258 documents the MAME Recalbox builds: libretro/mame HEAD + # is a later release, and every ref still anchors there. + self.path.write_text(SAMPLE + "built_by: [recalbox]\n", encoding="utf-8") + part = PartResult( + RefPart("a.c", 10, 12, "a.c:10-12"), "ANCHORED", None, 10, 12, [] + ) + report = ProfileReport( + name="p", repo="o/n", pin="pin", head="newhead", + entries=[ + EntryReport("a.bin", "a.c:10-12, (just a note)", "ANCHORED", [part]) + ], + counts={"ANCHORED": 1}, + ) + with contextlib.redirect_stderr(io.StringIO()): + self.assertFalse(bump_commit(self.path, report)) + self.assertNotIn("newhead", self.path.read_text()) + def test_refused_when_no_ref_reads_the_pinned_repository(self): """gam4980: every bare libretro.c ref anchored in upstream, and the bump wrote the fork's HEAD over refs that never described it."""