English | 简体中文
Most projects need nothing more than mcpp.toml. When you need build-time logic —
probe the host, generate a source, decide a flag from the environment — put a
build.mcpp in your project root. It is the mcpp analog of Zig's build.zig and
Cargo's build.rs, but written in C++: no second language, and it dogfoods
mcpp itself.
mcpp compiles build.mcpp with your toolchain and runs it before the main
build. The program talks to mcpp by printing mcpp: directives to stdout; those
directives augment the build.
// build.mcpp
#include <cstdio>
#include <fstream>
int main() {
// Generate a source the main build will compile + link.
std::ofstream("src/generated.cpp") << "const char* banner() { return \"hi\"; }\n";
std::puts("mcpp:generated=src/generated.cpp"); // add it to the build
std::puts("mcpp:cxxflag=-DHAVE_BANNER=1"); // define a macro for all C++ TUs
if (std::getenv("USE_FAST")) std::puts("mcpp:cxxflag=-DFAST_PATH=1");
std::puts("mcpp:rerun-if-env-changed=USE_FAST"); // re-run me when USE_FAST changes
return 0;
}mcpp build # compiles + runs build.mcpp, then builds the projectPrint these to stdout (one per line). Any line that does not start with mcpp:
is ignored, so you can freely log diagnostics.
| Directive | Effect |
|---|---|
mcpp:cxxflag=<flag> |
add <flag> to the C++ compile flags |
mcpp:cflag=<flag> |
add <flag> to the C compile flags |
mcpp:link-lib=<name> |
link -l<name> |
mcpp:link-search=<dir> |
add a library search dir (-L; relative dirs resolve against the project root) |
mcpp:cfg=<name> |
define -D<name> for both C and C++ |
mcpp:generated=<path> |
add a generated source (relative to the project root) to the build |
mcpp:source=<path> (0.0.100+) |
select a pre-existing source file into the build (absolute, or relative to the package root). Same downstream effect as generated=; use it for files the program chose (payload/vendored tree) rather than wrote — e.g. a per-target source selection over a large tarball |
mcpp:include-dir=<dir> (0.0.100+) |
add a private include directory (-I) for this package's own TUs (absolute, or relative to the package root; normalized). Replaces the cxxflag=-I + cflag=-I double emission |
mcpp:include-dir-after=<dir> (0.0.100+) |
like include-dir, but searched after the system directories (-idirafter) — for payload trees that shadow system headers |
mcpp:rerun-if-changed=<path> |
re-run build.mcpp when this file changes |
mcpp:rerun-if-env-changed=<VAR> |
re-run build.mcpp when this env var changes |
The program requests build edges (flags, libraries, sources). It cannot add a
registry dependency — keep your dependency graph declarative in mcpp.toml
(including platform-conditional [target.windows.dependencies]). build.mcpp
is for leaf decisions: flags, codegen, link requirements.
include-dir/include-dir-after are deliberately private (Cargo
discipline): they color only this package's own TUs and are never propagated
to consumers. An include directory consumers must see is part of the public
interface and belongs in the declarative manifest/descriptor
([build] include_dirs), not in a build-time program.
Instead of printing raw strings you can write build.mcpp modules-first —
import mcpp;, no #include, no import std;. The mcpp module is bundled in the
mcpp binary (so it always matches your mcpp's protocol) and is compiled on demand;
its functions just emit the directives above:
// build.mcpp
import mcpp;
int main() {
mcpp::cxxflag("-DHAVE_BANNER=1");
mcpp::link_lib("m"); // -lm
mcpp::link_search("vendor/lib"); // -L…
mcpp::define("HAVE_FEATURE"); // == mcpp:cfg= → -DHAVE_FEATURE
mcpp::generated("src/gen.cpp");
mcpp::rerun_if_changed("config.h");
mcpp::rerun_if_env_changed("USE_FAST");
}| Function | Emits |
|---|---|
mcpp::cxxflag(s) / mcpp::cflag(s) |
mcpp:cxxflag= / mcpp:cflag= |
mcpp::link_lib(s) / mcpp::link_search(s) |
mcpp:link-lib= / mcpp:link-search= |
mcpp::define(s) |
mcpp:cfg= (i.e. -D<s>) |
mcpp::generated(p) |
mcpp:generated= |
mcpp::source(p) |
mcpp:source= |
mcpp::include_dir(d) / mcpp::include_dir_after(d) |
mcpp:include-dir= / mcpp:include-dir-after= |
mcpp::rerun_if_changed(p) / mcpp::rerun_if_env_changed(v) |
the matching rerun-* directives |
If your build.mcpp also needs to write a generated file, mix in a textual
#include <fstream> — that's fine; only import std; is unnecessary. The raw
stdout protocol above remains the low-level substrate; import mcpp; is the typed
layer over it.
The running program receives the build context as MCPP_* variables
(Cargo's env-family equivalent), also exposed through typed readers:
| Variable | Typed reader | Value |
|---|---|---|
MCPP_TARGET |
mcpp::target() |
resolved canonical triple (the --target triple under cross; the host triple natively) |
MCPP_TARGET_OS (0.0.100+) |
mcpp::target_os() |
the target's OS segment (linux/macos/windows) — no need to hand-split MCPP_TARGET |
MCPP_TARGET_ARCH (0.0.100+) |
mcpp::target_arch() |
the target's arch segment (GNU spelling: x86_64, aarch64, …) |
MCPP_TARGET_ENV (0.0.100+) |
mcpp::target_env() |
the target's env segment (gnu/musl/msvc); empty string when the triple has none (macOS) |
MCPP_HOST |
mcpp::host() |
the host triple |
MCPP_PROFILE |
mcpp::profile() |
effective profile name (dev/release/…) |
MCPP_OUT_DIR |
mcpp::out_dir() |
a writable scratch/output dir owned by mcpp |
MCPP_MANIFEST_DIR |
mcpp::manifest_dir() |
the package root (= CWD) |
MCPP_FEATURE_<NAME> |
mcpp::has_feature("name") |
set to 1 per active feature (same <NAME> sanitization as the MCPP_FEATURE_ compile macro) |
MCPP_FEATURES |
— | comma-separated active feature list |
MCPP_DEP_<NAME>_DIR |
mcpp::dep_dir("name") |
the resolved install dir of each declared dependency (canonical and namespace-stripped name spellings; same <NAME> sanitization as MCPP_FEATURE_). Received by dependencies' build.mcpp and the root project's (the root runs after dependency resolution, 0.0.100+) |
These values are folded into the re-run key unconditionally — changing the
target, profile, or feature set re-runs the program without any
rerun-if-env-changed declaration.
A dependency that ships a build.mcpp gets it compiled and run too (the
Cargo build.rs model — building a package means trusting its build program),
after its features are resolved and before the source scan. Scope follows
Cargo: cxxflag/cflag/cfg directives color only that package's own
TUs; link-lib/link-search reach the final link. Its artifacts (binary,
cache, MCPP_OUT_DIR) live in the consuming project's
target/.build-mcpp/deps/<pkg>@<ver>/ — a registry package root is shared
across projects (and may be read-only), so it is never written to; relative
generated= paths resolve against MCPP_OUT_DIR, not the package root.
mcpp does not re-run build.mcpp on every build. It caches the program's
directives and re-runs only when something it depends on changed:
- the
build.mcppsource itself, - the toolchain,
- any file you declared with
rerun-if-changed, - any env var you declared with
rerun-if-env-changed, - (or a
generatedoutput /source=selection went missing).
So declare your inputs: if your program reads config.h or the USE_FAST
variable, emit mcpp:rerun-if-changed=config.h / mcpp:rerun-if-env-changed=USE_FAST.
This replaces the old "process exited 0, so assume it's fine" guesswork with an
explicit input/output contract — incremental builds stay correct.
When nothing changed you'll see build.mcpp up to date (cached); otherwise
build.mcpp compiling / running.
- Runs on the host — including under cross (mcpp 0.0.95+). Under
mcpp build --target <triple>the program is compiled with a host-resolved toolchain, runs on the host, and seesMCPP_TARGET= the cross triple. For purely declarative target gating,[target.'cfg(...)']tables remain the first choice — see 05 - mcpp.toml Manifest Guide. - CWD is the project root, so relative paths (
src/generated.cpp) land where you expect. - A non-zero exit from
build.mcppaborts the build and prints its output.