Skip to content

build: order the Room schema snapshot after every KSP round, not just the test ones - #1712

Merged
ryanbr merged 1 commit into
mainfrom
gradle-room-schema-ordering
Aug 28, 2026
Merged

ryanbr merged 1 commit into
mainfrom
gradle-room-schema-ordering

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Addresses #1711. One line, plus the comment explaining why the old scope was too narrow.

The cause

room.schemaLocation is set once, outside any variant block (app/build.gradle.kts:160-162), so every variant's KSP round writes that directory. syncRoomSchemaSnapshot reads it while declaring a relationship with only some of its producers:

dependsOn(tasks.matching { it.name == "kspFullDebugKotlin" })                                    // one variant
mustRunAfter(tasks.matching { it.name.startsWith("ksp") && it.name.contains("UnitTest") })       // some rounds

The dependsOn narrowness is deliberate and stays — the schema derives from the entities alone, so every round writes byte-identical JSON, and depending on all of them would cost four KSP rounds per test run.

The ordering line was the gap. It covered the hazard that had actually been observed (unit-test KSP clearing the snapshot after Sync copied it), but kspFullReleaseKotlin writes the same directory and wasn't covered. It's normally absent from the graph — which is why this never surfaced — but lintVitalFullRelease in the same invocation pulls it in, and Gradle then fails on the undeclared producer/consumer pair with an error that reads like a Room problem.

The change

mustRunAfter(tasks.matching { it.name.startsWith("ksp") && it.name.endsWith("Kotlin") })

A strict superset of the old matcher — I checked name by name across the variant/test task names this project generates, and nothing the old pattern matched fails the new one. The original guard is intact.

Verified, not reasoned

mustRunAfter silently forcing extra work is the regression worth fearing here, so it is measured rather than argued:

check result
previously-failing 3-task command BUILD SUCCESSFUL (fails on main)
KSP rounds in that run kspFullDebugKotlin, kspFullDebugUnitTestKotlin, kspFullReleaseKotlin — once each
plain testFullDebugUnitTest only the debug + unit-test rounds, no release round
unit suite 4,725 tests, 0 failures

That third row is the one that matters: it is the exact property the neighbouring comment protects, and it holds.

Parity

None required. Room/KSP schema generation is Android-only — there is no room.schemaLocation or generated-schema step on the Swift side. The schema_oracle.json fixture this pipeline feeds does have a byte-identical Swift twin read by SchemaOracleTests, but that is committed data and build wiring cannot affect it.

… the test ones

Reported as #1711. Running release lint in the same Gradle invocation as the debug test tasks failed
the build:

  Task ':app:syncRoomSchemaSnapshot' uses this output of task ':app:kspFullReleaseKotlin'
  without declaring an explicit or implicit dependency.

room.schemaLocation is set once, outside any variant block, so EVERY variant's KSP round writes that
directory. syncRoomSchemaSnapshot reads it while declaring a relationship with only some of its
producers: dependsOn one variant, and mustRunAfter the UnitTest rounds. The dependsOn narrowness is
deliberate and stays — the schema derives from the entities alone, so every round writes identical
JSON and depending on all of them would cost four KSP rounds per test run.

The ordering line was the gap. It covered the hazard that had been observed, unit-test KSP clearing
the snapshot after Sync copied it, but kspFullReleaseKotlin writes the same directory and was not
covered. It is normally absent from the graph, which is why this never surfaced; pull it in and
Gradle fails on the undeclared pair, with an error that reads like a Room problem.

Broadened to every ksp*Kotlin task. That is a strict superset of the old matcher — checked name by
name, nothing the old one matched fails the new one — so the original guard is intact.

Verified rather than reasoned, since mustRunAfter silently forcing extra work is the regression worth
fearing:

  - the previously-failing three-task command now succeeds
  - it runs kspFullDebugKotlin, kspFullDebugUnitTestKotlin and kspFullReleaseKotlin exactly once each
  - a PLAIN testFullDebugUnitTest still runs only the debug and unit-test rounds, no release round, so
    the one-variant optimisation the neighbouring comment protects is untouched
  - 4,725 tests, 0 failures

No parity work: Room/KSP schema generation is Android-only. The schema_oracle.json fixture this
pipeline feeds does have a byte-identical Swift twin, but that is committed data and build wiring
cannot affect it.
@ryanbr
ryanbr merged commit 15a92da into main Aug 28, 2026
4 checks passed
@ryanbr
ryanbr deleted the gradle-room-schema-ordering branch August 28, 2026 12:05
ryanbr added a commit that referenced this pull request Aug 28, 2026
Follow-up to #1712, which is where the need showed up.

That change added a matcher spelled startsWith("ksp") && endsWith("Kotlin") a few lines below one
that already existed and read almost identically — and the existing one deliberately INVERTS on the
UnitTest/AndroidTest rounds. I did not notice while writing it, which is the argument for this rather
than against it: two near-identical predicates meaning opposite things is a trap regardless of who
reads them next.

The shared half is now named, and its comment states the opposition instead of leaving it to be
diffed by eye. isMainSourceSetKspTask excludes those rounds so Gradle is not told they output a
directory they never write and then clears it out from under the round that did;
syncRoomSchemaSnapshot's mustRunAfter includes them because they still race the Sync.

Pure refactor. Both predicates were checked name by name against every KSP and compile task name this
project generates, and neither changes for any of them.

Verified anyway, because the failure mode here is a missing schema export rather than a compile
error, so it does not announce itself: the three-task command from #1712 still succeeds, the debug,
unit-test and release rounds still run exactly once each, 4,725 tests pass, and SchemaOracleTest —
the test that fails when the export goes missing — is 4 for 4. Run before and after the comment
relocation below, with identical results.

One note on how it got here: the first version inserted the new declaration BETWEEN the pre-existing
comment block and the function it documents, orphaning the load-bearing exclusion rationale onto the
wrong function. Caught in review and moved above the block. Worth recording because it is the exact
hazard this change exists to reduce, and because a comment attached to the wrong function is
invisible to every check in CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant