This document explains how the CircleCI pipeline in Heads is structured, what the cache layers mean, and how each coreboot fork saves its own modules cache.
See also: development.md, docker.md, architecture.md.
The CircleCI pipeline is optimized for two constraints:
- Avoid CircleCI workspace fan-in errors.
- Reuse expensive build outputs across pipelines without delaying unrelated board builds more than necessary.
The current layout favors a linear x86 seed chain followed by parallel board builds.
A workspace is data passed from an upstream job to downstream jobs in the same workflow run.
- Workspaces help sibling jobs in the current pipeline.
- Workspaces are downloaded fresh by downstream jobs.
- Persisting the same paths from multiple upstream jobs into one downstream job causes fan-in problems in CircleCI.
A CircleCI cache is stored for reuse by later pipeline runs in the same repository.
- Caches help future pipelines.
- Caches do not speed up sibling jobs in the same workflow run.
- Forks do not share caches with the upstream repository.
- Each x86_coreboot job saves both modules and coreboot caches for its fork.
The x86 chain is intentionally linear until a seed board has produced a usable workspace:
create_hashesx86_blobsx86_musl_cross_makex86_corebootseed jobs, one per coreboot fork- Downstream board builds for each fork, in parallel
For the coreboot 25.09 branch, the seed board is EOL_t480-hotp-maximized.
That job produces the workspace used by the other 25.09 boards in the same
workflow.
Other x86 forks follow the same pattern:
novacustom-nv4x_adlseeds thecoreboot-dasharo_nv4xforknovacustom-v560tuseeds thecoreboot-dasharo_v56forklibrem_14seeds thecoreboot-purismforkEOL_t480-hotp-maximizedseeds thecoreboot-25.09forkEOL_librem_l1umseeds thecoreboot-4.11forkUNTESTED_msi_z690a_ddr4seeds thecoreboot-dasharo_msi_z690forkUNTESTED_msi_z790p_ddr4seeds thecoreboot-dasharo_msi_z790fork
The downstream build jobs for each family consume the workspace from the
relevant seed job instead of rebuilding the fork toolchain from scratch.
The ppc64 chain mirrors x86:
create_hashesppc64_musl_cross_make- builds musl-cross-make toolchain, saves cacheppc64_coreboot- builds coreboot-talos_2 fork, saves cache- (no downstream boards - only one ppc64 board exists)
The x86 pipeline uses hierarchical cache layers:
-
{arch}-musl-cross-make-nix-docker-heads-{hash}- Base toolchain (GCC + musl = musl-cross-make)
- Paths:
build/{arch}/musl-cross-make-*,crossgcc/{arch},install/{arch},packages/{arch}
-
{arch}-coreboot-musl-cross-make-nix-docker-heads-{hash}-{coreboot_dir}- Includes musl + coreboot toolstack
- Paths:
build/{arch}/{coreboot_dir},build/{arch}/musl-cross-make-*,crossgcc/{arch},install/{arch},packages/{arch}
-
{arch}-modules-coreboot-musl-cross-make-nix-docker-heads-{hash}-{coreboot_dir}- Includes coreboot + musl + all built modules (FULL)
- Paths:
build/{arch},install/{arch},crossgcc/{arch},packages/{arch}
-
{arch}-blobs-nix-docker-headsare handled separately
Cache key naming: {arch}-{layer}-nix-docker-heads-{hash}[-{fork}]
The cache key naming shows the dependency chain: each layer includes everything from the layers below it.
Restore order (most complete to least):
1. {arch}-modules-coreboot-musl-cross-make-nix-docker-heads-{modules_hash}-{coreboot_dir}
2. {arch}-coreboot-musl-cross-make-nix-docker-heads-{coreboot_hash}-{coreboot_dir}
3. {arch}-musl-cross-make-nix-docker-heads-{musl_hash}
Each x86_coreboot job saves both:
- modules cache (full build state)
- coreboot cache (fork-specific toolstack)
The current pipeline behavior is:
- It uses explicit jobs for cache hashing, blob preparation, x86 musl seed, x86 coreboot forks (each saves both modules and coreboot caches), generic board builds, and the single ppc64 Talos II build.
- It uses a pinned
heads-dockerexecutor so the toolchain environment is stable across jobs. - It clears only
build/<arch>/log/*before a build, not the restored build trees themselves. - It keeps x86 blob preparation separate from toolchain and firmware builds.
- It keys x86 coreboot caches by fork so one fork cannot restore another fork's build tree.
- It restores the largest valid cache first, because CircleCI stops at the first matching key.
- It stores
install/<arch>together with the compiler and package trees so a restored musl toolchain still has its sysroot. - It refreshes restored
.configuredand.buildstamps before invokingmake, so fresh checkout mtimes do not trigger a redundant rebuild of an already restored musl-cross-make tree. - It decouples ppc64 into musl-cross-make and coreboot jobs (like x86) so each saves its cache immediately rather than at the end of a long combined build.
When changing .circleci/config.yml, update this document by answering these
questions in order:
- Did the job graph change?
Update the
x86 pipeline shapesection and the seed-board list. - Did a cache key, restore order, or saved path change?
Update
Cache layersandWhy musl could rebuild after a cache hit. - Did the change alter current runtime behavior or restore/build semantics?
Update
Current pipeline details. - Did the change affect the maintenance workflow itself? Update this section too.
If you cannot summarize the change in one of those sections, the document is missing a section and should be extended rather than worked around.
Use this map when modifying the pipeline:
- Add or remove a cache hash input:
edit
create_hashesin.circleci/config.ymland updateCache layershere. - Add or remove x86 blob preparation:
edit
x86_blobsand updatex86 pipeline shapeplusCache layers. - Add or remove an x86 coreboot fork seed:
edit the
x86_corebootworkflow entries and update the seed-board list inx86 pipeline shape. - Add or remove downstream boards for a fork:
edit the
buildworkflow entries and verify the seed dependency still points to the correct fork seed. - Change what makes musl reusable:
update the save/restore paths in
.circleci/config.ymland re-check the explanation inWhy musl could rebuild after a cache hit. - Change ppc64 behavior:
edit
ppc64_musl_cross_makeand/orppc64_corebootand re-check bothCache layersand the ppc64 chain description.
These are the current rules worth preserving unless a deliberate design change:
- Only one job at a time should persist a given workspace chain.
- Blob download is separate from x86 toolchain and coreboot builds.
- Each fork saves both modules and coreboot caches.
- x86 and ppc64 restore lists should prefer the largest valid cache first.
- Same-workflow cache misses can be expected when the broad key is being published during that workflow; this should improve on the next pipeline.
- Musl reuse requires both
crossgcc/<arch>andinstall/<arch>. - Each coreboot fork has its own cache keyed by
{coreboot_dir}to prevent cross-fork contamination. - ppc64 now uses decoupled musl-cross-make + coreboot jobs, each saving cache immediately.
Each x86_coreboot job (the first board for each coreboot fork) saves both:
- modules cache - full build state including all built modules
- coreboot cache - fork-specific coreboot toolstack
This means every fork is self-sufficient:
- First board of fork builds everything and saves both caches
- Downstream boards in same fork restore full modules cache
- No separate cache publication job needed
Original problem: Even when cache is restored, musl-cross-make was rebuilt
because the Makefile only checked if CROSS env var was set, not if the
compiler actually existed on disk.
Fix: The musl-cross-make module now uses wildcard to auto-detect if
crossgcc/<arch>/bin/<triplet>-gcc exists. If found, it sets CROSS and uses
the --version path (no rebuild). If not found, it builds from scratch.
The build logic also requires both:
- the compiler binaries under
crossgcc/<arch> - the installed sysroot under
install/<arch>
If the cache only restores the compiler tree but not the installed headers and libraries, the generic module build rules still have missing outputs and musl is rebuilt.
That is why the current branch stores install/x86 and install/ppc64 in the
musl and coreboot cache layers, not only in the broad modules cache.
There is a second reuse problem to watch for: restored stamp files can be older
than freshly checked-out source files in CI. When that happens, GNU Make can
decide that .configured and then .build are stale even though the restored
outputs are complete. The current CI job refreshes restored .configured and
.build timestamps before invoking make so restored musl-cross-make trees are
reused instead of spending several minutes rebuilding for timestamp reasons
alone.
Cold runs are still expensive because:
- Downstream jobs still download the upstream workspace chain.
- A fork starts with cold CircleCI caches because caches are repository-scoped.
- CircleCI restores only the first matching key, so an unexpectedly narrow hit can still leave later work to do if the cache contents are incomplete.
- Saving a large cache still requires uploading the selected directories.
Adjust the model only if one of these is true:
- The seed board is no longer representative of the fork workspace.
- The persisted workspace is too large and should be split further.
- The modules cache key is too broad and causes low reuse.
- CircleCI changes workspace or cache semantics.
- Each coreboot fork saves both modules and coreboot caches, eliminating single-point-of-failure.
- Cache key naming shows the dependency chain: modules includes coreboot includes musl.
- Restore ordering must be explicit and largest-first. If two keys are valid, CircleCI uses the first match only.
- Restored build markers can be older than fresh checkout files. Without stamp refresh, Make can rebuild musl-cross-make even after a correct modules-cache restore.
- For ppc64, the middle fallback
coreboot+muslimproves reuse whenmodulesis absent but a richer cache than plainmuslexists. - Each x86 coreboot fork saves its own modules cache keyed by
{coreboot_dir}. This prevents cross-fork contamination while enabling fork-specific reuse. - x86 coreboot forks avoid generic cross-fork fallback keys to prevent restoring another fork's coreboot tree.
- ppc64 uses decoupled musl-cross-make + coreboot jobs. Each saves its cache immediately rather than at the end of a long combined build.
- musl-cross-make module auto-detects existing crossgcc using wildcard check, skipping rebuild when compiler already exists from cache.
Cold cache run on new pipeline structure:
- x86-musl-cross-make: 30 min (vs baseline 14.5 min) - slower due to new overhead
- ppc64-musl-cross-make: 16 min (vs baseline 18 min) - slightly faster
The Make Board step takes longer in new pipeline because it persists more data after build (build/, install/, crossgcc/, packages/). The real test is second run when cache exists - verifies if wildcard fix skips rebuild.
Cache key hashes intentionally exclude .circleci/config.yml to prevent cache
invalidation on CircleCI configuration changes. Add back once cache model is stable
(see TODO in .circleci/config.yml create_hashes job).
Key files included in hashes:
all_modules_and_patches.sha256sums:./Makefile,./flake.lock,./patches/,./modules/coreboot_musl-cross-make.sha256sums:./flake.lock,./modules/coreboot,./modules/musl-cross-make*,./patches/coreboot*musl-cross-make.sha256sums:./flake.lock,./modules/musl-cross-make*