Reader: a contributor who will build and change mcpp itself.
The question this chapter answers: how is mcpp built from source, how are its own tests organised, and what does a contribution have to satisfy.
Not here: how a user builds their own project, which is 01 — Getting Started, and how a release is cut, which is 92 — Releasing mcpp.
mcpp is self-hosting — mcpp builds mcpp from source using mcpp itself. Any environment that already has a working mcpp binary can build from source.
Follow 01 — Getting Started to install a working copy of mcpp, then clone the repository:
git clone https://github.com/mcpp-community/mcpp
cd mcppmcpp build # compile the current source with the existing mcpp → ./target/.../bin/mcpp
mcpp run -- --version # run the artifact you just built
mcpp test # build and run C++ tests discovered under tests/**/*.cpp (including tests/unit)mcpp test does not run the shell end-to-end suite under tests/e2e/; run
those separately against the freshly built binary.
The first build automatically fetches the default toolchain; see 20 — Toolchain Management for details.
To produce a fully static binary identical to a release (the path taken by release.yml):
mcpp build --target x86_64-linux-musl
# → target/x86_64-linux-musl/.../bin/mcpp is a fully static ELFmcpp is a workspace. modules/ holds ten packages that the engine imports;
src/ is the engine itself. The operating system is reached through
mcpplibs.os; the one primitive mcpp keeps
per system, the lock every release of mcpp takes, is in src/native/.
modules/ workspace members, imported by src/
├── invocation/ the environment of one invocation: statements over the started one
├── manifest/ manifest and descriptor parsing
├── toolchain-model/ triples, dialects, fingerprints, the link model
├── buildmcpp/ the build.mcpp contract: protocol, directives, provisions
├── source-kind/ source-file role classification
├── versioning/ this binary's version, and SemVer requirements
├── dyndep/ ninja dyndep emission
├── graph/ the generic dependency graph
├── libs/ vendored text-format parsers
└── log/ leveled logging
src/
├── main.cpp entry point
├── native/ the lock, per system (flock, LockFileEx)
├── run.cppm run/ every program mcpp starts, and how mcpp ends at a request
├── effects.cppm the constraints of a run (offline, denied effects)
├── home.cppm home/ the home, its lock and its trust check
├── ui.cppm ui/ output, the terminal, key input
├── cli.cppm cli/ command dispatch and the commands
├── build/ build orchestration and the ninja backend
├── modgraph/ P1689 module scanning and the dependency graph
├── pm/ the resolver and the package-management commands
├── toolchain/ detection, fingerprinting, the std module
├── pack/ publish/ mcpp pack, mcpp publish and xpkg generation
├── fetcher/ fallback/ download, installation, fallback resolution
├── bmi_cache/ the cross-project BMI cache
├── runtime/ provider/ the runtime contract, launch environment and the xlings bridge
├── freestanding/ bare-metal targets, link line and runner
└── scaffold/ `mcpp new` and templates
tests/
├── unit/ 166 files of mtest cases, one test program (`[test] programs = "directory"`)
└── e2e/ 711 shell scripts against a real binary
Two layers, and they answer different questions.
Unit tests (tests/unit/, 166 files, and each member's modules/<name>/tests/)
are C++ programs mcpp test discovers. They exercise a module's contract with no
binary and no filesystem state. Each is a file of mtest
cases: an object UTest name{"Suite/Case", [] { ... }}; is one case, with no main
(mcpp test --guide). mcpp test --case 'TargetSelect/*' runs the cases a pattern
names; mcpp test -p <member> runs a member's. The files are one program, linked once and run as
one process per file.
End-to-end tests (tests/e2e/NN_<name>.sh, 721 files) run a real mcpp
binary against a real project. run_all.sh is the CI entry point. mcpp test
does not run them.
MCPP=<fresh-mcpp-binary> bash tests/e2e/02_new_build_run.shA capability gate decides which run. The first lines of an e2e script declare what it needs, and a runner without that capability skips it:
#!/usr/bin/env bash
# requires: elf gccgcc (80 scripts), elf, unix-shell, llvm, jq and fresh-sandbox are
the ones in use. A script that requires a capability no CI job provides never
runs anywhere, and its greenness means nothing — check that some job supplies
what a new script asks for.
CI runs them by mtest (2026.11.8.1+):
tests/e2e/mtest.toml makes each script a program, its # requires: line its
tags and a # resources: line what it holds while it runs (machine: nothing
runs beside it; a script that changes the shared home's configuration says so).
run_all.sh with E2E_MTEST=<mtest> runs the suite through it: E2E_SHARD
divides by the measured history of the store (tests/e2e/.mtest, which CI takes
from main's last run), E2E_JOBS runs that many scripts at once, and
E2E_SET=pr runs the scripts a change affects by the nightly coverage map
(.github/workflows/e2e-coverage.yml); a script the set leaves out is reported
unaffected, which the check that every script runs somewhere (rule R5) accepts
for that run, main's full runs being its coverage. mtest explain in tests/e2e says where
a script failed and how to run it again.
The most transferable rule in this repository, and the one that fails silently when it is skipped:
After writing a check, remove the fix and run it once. A check that has never been seen to fail is not known to measure anything.
Three shapes it catches, all of which have shipped here at least once:
| shape | form |
|---|---|
| the criterion never runs | a test gated on a capability no job provides |
| the criterion cannot fail | a substring search satisfied by any wording |
| the criterion measures the wrong object | a fixture whose directory accumulates across runs, so the search answers about an earlier build |
State the denominator too. "Every host in the table was scanned" is a check; "the scan found nothing" is not, because an empty enumeration also finds nothing.
.github/tools/ holds eighteen scripts. Four are worth knowing before a first
PR:
| script | the condition it refuses |
|---|---|
check_docs_style.sh |
question headings, second person in a reference chapter, a 简体中文 page whose heading structure has fallen behind |
check_docs_structure.sh |
a chapter citing a design record, a docs/NN-*.md path that does not resolve, a translation missing a table |
check_version_pins.sh |
a version written in one place and not the others |
check_modules_wiring.sh |
a workspace member wired into some of the three places that must know about it and not the others |
Closed syntax, open vocabulary: whoever owns the parsing semantics defines the keys; whoever owns the domain knowledge defines the values.
- mcpp only defines mechanisms (feature union/closure, capability require/provide/override, profile→compiler flags, platform→triple); the keys and shapes are fixed. Domain vocabulary such as feature names, capability names, and backend names appears only in values, never in mcpp's code.
- Package-custom toml keys are not supported: key legitimacy must not depend on "first parsing the target package," otherwise the manifest loses static parseability (a prerequisite for lockfiles/LSP/auditing). A package's extension point = open value domains within fixed mechanisms.
- Package-level knobs all converge into features; for sugar keys (such as
backend=) to enter the core syntax, they must satisfy: ① domain-neutral (a cross-ecosystem general pattern) ② 1:1 desugaring with zero new parsing semantics. - A key that duplicates an answer another section already gives is not admitted.
Two places to state one fact is two places that can disagree, and the failure
is silent — whichever reader loses the race is simply wrong. Library packaging
(12) is the worked example: it added zero
manifest keys, because what to pack is
[targets.<n>].kind, which interface to publish is[lib]plus the module graph, which headers are public is[build].include_dirs, and the per-artifact evidence is[[runtime.artifacts]]. - A field that describes what a generated package IS (rather than what a build
should DO) belongs on
[[runtime.artifacts]]— see §2.11.provenancebeginning withmcpp-packis what marks a directory as one, and mcpp refuses tobuildinside it.
File issues at github.com/mcpp-community/mcpp/issues, ideally including the following:
- The full output of
mcpp self env - The full output of the failing command (
MCPP_LOG_LEVEL=debuggives more detail) - Your operating system, distribution, and glibc version (check with
ldd --version)
mcpp is in early iteration and its interfaces may change. Before submitting a PR, please note:
- For changes touching the CLI or the
mcpp.tomlschema, open an issue first to align on direction. - Keep each PR focused on a single change; write commit titles in English imperative form (
fix: .../feat: ...). - For behavior changes or test documentation, run
mcpp testand the relevant E2E scripts against a fresh binary before submitting. For documentation-only changes, recheck the examples and links; usegh pr checks <pr-number>for the PR's actual required checks.
- Community forum
- Chat group QQ: 1067245099
- mcpp-index — the default package index
- mcpplibs — the companion collection of modular C++ libraries