Reader: an author whose build now contains more than their own code.
The question this chapter answers: where does a dependency come from, what does a version constraint mean, and what happens when two of them disagree.
Not here: what makes two packages the same package — that is SPEC-001, which this chapter applies rather than restates — and how to publish one, which is 11 — Publishing a Library.
Before: 04 — The mcpp.toml Manifest is where these tables live among the others. After: 06 — Features and Capabilities is how a dependency becomes optional.
The recommended form is a dotted selector with an exact version. The dotted form names one identity — everything before the final dot is the namespace, the final segment is the package name — so what resolves does not depend on which namespaces happen to be configured.
[dependencies]
compat.gtest = "1.15.2"
imgui.core = "0.0.1"
imgui.backend.glfw_opengl3 = "0.0.1"
mcpplibs.capi.lua = "0.0.3"A bare name is accepted and resolves through the default namespace
(mcpplibs), which is convenient in a project that uses only that namespace and
ambiguous in one that does not:
[dependencies]
cmdline = "0.0.2" # resolves to mcpplibs.cmdlineEquivalent spelling: the namespace sub-table
Groups entries under one namespace. It resolves to exactly the same identities as the dotted form and is worth using when many dependencies share a namespace.
[dependencies.mcpplibs]
cmdline = "0.0.2"
tinyhttps = "0.2.2"
llmapi = "0.2.5"
[dependencies.compat]
glfw = "3.4" # Explicit namespace; no fallback searchA quoted key with dots, "mcpplibs.cmdline" = "0.0.2", is one TOML key whose
name contains dots. mcpp reads it as the selector those dots spell, warns that
the spelling is deprecated (since 2026.11.8.1, removed from 2027.2; docs/51)
and names the unquoted key that replaces it: mcpplibs.cmdline = "0.0.2".
A package and a package of its namespace in one table, mcpplibs.mtest and
mcpplibs.mtest.gtest, cannot both be written unquoted -- one name would be a
value and a table -- so the quoted key of one of them is their spelling and is
not deprecated:
[dev-dependencies]
mcpplibs.mtest = "0.9.0"
"mcpplibs.mtest.gtest" = "0.9.0"# Path dependency (local development)
[dependencies]
mylib = { path = "../mylib" }# Git dependency — at most one of tag / branch / rev; none follows the default branch
[dependencies]
mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" }
applib = { git = "https://github.com/user/applib.git", branch = "develop" }
tiplib = { git = "https://github.com/user/tiplib.git" } # the remote's default branch# Long-form dep spec: features and backend knobs
[dependencies]
imgui = { version = "0.0.3", features = ["docking"] } # Request a feature of this dependency
widget = { version = "1.0", backend = "glfw_opengl3" } # Sugar for: features=["backend-glfw_opengl3"]backend = "<impl>" is general-purpose convention sugar: it desugars 1:1 into
requesting the dependency's backend-<impl> feature (a library that supports this
knob should declare a backend-* family in its own [features]). If the target
package declares [features] but does not include the requested feature (including
the result of backend desugaring), a warning is issued by default, and an error
under mcpp build --strict.
An unchanged mcpp.lock retains its contents and modification time when a build
records the same resolution (2026.10.21.1+). LF and CRLF line endings are accepted
without rewriting an otherwise identical file. This does not skip dependency
resolution or change which dependencies the lock pins.
A git dependency that names none of tag, branch and rev follows the remote's
default branch, whose name the remote announces (main, master, trunk, ...;
2026.11.9.1+); it is a branch from then on, locked by commit. The same string
forms reach a platform or a tool on the command line:
--platform git+https://example.org/acme/acme-platform.git#tag=v1.
Git dependencies and mcpp.lock: a tag or rev already names a fixed point
in history, but a branch moves. The first build resolves the branch to a commit
and records it in mcpp.lock, and every later build rebuilds that commit — the
lock is authoritative, not a cache hint, so deleting ~/.mcpp/git or moving to
another machine cannot silently move the build to a newer tip. This holds for a
branch the project reaches through another package as for its own (2026.10.26.2+):
such a branch is recorded under the package's qualified name. A newer tip must be requested
explicitly:
mcpp update mylib # drop the recorded commit; the next build re-resolves it
mcpp update # same, for every dependencyBecause the recorded commit is enough to decide what to build, a rebuild with the
clone already in ~/.mcpp/git makes no network request at all and works under
--offline. Only two things need the network: resolving a branch that has no
commit in the lock, and cloning a commit that is not cached yet. A git = value
that names a local directory (or a file:// URL) needs neither, so it is never
refused offline.
SemVer constraints:
[dependencies]
foo = "^1.2.3" # >= 1.2.3, < 2.0.0 (caret, default)
bar = "~1.2.3" # >= 1.2.3, < 1.3.0 (tilde)
cat = "^0" # >= 0.0.0, < 1.0.0 (a segment not written is a wildcard)
dog = "~1" # >= 1.0.0, < 2.0.0
baz = "=1.2.3" # Exact match
qux = ">=1.0, <2.0" # Range combinationA bare requirement is a caret: foo = "1.2.3" is ^1.2.3, except that a bare
version equal to a published key addresses that key exactly. A bare partial
spelling follows the caret's wildcard rule as well: "0" is >= 0.0.0, < 1.0.0
and "0.0" is >= 0.0.0, < 0.1.0 (before 2026.10.31.1 both matched 0.0.0
alone). The dependency conflict check and the address check of --locked read
requirements the same way.
[dependencies]
sdk = { version = "1.0", visibility = "private" }Every dependency edge carries a visibility, defaulting to public. It
decides whether the dependency's include directories, defines and flags —
what the dependency asks of a consumer, provides/requires aside — reach
only this package's own translation units, or reach this package's
consumers as well.
| Value | This package's own units | This package's consumers |
|---|---|---|
public (default) |
yes | yes |
private |
yes | no |
interface |
no | yes |
private is the ordinary case for an implementation detail: a vendored
library, a platform SDK a package needs to implement a facility but does not
expose in its own interface. interface is the rarer, opposite case — a
header-only dependency this package's own headers #include but whose
objects it never compiles against. public is what most dependencies want:
a type from the dependency appears in this package's own public headers, so
a consumer needs the same include path to use them.
Getting this wrong in one direction is silent (an unused public broadcasts
headers nobody asked for) and in the other is a build failure at the first
consumer that needed what private withheld — which is the safer failure
mode, and why [feature-deps.<feature>] (§06, "A platform SDK dependency
stays private") states private explicitly rather than relying on a default
that happens to work until a consumer is added.
Two edges in the dependency graph can name the same identity — the same
(namespace, name) pair — from two different places: the root manifest and a
dependency's own [dependencies], or two unrelated dependencies. A declaration
has a kind (version, git, or path) and a reference within that
kind (a SemVer constraint, a git URL plus rev/tag/branch, or a
filesystem path). mcpp resolves the identity once; every requester's edge is
recorded, and what one requester's declaration decides is what every other
requester of the same identity gets.
| first declaration | second declaration | outcome |
|---|---|---|
| any | same kind, same reference | Unchanged: the second declaration becomes an edge to the identity already resolved. Nothing is reported. |
version |
version, a different constraint |
The two constraints are AND-combined by SemVer, as above; an unsatisfiable pair is refused, naming both constraints and both requesters. |
git |
git, a different rev/tag/branch |
The root's declaration wins when the root is one of the two requesters; otherwise the declaration resolved first wins. A dependency/source-override warning names both requesters and both references, states which one is used and why, and how to take the other. This is never silent. |
path |
path, a different directory |
The same rule and the same warning as the row above, compared by canonical absolute directory rather than by git reference. |
a root path or git declaration |
a dependency's git or version declaration (a kind clash) |
The root's declaration wins, with the same warning. When the losing declaration is a version requirement, it is checked against the [package] version of the root's resolved checkout; a violated requirement is refused, naming the pin, the requester and the requirement. The index is not asked to select or pin a request the root has answered, so the root's checkout serves a version no index carries yet (2026.10.26.2+). |
a dependency's path/git declaration |
another dependency's declaration of a different kind (a kind clash, and neither party is the root) | Refused: "requested as both a … dep … and a … dep …. Pick one." The message adds one sentence: declare the identity in the root to settle it. |
The root's privilege here is bounded the same way linkage is bounded to the
root manifest's own edges (see [dependencies] above): a whole-graph choice of
which checkout an identity resolves to is a decision only the artifact's own
manifest may make silently on a dependency's behalf. A dependency that
disagrees with another dependency, with neither being the root, is never
settled by guessing which one was declared first — that is exactly the
"accident of queue order" this section replaces.
In a workspace (mcpp 2026.9.30.2+). The root is the package a command
builds: a rooted workspace's own package, and each member the command selects
(every member under --workspace, X under -p X). Each holds the root's
position for its own declarations, as it did when every member was planned as
its own root, so a member's path override of a dependency that another
package requests by version wins, with the warning above. Two selected
members that declare one identity by two kinds, or by two references of one
kind, are refused, naming both: one configuration builds one checkout of a
package. The same holds for linkage
on a selected member's dependency edges, which is honoured, and refused when
two selected members ask for two forms of one package.
A later edge that asks more features (mcpp 2026.11.7.1+). A package is
built with the union of the features its edges ask. When an edge reaches a
package already resolved and activates a feature the earlier edges did not,
that feature's [feature-deps] and forwards are walked as well, so the
dependencies it needs, and the features it asks of them, reach the graph.
Before, the feature's sources were compiled and its dependencies were not
(mcpp#897; for example v selected beside s, which asks v for x).
Under a platform's version group (mcpp 2026.11.7.1+). When the target's
platform states [platform.versions], a request of a package the group lists,
by version, resolves to the group's version if its constraint admits it, and is
refused naming the request and the group if it does not (docs/35, "The version
group"). A family whose packages name one another by compatible ranges is
therefore resolved by its group, one version of each package per build.
A declaration of the root written override = true answers every request for
its package in the graph its table governs, whatever version, path or
repository the request names:
[dependencies]
mcpplibs.os = { version = "0.4.4", override = true }- Where it holds. On the root's tables and on
[workspace.dependencies], which governs every plan of the workspace's members (declaring a package there does not make a member depend on it).[dependencies]governs the build's graph and the tests';[dev-dependencies]the tests' only;[build-dependencies]the build programs' only. A library that writes it is told that it is ignored: the decision is the project's. - What it states. Each request the override replaces and does not already
admit is named once,
Overrode <package> <request> (requested by <who>) -> <override> (<table>).mcpp.lockmarks the packageoverride = true, and--lockedreads the marker as part of the recorded resolution. - The platform's version group. An override of a package the group lists is taken, with a warning that names the group's version: the platform states the defaults, and the project may choose another.
- When it is needed. Two version requests that no SemVer merge reconciles
are refused naming both, and the refusal ends with the line that overrides
the package. A library that pins one release of a package every consumer
shares (one that provides a target-side layer or a kernel ABI) is noted to
the projects that use it, and
mcpp publishrefuses the pin in a library: a range leaves the choice to the project.
A path or git dependency is the package its manifest declares, whatever key
reaches it. The source fixes the package, so a bare key states only the short
name: it does not mean mcpplibs, the declared namespace is adopted, and
nothing is reported.
# comp/mcpp.toml; fw/mcpp.toml declares namespace = "huxdemo"
[dependencies]
fw = { path = "../fw" } # huxdemo.fw is used; no warningA key that writes a namespace (a [dependencies.<ns>] table or a dotted key)
states an identity, and a manifest that declares another one contradicts it.
The declared identity is still used, and mcpp warns once for each consumer
manifest and declared namespace, after resolution. The warning names the
manifest by its path relative to the project root and lists the keys:
# mcpp.toml; libs/fw and libs/comp declare namespace = "huxdemo"
[dependencies.acme]
fw = { path = "libs/fw" }
comp = { path = "libs/comp" }warning: mcpp.toml names 2 dependencies in namespace acme, and the manifests they reach declare huxdemo; the declared identity is used: acme.fw, acme.comp
help: write them in a [dependencies.huxdemo] table, for example `fw = { path = "libs/fw" }`
Two edges written fw and huxdemo.fw over one directory are therefore one
package, compiled once, and mcpp why deps lists both keys under it. A
manifest that declares no namespace takes the key's, so two keys with
different namespaces over one such directory are two identities over one
source: the build is refused before scanning, naming both, and the fix is a
namespace in that manifest or one key in both places. A version dependency
is unaffected; its identity is the key.
A git dependency names a repository, and its key names which package of the
repository is meant. The root manifest's package is one; each entry of that
manifest's [workspace] members is another. A key whose identity is not the
root package's selects the member whose manifest declares it, at the same
commit:
# repo/mcpp.toml declares spike.fw and [workspace] members = ["tool"];
# repo/tool/mcpp.toml declares spike.fw-installer
[dependencies]
spike.fw = { git = "https://example.org/fw.git", rev = "cc3c74c5" }
spike.fw-installer = { git = "https://example.org/fw.git", rev = "cc3c74c5", tools = ["fw-installer"] }- The member inherits the repository's
[workspace.package], as it does when the repository is built from its own checkout. - A member's
pathedge that stays inside the clone, such asspike.fw = { path = ".." }, names the same git source at the same commit, so it is the package the root's key resolved rather than a second, path-sourced declaration of it. - A key that names neither the root package nor a member keeps the rule of the section above: the root manifest's identity is used, with the warning.
A key selects by identity, and no subdir key exists: an older client would
ignore such a key and build the root package without a word.
Every package has a two-part identity: a namespace and a name. Every selector normalizes to exactly one identity:
cmdline→(mcpplibs, cmdline); omitting the namespace means themcpplibsdefault, and nothing else.compat.gtest→(compat, gtest).mcpplibs.capi.lua→(mcpplibs.capi, lua).
There is no ordered fallback or fuzzy, index-wide search by short name:
# Correct — dotted selector
[dependencies]
chriskohlhoff.asio = "1.38.1"
# Correct — namespace sub-table (preferred for several packages from one org)
[dependencies.chriskohlhoff]
asio = "1.38.1"
# Wrong — a bare name never reaches the `chriskohlhoff` namespace
[dependencies]
asio = "1.38.1"The third form fails with an error that names the exact (mcpplibs, asio)
identity that was tried and, when the short name exists elsewhere, gives a
copyable explicit selector.
Every published compat.* package and every manifest written before exact
identity spells its dependency bare — gtest = "1.15.2". Failing those
outright on upgrade would break builds against data that is already published
and cannot be edited retroactively, so for one release a bare name that misses
mcpplibs still reaches compat.<name>, and a descriptor that declares no
namespace at all still answers to its bare name.
It is not quiet about it:
warning: dependency 'gtest' resolved to 'compat.gtest' through the deprecated
bare-name search; namespace omission means `mcpplibs` only. Write the exact
package:
[dependencies.compat]
gtest = "1.15.2"
(or run `mcpp add compat.gtest@1.15.2`). This fallback is removed in 2026.9.
What reaches mcpp.lock, the install layer and the cache is the canonical
identity, so the ambiguous spelling lives in exactly one place — the manifest —
until it is rewritten. mcpp add gtest@1.15.2 performs that rewrite.
The window does not apply to a selector that states a namespace
(mcpplibs.gtest misses and stays missed), and a bare name still never reaches
a third-party namespace.
Why one identity? Dependency resolution has to be reproducible. Candidate search would let two namespaces with the same short name be settled by index state, and adding an index could silently retarget an existing dependency.
For xpkg authors: in an index descriptor, identity is the pair
(package.namespace, package.name). The namespace is the dotted path; name is
a single atomic segment:
package = {
namespace = "chriskohlhoff",
name = "asio", -- one segment; NOT "chriskohlhoff.asio"
}
package = {
namespace = "mcpplibs.capi", -- depth belongs here
name = "lua",
}The filename is only a hint — a descriptor is found by its declared identity, so
pkgs/c/chriskohlhoff.asio.lua and pkgs/z/anything.lua resolve identically.
<name>.lua or <namespace>.<name>.lua are recommended (they hit mcpp's fast
path) but not required.
The older fully-qualified spelling (name = "chriskohlhoff.asio") is still
accepted, so already-published descriptors keep working. mcpp xpkg parse
enforces the descriptor rule; run it in index CI. Descriptor identity
requires mcpp >= 0.0.106; exact selectors require mcpp >= 2026.8.10.1; both use
xlings >= 0.4.69. Full normative text is in docs/specs/package-identity.md.
mcpp new --template deliberately reuses this identity model instead of
creating another package grammar: [ns.]name[@version][:tname]. A bare name
there also means only mcpplibs; version and template may be omitted
independently. Omitted tname selects the sole explicit default, or the only
template when no default = true is present. Multiple unmarked templates are
an error, never a directory-order choice. See the normative template rows in
docs/specs/package-identity.md §4.4.
mcpp build / run / test refresh the package index only when a dependency
cannot be resolved from the local copy — never merely because time has passed.
Concretely, a refresh happens when there is no local index at all, when a
dependency's descriptor is missing from it, when a SemVer constraint matches
none of the versions it knows, or when the install of an exact version is
refused because the index does not carry it (a version published after the
last sync); after that refresh the install is asked once more. A build whose
dependencies all resolve locally makes no network request, however old the
local index is.
The consequence worth knowing: a constraint like ^1.2 resolves against the
the versions the local index knows. A 1.3.0 published upstream after the last refresh
is invisible until it is fetched:
mcpp index update # sync the index
mcpp update # sync, then re-resolve dependencies
mcpp index status # local state: state, age and revisionControls, in order of precedence:
| Control | Effect |
|---|---|
--offline (any command) |
Never touch the network — no index refresh, no downloads, no toolchain auto-install, no git ls-remote/clone. Anything already installed still builds, including git deps whose commit is in mcpp.lock and whose clone is cached |
MCPP_OFFLINE=1 |
Same, for a whole shell session or CI job |
[index] auto_refresh = false in ~/.mcpp/config.toml |
Never refresh an index implicitly: not on a dependency miss, not before installing a package the local index lacks, and not for the first sync of a project's custom index (that build stops and names mcpp index update). Downloads still work |
MCPP_NO_AUTO_INSTALL=1 remains accepted as the older, narrower spelling of
--offline (it gates only toolchain auto-install).
A refresh is bounded. [index] refresh_timeout (seconds, default 120) is the
longest one refresh may take; a refresh that exceeds it is stopped, a warning
names the setting, and the build continues with the local index, as it does
after any failed refresh. An install through xlings is stopped when xlings
writes nothing, not even its heartbeat, for 300 seconds. Terminating mcpp
terminates the xlings process it started.
Run any command with -v to see the decision for each dependency and why.
[dev-dependencies.compat]
gtest = "1.15.2"mcpp build ignores these; mcpp test resolves and uses them, for the test units only: the package's own sources compile under mcpp test as under mcpp build (2026.11.8.1+, 08). mcpp test automatically discovers tests/**/*.cpp and compiles them into test binaries. The runner is framework-agnostic: each file is an independent binary judged by exit code — a bare main, gtest (via [dev-dependencies] + gtest_main), or any other framework all work identically, and -- args are forwarded to every test binary (e.g. -- --gtest_filter=...). Note: synthesized test target names may contain / (tests/00-a/0.cpp → 00-a/0), unlike [targets.*] names — the two namespaces are intentionally separate (test targets never enter the manifest or publishing). Tests are named by their tests/-relative path (tests/00-a/0.cpp → 00-a/0), each test compiles in isolation (a broken test fails alone; package/dep breakage is reported as a build error instead), and mcpp test <pattern> / --message-format json filter and machine-format the run.
The package being built stands for itself in its own graph (2026.11.9.1+).
A dependency reached through the package's [dev-dependencies] may name the
package being built: a library tested with a framework that itself uses the
library (mcpplibs.os under mtest). The
edge is the package itself, whatever version it asked for, and a version it did
not ask for is said; a second copy would provide every module of the package
twice in one program, and what is under test is the package as it is here. The
root's [dev-dependencies] serve its tests only, so the framework reaches the
library without a cycle of packages.
[build-dependencies]
tomlplusplus = "3.4.0" # a library build.mcpp imports
protobuf = { version = "35.1", tools = ["protoc"] } # a program the build runsA dependency is described on two axes (mcpp 2026.10.11.1+, SPEC-004 §12), and each axis has its own place in the manifest.
- The section decides the configuration the dependency serves.
[dependencies]and[dev-dependencies]serve the target: the package is compiled for it and linked into the project.[build-dependencies]serves the project's build program (build.mcpp): the package is compiled for the machine that runs the build and linked into that program. A configuration is inherited along[dependencies], so the dependencies of a build dependency serve the build program as well. - The request on the edge decides which products are wanted. A
[build-dependencies]entry asks for the package's library unless it liststoolsorartifacts;host-module = trueasks for the library beside them, andhost-module = falsedeclines it.tools = [...]asks for programs built for the machine that runs the build, from either section.
| Entry | Effect |
|---|---|
[build-dependencies] x = "1.0" |
build.mcpp imports the public modules of x, includes its public headers and links its objects |
[build-dependencies] x = { tools = ["gen"] } |
the program gen; no library |
[build-dependencies] x = { tools = ["gen"], host-module = true } |
the program and the library |
[build-dependencies] x = { host-module = false } |
neither; the directory of x (mcpp::dep_dir) and its payloads |
[dependencies] x = { tools = ["gen"] } |
the library in the target, and the program |
The name host-module is older than its meaning: it was introduced when a build
program received the lib-root module of a dependency and nothing else, and a key
that has reached published descriptors is not renamed (SPEC-004 §5.2). A library
used by a build program is compiled as the package it is: its include
directories, defines, features, dependencies and implementation units take part,
and the build program is compiled by the same plan (30 — build.mcpp).
One package in both tables. A package written in [dependencies] and in
[build-dependencies] of one manifest has two instances, one compiled for the
target and one for the build program. Both have the version the lock records.
Their features are activated separately: a feature requested only by a
[build-dependencies] edge does not reach the target instance.
A reading kept for compatibility. A [dependencies] entry that asks for
host-module = true, or for a feature that declares rule_module, is read as a
[build-dependencies] entry: the package serves the build program and not the
target, as it did before mcpp 2026.10.11.1. The reading is deprecated since
mcpp 2026.10.11.1 and removed from mcpp 2027.1 (host-module-in-dependencies,
51 — Supported Versions); mcpp why compat lists its uses.
A package of programs contributes only its programs (mcpp 2026.9.16.1+). A
dependency whose declared [targets] are all programs (bin, app, test)
has nothing to link. Its tools are built by the tool sub-build, which resolves
the package as its own root, and in the consumer's graph it provides its tools
and its directory and nothing else: its own dependencies are not resolved
there, its sources are not compiled there, and its ldflags do not reach the
consumer's link. A program may therefore depend on the package that requests
it, which is how an SDK provides a program built against itself. A package
that declares no [targets] table is unaffected, even when a src/main.cpp
infers a program for it.
A cycle among packages is refused where the graph is resolved, naming its edges, under every cache mode; a tool whose own sub-build requests it again is refused at that first repetition, naming the chain.
Unlike [dev-dependencies], build dependencies are walked transitively and
resolved with the project, one version per package: a build dependency's own
dependencies are what make it work, and they serve the build program with it.
A feature scopes build-time requests without a second declaration site. A
[feature-deps.<name>] entry may restate a dependency already declared
unconditionally, with the same source, and add tools to it, so "only when
needed" needs no separate table:
[dependencies]
spike.installer = { path = "../installer" }
[feature-deps.installer]
spike.installer = { path = "../installer", tools = ["installer"] }The restatement names its source because every dependency table does: an entry
without path, git, version or workspace is read as a namespace table
and refused, and the refusal says to restate the source. tools, features,
host-module and reexport of the restatement are added to the declaration in
effect on the row. A restatement that names another source is refused, naming
both sources (mcpp 2026.9.16.1+); before that release it was ignored.
Which compiler builds a tool (mcpp 2026.9.27.1+). The build that requests
a tool decides its toolchain once: --toolchain when given; otherwise the tool
package's own declaration, read as its own build reads it (for a workspace
member, after the workspace root's [toolchain], [target.<triple>] and
[indices]), its host row's toolchain before [toolchain]; otherwise the
host toolchain the requesting build compiles its build programs with. The
choice is passed to the tool's sub-build and recorded in the tool store key, so
mcpp build -p <tool> in the workspace and the same tool built for a consumer
use the same compiler. The source digest of a tool package skips directories
that hold their own mcpp.toml, so editing a workspace member does not rebuild
a tool the workspace root provides.
A package whose feature needs one of its own programs on the build machine states it on the feature, and a consumer names only the feature:
# the tool package
[features.codegen]
tools = ["codegen"]
[targets.codegen]
kind = "bin"
main = "src/codegen.cpp"# the consumer
[dependencies]
toolpkg = { path = "../toolpkg", features = ["codegen"] }Enabling the feature has the effect of tools = ["codegen"] on the edge: the
program is built for the host and mcpp::dep_bin("toolpkg", "codegen") names
it. A consumer that does not enable the feature builds nothing. An entry that
names no bin target of the package is refused when the manifest loads, and the
message lists the package's bin targets.
tools builds a program for the machine that runs the build. A program that
ships with the consumer and runs on its target (an updater, a helper process)
is requested with artifacts:
[dependencies]
updater = { path = "../updater", artifacts = ["updater"] }- The dependency's
bintarget is built for the consumer's target and profile, as a link unit of the consumer's plan, into the consumer'sbin/. Under--target x86_64-linux-muslit is a musl program. - None of the dependency's code is linked into the consumer through this edge. A package also reached through an ordinary edge is linked as usual.
- An action of the consumer's build program names the program with
${mcpp.artifact:updater/updater}, in its arguments and its inputs; a name that matches noartifactsentry fails planning and names the placeholder. mcpp rundoes not choose it, andmcpp packstages it beside the consumer's program.
The section has been parsed since early versions and, until 2026.8.29.1, read by nothing that made a decision: writing it produced a manifest that loaded, no diagnostic, and no effect.
A module name identifies one module within one program. The compilers name a
module's entities and its initializer after the module, so a program cannot
link two modules of one name. A build can hold several programs (a package and
the programs it ships through artifacts, the members of a workspace), and
each of them may have its own module of one name.
- An import is resolved within the importing package's closure: the package
and every package it reaches through its dependencies. The closure does not
follow
artifacts,toolsor[build-dependencies]edges, whose programs are built separately. - Two packages that one program links may not provide the same name. The build is refused, and the message names the package whose closure holds both.
- Every package's BMIs lie below the package's directory in the build
directory,
gcm.cache/<package>/(pcm.cachewith Clang), except the root package's, which lie at their names, as object files do (2026.10.1.2+). A compile that imports a module of another package is told where its BMI is, and where the BMIs of the modules that one imports are, through one module map: a mapper file with GCC (-fmodule-mapper=), and an argument file of-fmodule-file=lines with Clang or/referencelines with MSVC (@<build directory>/modmap/<package>-<hash>.modmap). The map lists what the unit reaches through its imports and nothing else, so the command does not change with the rest of the graph. A project whose modules are all its own is laid out, and every command spelled, as before. - Until 2026.10.1.2 a BMI moved below its package's directory only when two
packages of the graph provided its name, and such a package was not served
from the global dependency cache. Both depended on which packages a command
selected, so a workspace member was compiled again at each switch between
-pand--workspace(mcpp#751). - clangd finds a module by its name in the compilation database, so for a name two packages provide it may show the other program's module. The build is not affected.
A module that several programs share is best provided by one package that the others depend on; it is then compiled once.
Two fields of a dependency edge use a package's source without copying it.
[dependencies]
"compat.zlib" = { version = "1.3.2", patches = ["patches/0001-fix.patch"] }
"compat.mimalloc" = { version = "3.6.1", embed = true, exclude = ["*/src/prim/prim.c"] }patches are unified diffs (diff -u, git format-patch), named relative
to the manifest that declares them and applied in order to the package's
verified source.
-
A patched library is another library, a variant: its identity is the package, its version and the patch set, named by the SHA-256 over each patch's SHA-256 in order (not by the files' names). Its source is a tree of its own,
cache/src/<package>/<version>+p<digest>under the mcpp home, and its build, its BMIs and its cache entries are its own. One patch set carried by two packages is one variant. -
The applier is mcpp's and is exact: a hunk applies where its header says, every line of context and every line it removes byte for byte, or the build is refused naming the patch, the file, the hunk and the line. Nothing is searched for elsewhere and no line ending is converted. Paths in a patch carry one leading directory (
a/,b/), which is removed. Renames, copies and binary changes are refused. -
A patch names files from the package's root: the directory of the package's resolved manifest inside its archive (
run/of an archive whose descriptor readsmcpp = "*/run/mcpp.toml"), so that one patch applies to every version whose layout is the same.patch_root = "<dir>"on the edge names another directory, relative to the package's root and inside its source. A patch written from the archive's top, as before 2026.11.9, applies as written, and the build says so. -
mcpp why depsreports each dependency's root as the build reads it (its patches applied). A patch is the difference git writes from a copy of that tree:cp -R <root> /tmp/zlib && cd /tmp/zlib git init -q && git add -A && git commit -qm base # edit, then git diff > <project>/patches/0001-fix.patch
and the edge lists it in
patches. A further patch is written the same way from the root the patched build reports, and is listed after the first. -
Version and git dependencies may carry patches; a
pathdependency is a tree its consumer owns and is refused with them. -
mcpp.locknames the variant and each patch by its path and SHA-256;mcpp emit sbomstates them as the component's pedigree. -
One program holds one variant of a package. Two programs of one configuration (members of a workspace) may each use their own; a program that reaches two variants of one package, or a variant and the package without patches, is refused, naming them.
-
The variant's tree is collected as the cache's entries are: by
mcpp cache gcandprune, and after a build once unused for[cache] unused_days.
embed = true compiles the dependency into its consumer.
- The dependency's sources, as its description selects them and less
exclude(globs relative to the dependency's root), are compiled as the consumer's, with the consumer's flags and, for those files, the dependency's include directories and compile flags. - The dependency builds nothing of its own in that configuration: every other edge to it takes its interface, and the embedder supplies its definitions. A program that reaches it without its embedder is refused, naming both.
- Its link flags and libraries are not the program's: the embedder supplies the definitions, over the operating-system layer it chose, and states what they link against.
excludeis allowed only withembed: a library built on its own that compiled fewer files would change for every consumer, which is a patch's to state.
A C library uses embed to carry an allocator or another dependency that would
otherwise need the C library itself to be built. A package that uses either
field states mcpp = ">=2026.11" in [package], so that an older mcpp refuses
it rather than building it without its patches.
- Two things need the network, and only two: resolving a branch that has no
commit in the lock, and cloning a commit that is not cached yet. A
git =value naming a local directory or afile://URL needs neither and is never refused offline. - The index-refresh window does not apply to a selector that states a
namespace.
mcpplibs.gtestmisses and stays missed until the next refresh. mcpp.lockrecords and verifies a resolution; it does not constrain one. See 51 — Supported Versions and Compatibility.