ADR-0038: The superbuild download is not a hang
Context
The first ./gradlew build on a fresh checkout was reported as hung, and killed.
It was not hung. It was cloning.
The superbuild fetches five upstreams — Blend2D, AsmJit, Yoga, HarfBuzz and SDL3
— with FetchContent. Shallow, but still about 330 MB of working tree, of which
HarfBuzz alone is 161 MB and SDL3 116 MB. On the machine that prompted this
record that was three minutes, and CMake printed nothing for the whole of it.
Two defaults conspire:
FETCHCONTENT_QUIETisTRUEby default, which swallows the populate step’s output.git clonewrites no progress when stdout is not a terminal, and under Gradle’sExecit never is.
So the console showed > Task :natives:cmakeConfigure and sat there. There is no
way for a newcomer to distinguish that from a deadlock, and the reasonable
response — Ctrl-C — is the one that guarantees the next attempt starts over.
It started over more often than it needed to for a second reason. FetchContent
puts its clones in _deps under the CMake binary directory, which here lives
inside natives/build/. ./gradlew clean is therefore a 330 MB re-download, and
nothing said so.
Measured afterwards on that same machine, with the sources already present: configure 32.8 s, compile and link 27.1 s. The build is a minute of work behind a download that can be five times longer than it and says nothing.
Decision
Make the download audible, and stop discarding it.
Audible. FETCHCONTENT_QUIET FALSE in the superbuild’s CMakeLists.txt, and
GIT_PROGRESS TRUE on every declaration — the latter passes --progress, which
is what makes git report into a pipe. Before the silence rather than after it,
cmakeConfigure logs at lifecycle what is about to happen, how big it is, where
it is going, and roughly how long it takes. That message prints only on a cold
cache, so it is information the first time and not noise the twentieth.
Kept. Gradle points FETCHCONTENT_BASE_DIR at natives/.deps/<target-id>,
outside build/, so clean no longer touches it. :natives:cleanNativeDeps
discards it on purpose. Per target rather than one shared directory, because the
base directory also holds the generator-specific <name>-build and
<name>-subbuild trees; two targets sharing one would fight over them.
Two corrections came out of the same reading:
The pinned refs are task inputs now. They reached CMake as -D arguments but
were declared nowhere, so cmakeConfigure was up to date after a version bump in
gradle/libs.versions.toml and the build quietly kept building the old revision.
They are an inputs.property each.
Superseded by ADR-0035, which landed in parallel with this and was merged after it. The refs no longer reach CMake as
-Darguments at all — the superbuild readsgradle/libs.versions.tomlitself — so there is nothing left to mirror into aninputs.property. The catalog is declared as an input tocmakeConfigureinstead, which carries the same guarantee for one file rather than six properties, and carries it for a ref this build file has never heard of. The bug described above, and the reasoning about it, stand; only the mechanism changed.
cmakeConfigure no longer declares the whole work directory as its output.
That directory contained _deps — 7,597 files — and Gradle fingerprinted all of
it on both sides of every invocation. Its real outputs are CMakeCache.txt and
build.ninja. Moving _deps out of build/ shrinks the tree anyway; declaring
the two files that actually mean “configured” is the honest description.
Alternatives considered
Leave it and document it in the README. The README already says the native build needs CMake, Ninja and a toolchain. It did not say the first run downloads a third of a gigabyte in silence, and adding a sentence would help only the people who read documentation before their first build — not the people who have already hit Ctrl-C. The fix belongs where the silence is.
Vendor the sources, or commit a lockfile of tarballs. Removes the clone entirely and makes builds reproducible offline. It also puts 330 MB of other people’s code in the history, and ADR-0012 already fixes reproducibility at the pinned-ref level. Not worth the repository weight for a one-time cost.
Release tarballs instead of git clones. Materially smaller than a shallow
clone for HarfBuzz and SDL3, whose bulk is test fixtures. It does not work for
Blend2D and AsmJit, which are pinned at master and so have no tarball; a
superbuild that fetched two upstreams one way and three another would be harder
to read than what it saves. Worth revisiting when those two get pinned to
releases.
A shared cache across targets, in ~/.cache or the Gradle user home. One
copy per machine instead of one per checkout. Rejected for the -build and
-subbuild collision above, and because a cache outside the working tree is a
cache people forget they have — natives/.deps/ is visible next to the thing it
belongs to, and .gitignore covers it.
Turn the whole thing off by default and make the native build opt-in.
-Pgoldberry.skipNative=true already exists for Java-only contributors. Making
it the default would mean the ordinary ./gradlew build no longer tests what it
claims to test, which is the trade ADR-0012’s wiring deliberately made the other
way.
Consequences
The configure step is noisier. It prints git’s progress meter and a paragraph of explanation on a cold cache, where before it printed one line. That is the point, and it is the cost: anyone parsing configure output now has more to skip.
clean no longer means clean. natives/.deps/ survives it, which is a small
surprise in the other direction — mitigated by naming cleanNativeDeps in the
message that creates the cache, but a surprise nonetheless. A stale cache is
still correct, because FetchContent re-checks the ref against the recorded
stamp; it only costs disk.
Version bumps in gradle/libs.versions.toml now actually re-configure. That is a
bug fix, and it means the next bump will be slower than the last one was — it
will do the work it was previously skipping.
The three-minute figure is one machine on one connection, and the message says “typically a few minutes” rather than a number, because the honest answer is that it depends on the link. What is not machine-specific is the ratio: the download dominates, and the build everyone assumes is slow takes 27 seconds.