From a0b2263baeba66e9eaf4efc8e527e5d98a32641e Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:50:53 +0200 Subject: [PATCH] feat: let a profile say its pin is held on purpose nethersx2-turnip-classic documents the shim rules of the revision it pins, and says so in prose: the classic build is that revision. Compared to HEAD it reported the same two changes every pass, and recaling would have repointed its refs at rules this build never had. pin_frozen says the freeze is deliberate, and the profile is then judged against its own revision, exactly as one pinned to a superseded tag already was. Recale and bump are refused for it, which is what makes the declaration safe to trust. --- emulators/nethersx2-turnip-classic.yml | 1 + schemas/emulator.schema.json | 4 +++ scripts/profile_sync.py | 9 ++++++- tests/test_profile_sync.py | 37 ++++++++++++++++++++++++++ wiki/profiling.md | 1 + 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/emulators/nethersx2-turnip-classic.yml b/emulators/nethersx2-turnip-classic.yml index d220a779..940066ba 100644 --- a/emulators/nethersx2-turnip-classic.yml +++ b/emulators/nethersx2-turnip-classic.yml @@ -6,6 +6,7 @@ source: "https://github.com/nckstwrt/NetherSX2-Turnip" upstream: "https://github.com/Trixarian/AetherSX2" author: "nckstwrt" profiled_date: "2026-08-12" +pin_frozen: "the classic build is this revision. v0.7 ships a newer shim, and its rules are described in the notes; recaling would repoint these refs at code this build never had." source_commit: "8e17e584884e19b0a912f1988730d576d6d5aa84" upstream_commit: "1d1f795dfc70e38da67724c7298779260f513889" core_version: "2.2n-3668-Turnip-v0.7" diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index eef582be..9f6ba90d 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -598,6 +598,10 @@ "type": "string", "description": "Branch the port was read from, when it is not the default one." }, + "pin_frozen": { + "type": "string", + "description": "Why source_commit is held on purpose rather than followed. The profile is then judged against its own revision, like one pinned to a superseded tag: comparing it to HEAD says nothing and recaling would repoint its refs at code the build it documents never had." + }, "upstream_gone": { "type": "string", "description": "Why the declared upstream can no longer be reached, when that is settled rather than transient. Records the death instead of leaving the profile failing every pass; profile_sync reports the contradiction if the forge answers again." diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index d3daa2fd..be662886 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -1272,7 +1272,14 @@ def build_report( report.repos = [v.repo.slug for v in views] report.pin, report.pin_origin = primary.pin, primary.origin report.head = primary.head - report.pinned_tag = detect_pinned_tag(profile, views, cache_dir, offline) + # A profile can hold its pin on purpose, documenting a build that is that + # revision. Comparing it to HEAD says nothing and recaling would repoint + # its refs at code the build never had, so it is judged like a frozen tag: + # against itself. + frozen = str(profile.get("pin_frozen") or "").strip() + report.pinned_tag = frozen or detect_pinned_tag( + profile, views, cache_dir, offline + ) # A profile carrying no source_ref still has a pin worth writing and a # version worth checking, so the revisions above are resolved first. diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index d5e281f9..5dd870db 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -2160,6 +2160,43 @@ class TestWriteDryRun(unittest.TestCase): self.assertEqual(self.path.read_text(), before) +class TestDeliberatelyFrozenPin(TestBuildReport): + """A pin held on purpose is judged against itself, like a frozen tag. + + nethersx2-turnip-classic documents the shim rules of the revision it + pins and says so: recaling would repoint its refs at rules the classic + build never had. Without a way to say the freeze is deliberate, it + reports the same drift every pass and nobody can act on it. + """ + + def _frozen(self, reason="the classic build is this revision"): + profile = self._profile(["a.c:2"]) + profile["source_commit"] = "pinned" + profile["pin_frozen"] = reason + return profile + + def test_a_frozen_pin_is_checked_against_itself(self): + self.files[("pinned", "a.c")] = ["x", "the cited line"] + self.files[("headsha", "a.c")] = ["y"] * 40 + report = build_report("test", self._frozen(), self.dir) + self.assertTrue(report.pinned_tag) + self.assertEqual(report.entries[0].parts[0].status, "ANCHORED") + + def test_a_frozen_profile_is_never_recaled(self): + self.files[("pinned", "a.c")] = ["x", "the cited line"] + report = build_report("test", self._frozen(), self.dir) + path = Path(self.dir) / "p.yml" + path.write_text(SAMPLE, encoding="utf-8") + self.assertEqual(rebase_refs(path, report), []) + self.assertFalse(bump_commit(path, report)) + + def test_an_ordinary_profile_is_unaffected(self): + self.files[("pinsha", "a.c")] = ["x", "hit"] + self.files[("headsha", "a.c")] = ["pad", "x", "hit"] + report = build_report("test", self._profile(["a.c:2"]), self.dir) + self.assertFalse(report.pinned_tag) + + class TestProseSpacingIsNotADifference(unittest.TestCase): """A recale keeps the author's spacing; the check has to allow for it. diff --git a/wiki/profiling.md b/wiki/profiling.md index 3fb56ffc..2308e202 100644 --- a/wiki/profiling.md +++ b/wiki/profiling.md @@ -353,6 +353,7 @@ which CI validates every profile against. | `min_size`, `max_size` | size range when the code accepts a range | | `md5`, `sha1`, `crc32`, `sha256` | expected hashes from source code | | `known_hash_adler32` | expected Adler-32 hash (Dolphin's DSP ROMs); pair with `adler32_byteswap` when the code hashes 16-bit byte-swapped data | +| `pin_frozen` | why `source_commit` is held on purpose rather than followed. The profile is then judged against its own revision, like one pinned to a superseded tag | | `upstream_gone` | why the declared upstream can no longer be reached, when that is settled rather than transient. Records the death instead of leaving the profile failing every pass; profile_sync reports it as declared, and reports the contradiction if the forge answers again | | `source_mirror` | a repository carrying the same tree, consulted after `source` and `upstream`. What keeps a profile checkable once its own forge stops answering | | `validation` | checks the code performs: `size`, `crc32`, `md5`, `sha1`, `adler32`, `signature`, `crypto`. Can be a list or dict `{core: [...], upstream: [...]}` for divergent checks |