CI runs nothing that you cannot run here. Every job in
.github/workflows/main.yml is one bake target
from bake.hcl plus a handful of environment variables, so a red
cell reproduces with one command:
feat_set=all cargo_profile=release ./docker/bake.sh suiteThe workflow files hold no build logic. They choose a machine, name the cells,
and call bake.sh.
| Target | What it runs |
|---|---|
build |
cargo build --workspace |
test |
cargo test --workspace, so unit, integration and doc tests |
valgrind |
the same tests under Memcheck, continuing through every target |
clippy |
cargo clippy --all-targets, denying warnings |
fmt |
cargo fmt --check, pinned to nightly by rustfmt.toml |
doc |
cargo doc, with the jevmalloc_docs cfg the binding shapes need |
bench |
type-checks benches/, which nothing else compiles (see below) |
suite |
jemalloc's own suite, about 1800 cases, via make check |
There are three groups: lint (fmt, clippy, doc, bench), tests
(test, valgrind, suite), and default, which is both. With no target you
get default, on whatever axes are set.
The gating Valgrind cell uses feat_set=none, the prefixed symbol regime, and
passes --no-fail-fast so one report cannot hide later test binaries. The
dedicated tools layer pins cargo-valgrind 2.4.1, whose standard-library
suppressions cover the v0-mangled std::thread::current allocation used by
current Rust test harnesses. VALGRINDFLAGS leaves program-defined allocator
symbols unintercepted, so Valgrind cannot replace only the unprefixed half of a
statically linked jemalloc build.
Each takes one value from the environment, or a JSON array through its plural
form (feat_sets='["all","none"]') to widen it locally.
| Variable | Values |
|---|---|
feat_set |
default, stats, prefixed, none, all |
cargo_profile |
dev, release |
cc_name |
gcc, clang |
rust_toolchain |
stable, nightly |
rust_target |
any installed target; defaults to the host |
sys_name |
debian |
Three of these move the C build, which is the point of matrixing them:
feat_set. Every jemalloc feature passes an explicit--enable-Xor--disable-Xtoconfigure, soallandnoneare the two extremes of the option spread rather than a default and a superset.defaultandstatslink jemalloc unprefixed, where it also services libc's own allocations;prefixedandnonedrop the default features, which is what asks for--with-jemalloc-prefix=_rjem_and sets theprefixedcfg. Those are two distinct links, andjevmalloc-syshas tests that compile under only one or only the other, so both regimes have to run rather than build.cargo_profile. There is nodebugcargo feature:build.rsreadsdebug_assertions, sodevconfigures jemalloc with--enable-debugandreleasewith--disable-debugand-DNDEBUG. They are disjoint C builds.cc_name. Selects the compilerbuild.rshands tocc, asCC_<target>. On a musl targetgccresolves to themusl-gccwrapper.
A musl leaf also picks up .cargo/config.toml, which
names -lc a second time at the end of the link line. rustc places the standard
library's own -lc ahead of the bundled jemalloc objects and nothing puts
another after them, so without it every musl test binary fails to link against
the whole libc surface jemalloc touches.
JEMALLOC_SYS_WITH_MALLOC_CONF, JEMALLOC_SYS_WITH_LG_PAGE,
JEMALLOC_SYS_WITH_LG_HUGEPAGE, JEMALLOC_SYS_WITH_LG_QUANTUM and
JEMALLOC_SYS_WITH_LG_VADDR pass straight through under their real names, so a
bake leaf and a local cargo run take the same environment. None of them is
set by a gating cell: jemalloc's suite is not known to pass under a non-default
page or quantum, and a lowered quantum is under-alignment UB that
jevmalloc/tests/flags.rs exists to catch. Reach them through the workflow's
manual dispatch, or here:
JEMALLOC_SYS_WITH_MALLOC_CONF=background_thread:true ./docker/bake.sh suiteThe layers are system (the distribution and every C toolchain), rustup,
rust (the toolchain and its components), source, then one leaf per cell.
Caching stops at rust. That layer is the expensive, slow-moving one, and it is
shared by every leaf; the cargo registry, the rustup downloads and apt's own
downloads ride along in cache mounts.
No leaf shares a CARGO_TARGET_DIR with another, and none is cached. A cold
C build is around 25 seconds, so there is little to win, and quite a lot to
lose: build.rs deliberately does not watch JEMALLOC_SYS_RUN_JEMALLOC_TESTS,
so a suite leaf that inherited a warm target dir would take the cache hit and
report a pass without running a single case. The whole matrix is only
meaningful if each cell configures and compiles jemalloc itself.
source is an allowlist of the files that actually feed a build, so editing
this README, a workflow, or bake.hcl invalidates no cargo layer.
The builder is named for the GitHub actor and is shared with Tuwunel, which builds on the same machine and addresses it by the same name. One builder means one buildkit, one layer cache and one garbage-collection policy across both projects rather than two full-fat caches competing for the same disk.
Because it belongs to neither project alone, it is created by an Init job
running .github/workflows/init.sh, a verbatim copy of Tuwunel's, before any
cell starts. That script also carries the reaper that sweeps other actors'
builders once they have been idle a day, which works only because every
repository sharing the machine marks itself on every run. Its policy holds
dependency cache mounts (the cargo registry, rustup downloads) apart from build
layers, so trimming layers never evicts them, and keeps a warm floor that
garbage collection will not prune below. Commit-message directives reach it:
[ci clean] discards the builder so it is recreated, [ci clean nocache]
recreates it cold.
buildkit applies a builder's configuration only when the builder is created, so
the budgets in the Init job must match Tuwunel's. Whichever repository runs
first is the one that decides them, and a disagreement is a policy the machine
may or may not get. Changing them means deleting the builder, which is what
[ci clean] is for.
A workstation and the GitHub-hosted machines that pull requests build on have no
Init job and no shared disk. There docker/bake.sh creates a plain builder
with no policy at all, which is also why it configures none: init.sh stays the
only thing that can define the shared builder.
The x64 self-hosted pool is a single shared machine that also carries Tuwunel's
CI, so cells are capped in flight rather than fanned out as wide as they will
go, and a superseded run on a branch other than main is cancelled.
Self-hosted runners are reachable only from pushes and manual dispatches. A pull request can carry a fork's code, so those runs stay on GitHub-hosted machines. Nothing about the build changes; only the machine does.
Darwin is the one leg outside all of this, because there is no macOS docker host to bake on. It builds natively in the workflow and matches what the support table in the top-level README claims for it.
Found by running the matrix, and the reason two cells are not the obvious ones:
feat_set=allon musl does not build.profilingreachessrc/prof_sys.c, which includes<execinfo.h>underJEMALLOC_PROF_FRAME_POINTERas its fallback unwinder, and musl has noexecinfo.h.profiling_frameptrdoes not rescue it. The musl cell therefore usesstats.feat_set=noneunderdevfails jemalloc's suite.test/unit/double_freeexits with a status the harness does not recognise, reported asTest harness error, once--enable-debugsits on top of everything-disabled. Thenonesuite cell therefore runs underrelease, where it is clean. Thetesttarget runsnoneunder both.
Neither is carried as a permanently failing report-only cell: a red that never changes teaches people to ignore reds. They are written down here instead.
The aarch64 jemalloc suite and the Darwin test run carry soft, which reports
their result without failing the run. Both pass today, and the support table
says so; they stay report-only because they are the two legs nobody can
reproduce from a development machine here, so a red in either is a thing to go
and look at rather than a thing to block on.
soft sits on the step and not on the job, which matters more than it sounds.
A job carrying continue-on-error still reports its check run as failed, and
the commit list aggregates check runs rather than the run's own conclusion, so
that spelling hangs a red X on a commit whose run reads green. Failing the step
keeps the job green; a Report step then raises a warning annotation, which is
what carries the result up to the run summary.
Two combinations in the support table have no cell at all: the jemalloc suite
on musl and on Darwin. They are marked ? rather than ✗, so the table claims
only what it measures.
benches/roundtrip.rs is #![cfg(bench)], so without RUSTFLAGS='--cfg bench'
it compiles to an empty crate and clippy --all-targets type-checks nothing in
it. The bench target is the only leaf that passes that cfg, and it is why the
benches cannot rot unnoticed. Its harness needs nightly.