A build must answer one question before it can emit a command line: where the target's compiler runtime, platform interface, C library and C++ runtime come from. mcpp resolves that question once, after the dependency graph is known, and every later stage reads the single resolved value.
This document specifies the model, the rules that govern it, what a project writes, and what a package declares.
The target side of a build consists of five layers.
| Layer | Content | Implementations |
|---|---|---|
compiler |
the program that compiles | llvm, gcc, msvc |
compiler-runtime |
the compiler's own runtime: integer and floating-point builtins, the unwinder | compiler-rt with libunwind, libgcc |
kernel-abi |
the platform interface, or its equivalent | linux, windows, darwin, openkal |
c-abi |
the C library | glibc, musl, picolibc, ucrt, libSystem |
c++-abi |
the C++ library and its ABI runtime | libc++ with libc++abi, libstdc++, MSVC STL |
A component is a layer when three conditions hold simultaneously: at least two interchangeable implementations exist; it can be replaced independently of its neighbours; and it stands in a definite "was configured for" relation to the layer beneath it. A component failing any one of the three belongs to an adjacent layer rather than to one of its own.
compiler-runtime is separate from c++-abi because the builtins are what a
C program requires. Treating them as part of the C++ runtime asserts that a C
program needs no integer division, and that assertion has already produced a
measured defect: a C program cross-compiled to macOS was asked whether a C++
runtime was present, answered that none was, and the link line consequently
retained the compiler payload's own libc++.
kernel-abi is unnamed on a conventional stack, where a C library issues
system calls or invokes platform entry points directly. Naming the seam is what
permits one C library implementation to sit above several platforms.
Each layer is supplied from one of four origins.
| Origin | Meaning | Known |
|---|---|---|
payload |
the compiler payload carries it | before dependency resolution |
prebuilt |
a named prebuilt payload supplies it | before dependency resolution |
graph |
a package in the dependency graph supplies it | after dependency resolution |
— |
nothing supplies it, and that is a statement | — |
Two origins are knowable before dependency resolution and two only after it. The target side is therefore resolved exactly once, at the point where the graph exists. Any earlier derivation is an inference about a fact that does not yet exist, and independent inferences about such a fact do not agree.
An absent layer is an answer rather than a gap. A bare-metal target has no kernel; a project that depends on no C library has no C library.
A C library, a platform interface and a C++ runtime are mutually exclusive choices rather than additive contributions. Two suppliers for one layer is an error, reported during resolution and naming both packages together with how each entered the graph.
The failure mode motivates the strictness: selecting the wrong supplier does not fail the link. It produces a program that runs and intermittently does not.
An implementation is usable only above the layer it was configured for. A
libc++ build records that configuration in its __config_site; a libgcc
build is configured for GCC. The relation is declared rather than inferred —
see requires below.
Two consequences follow. The compiler payload's C++ runtime is eligible only
when the C library is also the payload's. A compiler runtime must belong to the
compiler's own family, since a build in which the two disagree resolves
__udivti3 differently from every other link in the same program.
The engine wires two layers together only when they come from different origins.
| Combination | Relation expressed by | Engine |
|---|---|---|
both from graph |
ordinary dependencies between packages | no involvement |
both from payload |
the payload is internally consistent | no involvement |
| one prebuilt, one from the graph | only the engine knows both addresses | wiring required |
Moving a layer from a prebuilt payload into the dependency graph therefore removes engine work rather than adding it. This is the mechanism by which one source reaches several platforms without an engine change.
The five layer names are a closed set compiled into the engine. The implementations filling them appear in package manifests and in the index, and in no line of engine code.
Layer names may be fixed because the layers are determined by the C and C++ build model and do not grow. Implementations may not, because growth is precisely what they do: an ecosystem's combinations are the product of its implementations while its packages are their sum.
Layer names do not appear in a project manifest. A project expresses its target side through three existing mechanisms.
--target <triple>, or [build] target. The OS field selects the platform
interface. The environment field states a request for a C library; it is a
request rather than the answer, and the resolved value is reported by the
build.
Omitting the field declines to state one: x86_64-linux asks for whatever
supplies that layer, and x86_64-linux-musl asks for musl. When the dependency
graph supplies a different one the graph decides, and the build reports that the
name is inaccurate together with the spelling to use instead. The request is
ignored rather than violated, so the artifact is the same either way.
mcpp toolchain default <family>@<version>, [toolchain] in the manifest, or
[target.<triple>].toolchain for one target. This selects the compiler
layer, which is the one layer no package may supply.
A target row may carry a convention — a toolchain whose payload supplies that target's C library. The convention applies when the manifest states nothing for that target AND nothing in the dependency graph supplies the target's system. The second condition is knowable only after resolution, so the toolchain is resolved there rather than before it.
Every other layer is selected by depending on a package that supplies it. A single dependency may supply several layers, and may bring further suppliers through its own dependencies.
[dependencies]
openkal-llvm-runtime = "0.1"The build prints what it resolved. A manifest line states an intention that goes stale when the packages beneath it change; a report states the outcome and cannot.
By default the report lists only the layers the compiler payload did not
supply. A zero-configuration build resolves all five from one payload, and five
lines reading (payload) answer a question nobody asked.
Target x86_64-linux-gnu
Target x86_64-windows-gnu → x86_64-w64-windows-gnu
kernel-abi openkal (openkal-windows@0.1.3, graph)
c-abi musl (openkal-musl@0.3.3, graph)
c++-abi libc++ (openkal-llvm-runtime@0.1.1, graph)
MCPP_VERBOSE=1 lists all five. Diagnostics always list every layer the
decision rested on, including the ordinary ones, because an error that omits
its evidence cannot be checked by its reader.
Interface and implementation are separate columns. openkal is an interface
and openkal-windows an implementation of it; collapsing the two would conceal
why one source reaches several machines.
A package supplying a layer declares it in the reserved mcpp: namespace.
provides = ["mcpp:compiler-runtime=compiler-rt", "mcpp:c++-abi=libc++"]The grammar is mcpp:<layer>[=<implementation>]. The layer name is validated
against the closed set; a misspelling is an error rather than a silently
disabled behaviour. Names outside the prefix belong to the feature system and
pass through unexamined.
mcpp:compiler may be required but not provided. A compiler is a payload this
engine installs and drives, and the differences between families — flag
spellings, the module model, the BMI format, the driver configuration file —
are facts the engine must hold rather than data a package can describe.
requires is the symmetric half, and the mechanism by which the layering rule
is enforced without an implementation name in the engine.
requires = ["mcpp:compiler=llvm"]A C++ runtime built from libc++ sources is compiled, and its module compiled,
by Clang. That fact belongs to the package. The engine checks a relation it can
state generically — the named layer must resolve to the named implementation —
and reports a mismatch by naming both, which a table of families compiled into
the engine could not do for a family it had never heard of.
The check runs before compilation begins. The combination it rejects otherwise fails inside the runtime's own headers, in a message naming a file the reader has never opened and no decision mcpp made.
A package that is a standard library states where its std module source is
and what that source requires.
[build]
std-module = "llvm-generated/std.cppm"
std-compat-module = "llvm-generated/std.compat.cppm"
std-module-flags = ["--no-default-config", "-nostdinc", "-nostdinc++"]These keys belong to [build] because the module source is one of the
package's translation units: it is compiled with the package's include
directories and definitions. Membership of [build] also makes the flags
conditional, which a package supplying one C++ runtime over several C libraries
requires.
[target.'cfg(c-abi = "musl")'.build]
std-module-flags = ["-D_GNU_SOURCE"]Declaring std-module without a matching provides entry is an error: the
package describes a library it does not supply.
The [package] spelling of these three keys remains accepted and is not
conditional.
A package supplying a layer frequently supports several implementations of the layer beneath it. It queries the resolved target side rather than being told.
[target.'cfg(c-abi = "musl")'.build]
include_dirs = ["config/musl"]
[target.'cfg(c-abi = "picolibc")'.build]
include_dirs = ["config/picolibc"]Requiring a feature selection for this would oblige a project to restate what the target triple or its dependency graph has already established, and permit the two statements to disagree.
The predicate keys are the five layer names, and their values are the interface
names in the table at the top of this chapter — the same strings the Target
report prints. They combine with the triple keys under all/any/not:
[target.'cfg(all(linux, c-abi = "musl"))'.build]
cxxflags = ["-D_GNU_SOURCE"]musl and diverge for gnu: on Linux that segment asks for glibc, and on
Windows it names the MinGW flavour of the toolchain, whose C runtime is the same
UCRT the MSVC flavour links. The spelling is c-abi = "glibc", never
c-abi = "gnu"; the request, as opposed to the answer, is env = "gnu" — a
different question (docs/spec/target-side.md §3.4).
env and c-abi are not interchangeable. env is what the triple
asked for; c-abi is what the graph and the payload answered. An
openkal-musl in the dependency graph supplies musl under an x86_64-linux-gnu
triple, and only c-abi sees that.
These predicates are available in [build] sections only. The target side is
resolved after dependency resolution, so a dependency selected by one would
form a cycle; [target.'cfg(<layer> = …)'.dependencies] is reported and
ignored rather than silently dropped. A package whose C libraries require
different dependencies is split per C library, or depends on the union and
selects sources in [build].
A key mcpp does not know — a typo, or a predicate from a newer mcpp — is reported as a schema warning and the section does not apply. It used to evaluate to false in silence, which reads exactly like a section that correctly did not match.
Four conditions are reported by the engine rather than by a compiler.
| Condition | Report |
|---|---|
| a required implementation is not what resolved | names both, and the command that selects it |
| two packages supply one layer | names both, and how each entered the graph |
| a layer has no supplier | names the layer, and the capability to depend on |
| the payload's C++ runtime sits above a foreign C library | names both, and the two ways out |
A message from a compiler or a linker about a target-side combination indicates a missing diagnostic. The engine knows the combination is untenable before any command line is emitted.
Three provisions preserve existing manifests and existing builds.
The capability hosted-standard-library continues to denote the C++ layer. A
package carrying both spellings is one supplier, and the entry naming an
interface is the one reported.
The toolchain family spelling openkal-llvm normalises to llvm. It named the
same payload and carried a fact about the target side, which the model above
resolves from what packages declare.
An unknown name inside the reserved prefix is an error in the root project's own manifest and a warning in a dependency's. The first is a misspelling the author is looking at; the second is a manifest written against a newer engine, and refusing it would mean the layer vocabulary could never be extended by a published package. An unknown key elsewhere in a manifest is ignored.
That provision governs future engines only. A package declaring a layer name still requires its consumers to run an engine no older than the release that introduced the name.