fix(build): generate PLUGIN_VERSION from the project version — one source of truth (#192) - #202
Conversation
…urce of truth (#192) The project version lived in six places. The 0.3.4 prep bumped five and missed `KPlusPlusCompilerGradlePlugin.PLUGIN_VERSION`, a `const val` in plugin source that no build can check: a wrong-but-well-formed version string is not a compile error, so `-p compiler build` (apiCheck included) stayed green. Had it shipped, that constant keys `~/.gradle/kplusplus/tools/$PLUGIN_VERSION/`, so every upgrading consumer would have silently kept running the previous release's tool binary. Option 1 from the issue — remove the bug class rather than detect it: * `version` in the REPO ROOT gradle.properties is now the ONLY declaration. The compiler build is a separate build and cannot read the root's project version, so it reads the file (the same cross-build reach by relative path it already uses for the root's version catalog). The three per-module `version = "…"` lines (and their duplicated `group`) are gone. * `PLUGIN_VERSION` is GENERATED into the plugin's main source set from that version by `:kplusplus-compiler-gradle:generatePluginVersion`. There is no literal left to forget. A `const val` is inlined at compile time and its package is not relocated, so this is unaffected by the #194 shading. Part two, for the deliberately LAGGING references: the root `pluginManagement` pin and the two `samples/` pins name the last PUBLISHED release and move together in the post-release migration, so there is no single value to derive them from (the samples must stay copy-pasteable for a real consumer). `verifyConsumedPluginPins`, wired into `-p compiler check`, instead asserts the two properties that hold in BOTH phases: all three name the same version, and none is ahead of the declared one. That catches a half-done migration — how samples/minimal went stale for a whole release cycle (#195). `apiDump` regenerated: the companion's `PLUGIN_VERSION` static field leaves the ABI dump. It is a member of the plugin's implementation class, in a *private* companion, instantiated by Gradle from its plugin id — no consumer compiles against it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLWsoRAe7f1RdQrYJ2ZXqm
|
Reviewed: 7d6ed71 Independent runtime verification of the mechanism (own worktree Caveat found and worked around: a first pass at "publish 0.3.4 to mavenLocal, then build With that isolation:
All test artifacts cleaned up afterward: Continuing with the |
|
Reviewed: 7d6ed71 Verdict: MERGE1. Single-source, my own grep
The three per-module 2 & 3. Runtime correctness + shading — verified independently, see the earlier comment on this PRShort version: my first attempt at "publish 0.3.4 to mavenLocal, build a from-published consumer" gave a false positive from a stale Gradle module-cache entry (not a 4. The
|
| gate | result |
|---|---|
:krapper:nativeTest |
304/0 (summed from JUnit XML) |
:featuregen:nativeTest |
195/0 |
:cppfixture:nativeTest |
2/0 |
:krapper:noncopyableDeterminismCheck |
8/8 PASS |
-p compiler build (incl. apiCheck) |
green, fresh (non-cached) run |
:featuregen:kplusplusSync / :cppfixture:kplusplusSync |
both byte-identical (git status clean after each) |
ktlintCheck --rerun-tasks (root) |
green |
-p compiler ktlintCheck |
not a task — the compiler build has no ktlint plugin wired (pre-existing, matches prior memory re: krapper_parse .kt ktlint debt); not a gap introduced by this PR |
No defects found. This is a real fix that removes the bug class rather than adding a detector, verified correct at runtime (not just compile time) with an independently-constructed test that specifically avoided the caching trap that could have masked a bad result.
Merging.
monkopedia-reviewer
left a comment
There was a problem hiding this comment.
Reviewed: 7d6ed71 — see detailed comments above. Full gate green, mechanism verified genuinely single-source and runtime-correct including through the #200 shading, .api change judged legitimate, demonstration reproduced (bug is now unconstructible), lagging-pin guard independently reproduced in all three modes. Merging.
One line — `version` in the root `gradle.properties`. That is the whole change, and it is the direct payoff of #192/#202: the five other version declarations (three per-module `version = "..."` lines and the hand-written `PLUGIN_VERSION` literal in plugin source) no longer exist. During 0.3.4 prep the same operation touched six places, five were bumped, and the sixth was caught only by a reviewer's grep. The three consumed pins (root `settings.gradle.kts`, `samples/minimal`, `samples/multiproject/bindings`) DELIBERATELY stay at 0.3.4. The repo self-hosts one release behind, so they can only move once 0.3.5 exists on the Portal. `verifyConsumedPluginPins` (also from #202) was run and accepts this as a legal one-release lag. ## What 0.3.5 ships * **#194 / #200 — the reason for this release.** 0.3.4 is BROKEN for multi-project consumers: the plugin picked up whatever kotlinx-coroutines the root buildscript classloader owned (1.8.0 via kotlin-compiler-runner) rather than the 1.11.0 it resolves, and ktor-io calls a method coroutines 1.9+ moved onto the interface -> `NoSuchMethodError: Job.invokeOnCompletion$default`. The plugin's ksrpc/ktor/coroutines runtime is now shaded and relocated under `com.monkopedia.kplusplus.compiler.shaded`, so it is immune to the buildscript classloader's contents. `samples/multiproject` is a new canary reproducing the exact topology that shipped broken. * **#196 / #199** — drop-ledger diagnostics: structurally-expected drops (DEDUP, the operator== capability gate) demoted to INFO so the capped WARNING budget carries actionable entries. Totals still reported per-severity. * **#195 / #197** — `samples/minimal:nativeTest` repaired (it had not compiled since the context-parameter migration) plus the repo's first PR-triggered CI. * **#198 / #201** — that CI now asserts a non-zero test count from the JUnit XML and uploads it; previously it went green whether 3 tests ran or none. * **#192 / #202** — this release's own prep, made a one-line operation. ## Verification Full gate re-run on main @ `a65d066` immediately before the bump: krapper 304 / 0 featuregen 195 / 0 cppfixture 2 / 0 noncopyableDeterminismCheck green BUILD SUCCESSFUL in 4m 39s (34/34 tasks executed) `-p compiler check` green at 0.3.5, including `apiCheck` and the new `verifyConsumedPluginPins`. ## Follow-up, immediately after this publishes The post-release migration is one change: bump the three consumed pins 0.3.4 -> 0.3.5 AND delete the root `build.gradle.kts` `apply false` workaround (#193/#194), whose comment names exactly this condition. It cannot go earlier — `release.yml` builds `:krapper` on main to cut the release, and until 0.3.5 exists the pin cannot move off the unshaded 0.3.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLWsoRAe7f1RdQrYJ2ZXqm
Closes #192.
The problem
The version lived in six places. The 0.3.4 prep bumped five and missed
KPlusPlusCompilerGradlePlugin.PLUGIN_VERSION— aconst valin plugin source, not abuild file. Nothing catches that: a wrong-but-well-formed version string is not a compile
error, so
-p compiler build(apiCheck included) stays green. Shipped, it would have madeevery upgrading consumer silently keep running the previous release's tool binary out of
~/.gradle/kplusplus/tools/0.3.3/.Mechanism — option 1 (remove the bug class), no fallback needed
versionin the repo-rootgradle.propertiesis now the only declaration. Thecompilerbuild is a separate build (not anincludeBuildof the root), so it cannotread the root's project version — it reads the file, via
providers.fileContents(...).That cross-build reach by relative path is already how this build shares the root's
version catalog (
compiler/settings.gradle.kts). The three per-moduleversion = "…"lines (and their duplicated
group) are deleted;allprojectscovers them.PLUGIN_VERSIONis generated into the plugin's main source set by:kplusplus-compiler-gradle:generatePluginVersion. There is no literal left to forget.Six places → one.
Shading (#200) — checked, unaffected
A
const valis inlined into its call sites at compile time, and the constant's own package(
com.monkopedia.kplusplus.compiler.gradle) is not inshadedPackages, so relocation cannottouch it. Confirmed by inspecting the built shadow jar rather than assuming — the cache
path is present, inlined, unrelocated:
apiDumpregeneratedThe companion's
PLUGIN_VERSIONstatic field leaves the ABI dump (one line). It is a memberof the plugin's implementation class, in a private companion, instantiated by Gradle
from its plugin id — nothing compiles against it. The new generated constant is
internal,so BCV correctly keeps it out of the dump.
Demonstration — the guard was made to fire
The 0.3.4 bug is now unrepresentable, so there is no stale literal left to fail on; the
demonstration is that a bump propagates with no source edit, and that the residual
hand-written version references do fail loudly.
A. Bump only
gradle.properties(0.3.4 → 0.3.5), touch nothing else.B. …and it is correct at RUNTIME, not just at compile time. Full local release dry run at
that bumped version (
docs/releasing.md), against a from-published consumer, with nopre-existing 0.3.5 cache directory:
samples/multiproject(:bindings:build, the #194 multi-project/shaded shape) also buildsgreen against the same 0.3.5 publish. All demo state reverted afterwards (
gradle.properties,the mavenLocal 0.3.5 artifacts, the 0.3.5 tool cache dir).
C. The lagging-pin guard fires — three runs, real output:
Half-done post-release migration (root settings bumped, samples missed) — fails via
check:A pin ahead of the declared version:
A legal lag (declared 0.3.4, all three pinned 0.3.3) —
BUILD SUCCESSFUL, no false positive.What I did about the lagging references — and what I deliberately did not
The root
pluginManagementpin and the twosamples/pins legitimately name the previouspublished release between the release-prep bump and the post-release migration. I did not
dedupe them into one derived value:
samples/*are standalone builds whose whole point is tolook like a real consumer's build, and making them read the repo root's
gradle.propertieswould destroy that. Nor did I assert "== previous release" — the repo has no machine-readable
notion of it, and inferring one from git tags is exactly the brittle guard the issue warns
against.
Instead
verifyConsumedPluginPinsasserts the two properties that hold in both phases —all three agree, and none is ahead of the declared version — so a half-finished migration
fails immediately. That is the failure that actually happened:
samples/minimalsat on astale pin for a whole release cycle (#195).
Also not done: option 3 (a docs checklist).
docs/releasing.mdstep 1 is updated to saythere is now exactly one line to bump, which is a reduction of the checklist, not an
addition to it.
Verification
:krapper:nativeTest:featuregen:nativeTest:cppfixture:nativeTest:krapper:noncopyableDeterminismCheckkplusplusSyncgit status)-p compiler build(incl.apiCheck)--rerun-taskson the rebased treepublishToMavenLocal→samples/minimal:nativeTestsamples/multiproject:bindings:buildRebased onto
c8ab6e2(#198) after it landed; re-verified there.