The shared Linux toolchain image for pace, morph, Lightweight, and
formula-cpp.
It includes GCC 14, Clang 22, CMake, Ninja, pace-cc, and pace-kv. It is
published to GHCR by .github/workflows/publish.yml. Consumers use the moving
latest tag, which every successful master build advances.
Consumers reference ghcr.io/lastrada-software/cpp-ci:latest and never a
pinned digest. Read the usage guide before migrating a workflow.
In particular, every CMake configure must enable the cache with
-DUSE_COMPILER_CACHE=ON and set four pace-cc environment variables
(PACE_ADDR, PACE_TOKEN, PACE_SOURCE_DIR, PACE_BINARY_DIR). An
authenticated pace-kv alone does not distinguish cache readers from writers.
The workflow resolves an immutable Ubuntu base-image digest and the exact
pace master commit before building. It runs on pull requests (build and
smoke test only), on pushes to master, and once daily. Each master build is
pushed under its <sha> tag and then moves the latest tag to it. Consumers
use latest, so new images reach all CI runs automatically. The image labels
record both revisions.
Pull requests build and smoke-test the image without pushing it.
The current image is the toolchain foundation. The seed cache directory is created empty. The seed warmer is not yet implemented in this repository, so no consumer currently gets a seed. The design is below.
Status: proposal, reviewed, not implemented.
Every CI job on every Linux runner in the organization starts with a compile cache already populated for the presets it builds, so compile results are not recomputed per job.
The manifest is warm-targets.yml: 27 (project, preset)
pairs.
| Project | Presets |
|---|---|
pace |
9: gcc debug/release/gate, clang debug/release/gate/tracy, clang asan-ubsan/tsan |
morph |
8: gcc debug/release, clang debug/release/linux-everything, clang asan/tsan/ubsan |
Lightweight |
6: gcc debug/release, clang debug/release, clang asan-ubsan/tsan |
formula-cpp |
4: gcc release, clang debug/release/ubsan |
Excluded:
- Windows presets (
cl-*,clangcl-*,windows-everything): the image is Linux-only. - Coverage presets:
pace-ccbinds coverage-map objects to the checkout root, so they never hit from another checkout.paceandmorphalso refuse to configure coverage with a compiler launcher.
Each pair is its own cache namespace. Namespaces include the toolchain ABI, architecture, project, and preset, as the usage guide requires.
The warm-up is a job inside publish.yml, not a separate workflow. The seed
must be produced by the same pace-cc, pace-kv, and compilers it is baked
beside: cache keys include the compiler banner, and pace-kv refuses stores
written by a different pace build. publish.yml re-resolves pace master
on every run, so a separately scheduled warmer could seed an image with a store
it cannot read.
- Build base. Build
cpp-ci:<sha>-base(toolchain only) and push it. - Warm. For each project, run one job inside the base image. The job
checks out the project's default branch at a recorded commit and builds
each of its presets with
-DUSE_COMPILER_CACHE=ON, using a localpace-kvdaemon as the only writer. Release presets also runctest. Onlymasteror the schedule writes; pull requests never do. - Gate. A project's store is kept only if every one of its presets builds and passes. The store is closed with a graceful stop, which flushes pending writes, before it is copied.
- Validate. Each store is opened with the new
pace-kvbinary. A store that fails to open is dropped, and the image is built without a seed for that project. - Bake and tag. Copy the validated stores into the image under
/opt/pace/seed-cache/<project>/, push the image ascpp-ci:<sha>, and movelatestto it. - Consume. A job copies only its own project's store to a writable
location before starting its local
pace-kv, as the usage guide describes. A store that fails to open falls back to an empty one.
- Store layout: one store per project.
pace-kvcannot merge stores and has no key enumeration or export, so each writer job owns one store. Stores are started with a file path, not a directory, so the shard count cannot change between writer and reader. - Coverage: excluded (see Scope).
- Sanitizers and debug presets: warmed on every run. Sanitizer builds are the most expensive, so they are where a seed saves the most time.
- Tests: release presets run
ctest. Sanitizer and debug presets only build, because running their tests roughly doubles job time without changing what the cache holds. - Size cap: each project's warm-up runs
pace-kvwith a disk cap of 2 GiB, and the cap is revisited after the first measured run. The cap evicts least-recently-used entries, and it counts compressed size. The store file keeps free pages above the cap, so the file size is what gets measured. Measured locally:morphlinux-everythingobjects total 1.7 GiB, andgcc-debug8.6 GiB, because debug builds carry full-gline tables. Debug presets dominate the size. - Credentials: the warm-up uses a local
pace-kv, so it needs noPACE_TOKEN. All four repositories are public, so no checkout token is needed. The warm job getsGITHUB_TOKENwithcontents: readandpackages: writeonly. A shared remote daemon, if one is added later, would getPACE_KV_TOKENin a GitHub Environment limited tomaster, never on pull request events.
-
Path portability fails for Lightweight
gcc-release. Measured with the image'space-kvandpace-cc, cold build then a build from a second checkout at a different path against the same store:Build Compiles Hits Store size Cold, checkout A 243 0 317 MB Warm, checkout B (different path) 243 104 (43%) 545 MB Cause:
LightweightTestdefinesLIGHTWEIGHT_TEST_POST_MIGRATE_PLUGIN="$<TARGET_FILE:post_migrate_plugin>"(Lightweight/src/tests/CMakeLists.txt:211). That value is an absolute build-tree path, so every test compile's arguments differ between checkouts.pace-ccdoes not relativize-Dvalues:PathPortionOfinsrc/apps/pace-cc/CacheKey.cpponly tokenizes the flags in the path table, and-Dis not one of them. The 138 test objects that carry the define are exactly the misses. The 12 test objects without it hit, and so did all library and tool objects.Counterfactual: with the define set to the relative name
post_migrate_plugin, a build ofLightweightTestin a second checkout hit 226 of 228 compiles (99.1%). Before the change it hit 43%.Until this is fixed, a seed built in one path helps only jobs that build at the same path, so warm-up and consumer jobs must use the same checkout layout. No other local project (
pace,morph) matched this pattern.formula-cpphas not been checked for it. -
Compiler banner drift. If the
apt.llvm.orgclang 22 point release changes between daily builds, the cache is re-keyed every day. The base build pins the compiler version, so this can be detected by comparing banners between runs. -
Warm-up time and size are unmeasured. Per-preset times and compressed store sizes on GitHub runners are unknown until the first run.
-
Unverified for two projects. The coverage guard was verified only in
paceandmorph.Lightweightandformula-cppneed the same check before their presets are trusted. -
Seed size in every pull. All project stores are baked into one image, so every job pulls all of them. Splitting them into per-project images is an alternative if the pull cost is too high.