Skip to content

Latest commit

 

History

History
1314 lines (1043 loc) · 63.4 KB

File metadata and controls

1314 lines (1043 loc) · 63.4 KB

04 — The mcpp.toml Manifest

Reader: an author writing or reading a manifest.

The question this chapter answers: what may an mcpp.toml say, field by field.

Not here: four topics this file's tables belong to but this chapter does not own — dependencies are 05, features are 06, conditioning on a target is 22, and the project's environment is 23. Each is named where its table would be.

mcpp.toml is the project configuration file for the mcpp build tool, analogous to Cargo's Cargo.toml or Node's package.json. Place it in the project root; mcpp build discovers and reads it automatically.

1. Minimal Examples

mcpp is designed around convention over configuration — most fields have sensible defaults, so the simplest mcpp.toml is just a few lines:

1.1 Executable (minimal)

[package]
name    = "hello"
version = "0.1.0"

mcpp infers automatically:

  • Source files: src/**/*.{cppm,cpp,cc,c,S,s,asm}
  • Entry point: src/main.cpp → produces the hello binary
  • Standard: C++23
  • Modules: scans export module ... declarations and builds the dependency graph automatically

1.2 Library project (minimal)

[package]
name    = "mylib"
version = "0.1.0"

[targets.mylib]
kind = "lib"

lib-root convention: the primary module interface defaults to src/mylib.cppm (the last segment of the package name).

2. Full Field Reference

2.1 [package] — Package Metadata

[package]
name        = "myapp"              # Package name (required)
version     = "0.1.0"              # Semantic version (required)
standard    = "c++23"              # C++ standard (default c++23; can be set to c++20 / c++26)
description = "My awesome app"     # Description (optional)
license     = "MIT"                # License (optional)
authors     = ["Alice", "Bob"]     # Author list (optional)
repo        = "https://github.com/user/myapp"  # Repository URL (optional)

standard is the first-class setting for the C++ language standard. Recommended values:

  • c++23: the default, suited to the current module-based default templates.
  • c++20: the lowest level mcpp accepts — named modules are a C++20 feature, so nothing below it exists for this build model. Use it when an external constraint (an older in-house rule, a third-party API that stops at C++20) forces the level down. import std; still works there: it is a C++23 library feature, but GCC (≥ 15), Clang + libc++ (≥ 17) and the MSVC STL (from VS 2022 17.8) all provide the std module in C++20 mode as well. Note that C++23 library facilities (std::print, std::expected, …) are not available, including in the code generated by mcpp new.
  • c++26: for C++26 language features.
  • c++2a / c++2c: compatibility aliases, normalized to c++20 / c++26 after parsing.
  • gnu++20 / gnu++23 / gnu++26: GNU dialects; the choice enters the fingerprint and the std BMI cache key.
  • c++latest: resolves to the newest standard level the resolved toolchain supports. Good for local experimentation, but not recommended for release packages that require reproducibility.
  • c++fly: c++latest plus every experimental standard feature the resolved toolchain can enable (language + standard library). On GCC ≥ 16 this turns on C++26 reflection (-freflection) and contracts; on Clang/libc++ it adds -fexperimental-library; unsupported gates are skipped with a printed summary. Deliberately toolchain-dependent — the bleeding-edge playground mode, never for published packages.

Two properties worth knowing:

  • The standard is module-graph-global. The root package's standard applies to every translation unit in the build, dependencies included — a dependency's own standard is not used when it is being built as a dependency. This is not a simplification: BMIs are not compatible across levels (GCC rejects them with language dialect differs), so a single graph physically cannot hold two levels.
  • Levels never share caches. The standard is part of the fingerprint, the import std BMI identity and the dependency build-cache key, so switching between c++20 and c++23 gives each level its own target directory and its own std BMI instead of a corrupt hit.

If the sources import std; at a level the resolved toolchain does not provide the std module for, mcpp fails before compiling and names both the toolchain and the project level.

Both spellings of the value are accepted: standard = "c++26" and standard = 26.

When a dependency declares a level above the graph's, mcpp says so before compiling rather than letting it fail somewhere inside that dependency's sources. See workspace §4.2.

Dialect flags and the import std BMI

Some flags change what the standard library's headers declare, so the precompiled import std BMI has to be built with them too. That is what [build] dialect_cxxflags is for: it is applied to the std BMI prebuild, the module scan and every translation unit in the graph, including dependencies.

[build]
dialect_cxxflags = ["-fno-exceptions"]

mcpp promotes a few flags into that channel automatically when it finds them in cxxflags (-freflection, -fchar8_t, -D_GLIBCXX_USE_CXX11_ABI=…) — a graph that mixes those is ill-formed anyway, so no dependency can hold a different opinion about them.

-fno-exceptions and -fno-rtti are not promoted, because a dependency can legitimately disagree: they remove a language facility the dependency may use, and the consumer cannot make that choice on its behalf. Left in cxxflags they reach every TU and not the prebuild, so the build cannot succeed — mcpp refuses it before compiling and names the key:

error: `-fno-exceptions` changes the language dialect, but the `import std` BMI is
       precompiled without it, so every importing translation unit will fail with
       "language dialect differs".
       Declare it as a dialect flag instead:

         [build]
         dialect_cxxflags = ["-fno-exceptions"]

The check reads the effective flags, so it fires for the same flag written in [profile.<name>] cxxflags or in a [target.…] block. It does not fire when nothing in the graph imports std, where the flag is an ordinary per-unit option that works.

2.2 [targets.<name>] — Build Targets

# Executable (default; inferred automatically when src/main.cpp exists)
[targets.myapp]
kind = "bin"
main = "src/main.cpp"       # Optional, defaults to src/main.cpp

# Static library
[targets.mylib]
kind = "lib"

# Shared library
[targets.mylib]
kind = "shared"
soname = "libmylib.so.1"  # Optional: Linux/ELF ABI name; an alias of the same name is generated at runtime

soname is the ABI name for a shared library, analogous to SOVERSION/SONAME in Autotools/CMake. On Linux, mcpp passes -Wl,-soname,<name> to the linker and generates a <name> -> lib<target>.so alias in the output directory, so that downstream programs can load the library via its standard ABI name through DT_NEEDED or dlopen(). This field only applies to kind = "shared", and the value must be a filename basename.

Shared-library targets work on all three binary formats. ELF gets a .so with its soname and a $ORIGIN search path; Mach-O gets a .dylib whose install name is @rpath/<file>, so it survives being moved; PE gets both the .dll the loader opens and the import library the linker consumes, with the export list generated from the objects on the MSVC ABI (which exports nothing without __declspec(dllexport) or a .def). See tests/e2e/08, 257 and 259.

exports — the artifact’s published symbol set (mcpp 2026.9.6.5+)

[targets.mydriver]
kind    = "shared"
soname  = "libmydriver.so.1"
exports = "abi/mydriver.exports"     # or inline: exports = ["vk_icd*"]

Omitting the key publishes everything, which is what both platforms already do — ELF gives symbols default visibility, and PE gets an auto-generated .def listing every symbol. exports narrows that.

Two projects need the narrowing. A runtime with a stable ABI publishes a reviewed set and nothing else, so that what is not in the set stays free to change. A plugin loaded beside its rivals must not collide: a Vulkan ICD is found by name for vk_icdGetInstanceProcAddr, and one that also exports its internals collides with the loader and with the other ICDs in the process.

The file lists one symbol pattern per line, # starts a comment, and * is the only wildcard. The inline array says the same thing and is for the two or three entry points where a separate file would be ceremony.

One statement, three renderings:

Platform Rendered as
ELF a version script, -Wl,--version-script=
Mach-O -Wl,-exported_symbols_list (the leading underscore is supplied by the engine)
PE the .def, replacing the auto-generated all-exports one

It does not change compile-time visibility, and that is deliberate. The narrowing is a link-time property on all three formats, so one key has one effect. -fvisibility=hidden remains available through [build] cxxflags for the code-generation benefit it brings, and it is a separate decision because it also changes how this library's own translation units see each other.

Symbol versioning is not this key. foo@@LIB_1.0 alongside foo@LIB_0.9 is an ELF-only capability that cannot be stated neutrally; a package that needs it writes the version script itself and passes it through [build] ldflags, or computes it and emits mcpp:link-flag= (docs/07).

A soname is meaningful on kind = "lib" too — see dependency_linkage below, where the form a library takes becomes the consumer's decision.

Per-target keys

[targets.server]
kind     = "bin"
main     = "src/server.cpp"
defines  = ["BUILD_SERVER=1", "PORT=8080"]   # -D macros, applied to this target's entry only
cxxflags = ["-Wno-deprecated-declarations"]  # extra C++ flags for this target's entry (no -std=...)
cflags   = ["-DPURE_C"]                       # extra C flags for this target's entry

[targets.gui]
kind = "bin"
main = "src/gui.cpp"
required_features = ["gui"]                   # only built when feature `gui` is active
Key Meaning
defines Preprocessor macros (name or name=value); desugar to -D<x> on both the C and C++ entry compile.
cxxflags / cflags Extra compile flags for this target. Do not put -std=... here — use [package].standard.
required_features The target is emitted only when every listed feature is active in the build; otherwise it is silently skipped. A gate only — it does not activate features (use --features / [features].default). One exception, and it is not a second rule: when this target is requested as a host tool (tools = [...], §2.14), the target is what was asked for, so its required_features become the sub-build's inputs. Same field, one meaning — the resolution just runs in the opposite direction.

Scope (important): defines / cxxflags / cflags on a target apply only to that target's exclusive entry source (its main) — never to shared module/impl objects, which are compiled once and linked into every target (mcpp's compile-once model). They are the right tool when the flag only needs to affect a single binary's (or test's) own entry — for example a per-test contract evaluation semantic (-fcontract-evaluation-semantic=observe) for a test whose main exercises the violation, a feature macro the entry alone reads, or a local warning suppression. If a flag must reach shared code, it does not belong here — split into a workspace member or use [features], or for a whole-build mode use a [profile.*] (mcpp test --profile <name> builds the whole test image, code-under-test included, under that profile).

Unsupported keys under [targets.<name>] are reported as a warning (an error under --strict).

Choosing where build configuration goes — when more than one binary must differ:

You want Use
Different macros/flags on a binary's own entry per-target defines / cxxflags (above)
Two products that differ in code they share split into workspace members, each with its own [build] flags over a shared lib
To select a variant of a shared library (e.g. a backend) [features] on that library (§2.8) — additive, reaches the library's own compile
A whole-build mode (sanitizers, contract semantics, opt level) [profile.<name>] (§2.9) + --profile; also honored by mcpp test --profile <name>

mcpp deliberately does not compile a shared source two different ways within one build: a source maps to one object (and one BMI for modules), so divergence that must reach shared code belongs at the package/feature boundary, not on an individual target.

2.3 [build] — Build Configuration

Every entry sources matches must produce an object that gets linked. A file mcpp cannot place — an extension outside the built-ins and outside module_extensions — is refused, naming the file, the extension and the key. It is not ignored, because the failure that produced this rule was not "one file too many" but compiled and then linked by nobody: the scanner reads export module and gives the edge a BMI while the classifier says the file has no role, and what the author sees is undefined reference to a module-mangled symbol. Headers belong in include_dirs; Windows resource scripts in [resources].

sources = [] is not the same as omitting sources. An absent key selects the default glob; an explicitly empty list means compile nothing, which is what a header-only distribution package needs to say. Until mcpp 2026.8.18.1 the two were byte-identical, so there was no spelling for "nothing" and any file left under src/ was swept in.

A sources entry may carry the accelerator it is for (2026.9.5.2+): { glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }. The glob joins the list like any other; the constraint decides whether it applies to a given build. It must match at least one file (an empty match is refused: it would leave nothing to compile for that device and say so only at the link). Under --no-accel the glob is left out, which is how one project yields its CPU-only variant. Under an --accel that does not cover the constraint the build is refused naming both (accel-mismatch). Device-kind files the effective set matches — CUDA and HIP, the GLSL stages, HLSL, OpenCL C and Metal, listed in full in 42 — Heterogeneous Builds — are never compiled by the engine; they reach the build program as MCPP_DEVICE_SOURCES, where the rule package the project imports turns each into an mcpp::action.

[build]
sources      = ["src/**/*.cppm", "src/**/*.cpp"]  # Source globs (default: src/**/*.{cppm,cpp,cc,c,S,s,asm})
module_extensions = [".ixx"]      # Extra extensions used by module INTERFACES (§ below)
build_program_timeout = 1800      # Seconds a build.mcpp may run; 0 = no limit (§ below)
include_dirs = ["include", "third_party/include"]  # Header search paths
include_dirs_after = ["*"]         # Header dirs searched AFTER system dirs (-idirafter)
private_include_dirs = ["vendor/src/include"]  # Of `include_dirs`, the ones a consumer must NOT get
c_standard   = "c11"              # Standard for C source files (default c11)
cflags       = ["-DFOO=1"]        # Extra C compile flags
cxxflags     = ["-DBAR=2"]        # Extra C++ compile flags (do not put -std=... here)
ldflags      = ["-lfoo"]          # Extra link flags
defines      = ["BIZ=1", "QUX"]   # Preprocessor macros for every TU (desugars to -D; reaches module scans)
cxx_runtime  = "self-contained"   # C++ runtime contract (§ below); static_stdlib is the old spelling
target       = "x86_64-linux-musl" # Default build target when no --target is passed
                                   # (≙ cargo build.target; e.g. "ship fully-static")
macos_deployment_target = "14.0"   # Minimum supported OS version for macOS artifacts (macOS only)
dependency_linkage = "static"     # How dependencies arrive: static (default) | shared (§ below)
cache        = "global"           # Global dependency cache: global (default) | local | off (§2.10)
jobs         = "auto"             # Concurrent compiles: a positive number, or "auto" (§ below)
bmi_schedule = "auto"             # Module-edge scheduling: auto (= off) | on | off (§ below)

dependency_linkage — static or shared is the consumer's decision

[build]
dependency_linkage = "shared"        # whole-graph default; "static" is the default default

[profile.dev]
dependency_linkage = "shared"        # per profile

[dependencies]
"compat.zlib" = { version = "1.3.2", linkage = "shared" }   # one package

Until mcpp 2026.8.28.2 a dependency had exactly one shape and the package author chose it: kind = "lib" merged its objects into every consumer's link, kind = "shared" produced a real shared library. That is the wrong owner for the decision. Whether a library should be a separate file at run time is a property of the program being built — how it is shipped, how often it is relinked, whether something else in the process already provides that library.

  • static (default) — the dependency's objects are merged into the images that use it. Byte-for-byte what mcpp has always done; a project that does not write this key builds exactly as before.
  • shared — mcpp builds the dependency as a shared library beside the artifact and links against it, with $ORIGIN (ELF) / @loader_path (Mach-O) / the executable's own directory (PE) finding it again after the build directory moves.

This is not [target.<triple>].linkage (§2.7.1). That key answers the same-sounding question about the C library (a musl -static link, MSVC's /MT). The two are not independent, and the direction matters: a fully static image has no interpreter, so it cannot load a shared object at all. On a target whose C library is linked statically — which is the default for musldependency_linkage = "shared" is refused, and says so.

A package can say it must be one form, and only for a real reason:

The package writes mcpp reads it as
[targets.<n>] kind = "shared" must be shared — something else in the process will dlopen it, so there may only be one copy (X11, a Vulkan loader)
ldflags containing -L must be static — the package ships prebuilt archives mcpp did not compile and cannot place inside a shared object it builds
a packaged library (mcpp pack) whichever legs it actually ships, from [[runtime.artifacts]] role
anything else either form

kind = "lib" is not a constraint: it is the default value, and most packages write it without choosing anything. Absence of a statement is not a statement.

A per-dependency linkage is honoured only in the root project's [dependencies]. A package deep in the graph does not get to decide how the final program is laid out; one that genuinely must be a single shared copy says so on its own target instead.

soname on a library target

A soname (§2.2) may be declared on kind = "lib" as well as kind = "shared". It is the name a library is found by, and it is the only way mcpp's build of a package and a third party's copy of the same library can resolve to one file instead of two — which a package cannot state if declaring it forces the package to stop being consumable as a static library.

A descriptor that writes soname on a non-shared target cannot be read by mcpp releases before 2026.8.28.2 — the whole manifest fails to load, not just the key. Publishing one to an index therefore waits for that floor to move.

The symbol-provision check

After a link, mcpp asks whether every symbol in the image has exactly one provider. On ELF an executable is searched first, so a library statically merged into the program wins for every symbol it shares with a shared library loaded beside it — the shared copy is never called, and code inside that library runs against a build it was not linked against. No linker or loader diagnostic exists for this.

The check is a measurement, not a declaration: it reads the produced image's dynamic symbol table, removes the entries that are copy relocations, and reports only those a library in the artifact's own closure also defines. An arrangement with one copy in the process is silent. The verdict is recorded in target/<triple>/<fp>/resolution.json under runtime.symbol_provision, with the count and its denominator, so CI can read it without readelf.

It is a warning by default and an error under --strict. The ways out are ordered, and the order matters:

  1. Stop one side from providing it — usually a package shipping a copy of a library the graph already builds. Always correct.
  2. Make both resolve to one file by declaring the library's real soname on its target.
  3. dependency_linkage changes which form mcpp builds. It removes this finding, but on its own it can leave two copies loaded instead of one: measured on a graph staging glib (whose libgio needs libz.so.1) beside a statically built compat.zlib, switching the form dropped the executable's 88 exported symbols and then loaded both libzlib.so and libz.so.1. It unifies the two providers only when (2) holds as well.

private_include_dirs names the entries of include_dirs that stop at this package's own boundary: this package compiles with them, and a consumer never receives them.

Almost every package publishes exactly the set it is built from, which is why include_dirs alone was enough for a long time. The shape where the two differ is a package that vendors a library with an internal header overlay. musl reaches its own declarations through src/include, whose headers define hidden, weak and weak_alias — names that mean something only to musl's own sources. Publishing that directory hands those macros to every consumer, and a consumer that uses hidden as an ordinary identifier stops compiling for a reason it has no way to see.

[build]
# The relative ORDER of the two kinds is load-bearing: the internal overlay
# must precede the public headers for this package's own build. That is why
# this is a SUBSET of `include_dirs` rather than a second list — two arrays
# cannot express one order.
include_dirs         = ["port/include", "musl/src/include", "musl/include"]
private_include_dirs = ["musl/src/include"]

Entries take the same * glob convention as include_dirs, and are matched after expansion — so a glob may name exactly the directories it expands to. An entry that is not among this package's include_dirs withholds nothing and is reported as such rather than passing in silence.

On an older engine the key is ignored, never fatal. Measured on 2026.8.26.2: in a dependency's manifest it is accepted silently, and in a root manifest it warns — [build] has unsupported key 'private_include_dirs' (ignored) — and the build continues. So a package may adopt the key without waiting for its consumers to upgrade; those on an older engine simply keep receiving the directory as they did before. The one place this does not hold is a published xim descriptor's target_cfg block, where an unrecognised sub-key is a hard error that fails the whole manifest — do not put this key there until the index floor names an engine that knows it.

include_dirs_after (#249) lists header directories that are searched after the toolchain's system directories (emitted as -idirafter on GCC/Clang, as trailing /I under the MSVC dialect, and as plain -I for NASM assembly units — neither has an equivalent, and neither has a system-header chain to protect). Use it instead of include_dirs when the directory is an extracted source-tarball root that contains files whose names collide with standard headers — e.g. ffmpeg's top-level VERSION file shadows libc++'s <version> on case-insensitive macOS filesystems when the root is put on -I. With include_dirs_after the system header always wins while the package's real headers (<libavutil/frame.h>) remain findable. Entries support the same * glob convention as include_dirs, and they propagate to dependent packages along the same edges — consumers receive them as after-dirs, never upgraded to -I.

macos_deployment_target sets the minimum system version in the artifact's Mach-O header (LC_BUILD_VERSION minos), i.e. the oldest macOS the binary can run on. The precedence follows ecosystem convention: the MACOSX_DEPLOYMENT_TARGET environment variable (an explicit per-invocation override, honored the same way by cargo/rustc, cc, etc.) > this field (the project default, similar to SwiftPM's platforms:) > the built-in default 14.0 (rustc-style — every target has a baseline, and 14.0 is the floor of LLVM's official static libraries themselves). This value enters the BMI fingerprint, so switching targets automatically rebuilds the module cache.

Build concurrency (jobs) and module scheduling (bmi_schedule)

[build]
jobs         = "auto"    # or a positive number; --jobs / MCPP_JOBS override it
bmi_schedule = "off"     # auto (default, = off) | on | off

jobs is how many compiles run at once. "auto" is resolved against the machine doing the build, never frozen into the manifest: it takes the physical core count on a heterogeneous CPU (a 13900K is 8 P-cores + 16 E-cores, so its 32 threads are not 32 equal workers) and clamps that by free memory, because a single module interface compile peaks at 0.5–1.0 GB. Precedence is --jobs / MCPP_JOBS > this key > the backend's own default. A malformed value is reported, never silently treated as the default — a typo that quietly restores the default is a build slower than requested, with no indication why.

bmi_schedule decides when importers are unblocked.

value
"auto" the default, and it currently means OFF
"on" split the module edge: importers start when the BMI is published, not when the compiler exits
"off" one edge per module

Only those three spellings are accepted. "ON", "true" and "yes" are rejected with a diagnostic rather than quietly meaning off — and they are not harmless typos: the value enters the build fingerprint, so a rejected spelling used to select a different build directory (a full rebuild) while changing nothing about the schedule.

Why auto is off. 86% of a module interface compile is code generation that no importer reads, so publishing the BMI early is worth a lot — measured on mcpp itself, cold 86.7s → 35.7s and edit-body 80.9s → 29.8s. But a scheduling change that is wrong is wrong silently: a missed dependency does not fail the build, it just stops rebuilding something. It stays opt-in until it has been through CI on every platform.

What it does not help. Where mcpp already skips the cascade — touch-hub, edit-comment — there is no owed work to move off the critical path, and the key buys nothing. See the benchmark.

The mechanism differs per compiler and is selected automatically: gcc publishes its BMI with rename(), so code generation is detached and the edge returns at publication; clang gets two ordinary edges instead, because it writes the BMI to its final path with O_TRUNC and a reader could observe a half-written file. MSVC is left alone — neither /ifcOnly's cost nor .ifc atomicity has been measured, and guessing either wrong is silent.

Module interface extensions (module_extensions)

mcpp treats .cppm as a module interface unit. The C++ ecosystem has not converged on one spelling — Clang also recognizes .ccm and .cxxm, MSVC uses .ixx — so a project whose interfaces use another extension declares it:

[build]
module_extensions = [".ixx", ".ccm"]

The list is additive: .cppm is always a module interface and cannot be removed. To stop a particular file from being built, !-exclude it in sources; that is what sources is for.

Declaring an extension does three things at once, which is the point of having one key rather than several:

  1. the convention default for sources grows to match, so the files are found (src/**/*.ixx joins the default glob);
  2. those units compile with the module rule — they emit a BMI and their objects are linked unconditionally;
  3. the freshness fast path watches them, so adding an import to one invalidates the build graph instead of silently reusing a stale one.

Any extension is accepted except ones that already name a non-module role (.cpp .cc .cxx .c .m .mm .h .hpp .hh .hxx .S .s .asm); claiming one of those is a manifest error rather than a warning, because it would route (say) C files to the C++ module rule and fail somewhere that names neither the file nor this key.

Extensions are matched literally, without case folding.S and .s are different languages in this domain, so case is never ignored.

mcpp always tells the compiler explicitly that a module interface unit is one (-x c++-module on Clang, -x c++ on GCC, /interface /TP on MSVC), so an extension the compiler driver has never heard of works anyway. This is why any extension is allowed: mcpp does not need the compiler to recognize it.

Publishing note. An older mcpp does not know this key: it warns, ignores it, and then compiles those files as ordinary translation units — a wrong build rather than a clean failure. A published package that uses module_extensions, declare an mcpp version floor in its index descriptor.

Build-program timeout (build_program_timeout)

A build.mcpp gets 600 seconds by default, after which mcpp kills it and fails the build naming the package. A project whose build program legitimately runs longer (a large code-generation step) raises its own bound:

[build]
build_program_timeout = 1800   # seconds; 0 = no limit

The value is read from the manifest of the package that owns the build.mcpp — a dependency's generator is bounded by the dependency's own declaration, because its author is the one who knows how long it takes. The precedence follows the same shape as macos_deployment_target:

MCPP_BUILD_PROGRAM_TIMEOUT=<seconds>   (this invocation; highest)
  > [build] build_program_timeout      (that package's manifest)
  > 600                                (built-in default)

Leaving the key out is not the same as setting 0: unset means "use the default bound", 0 means "no bound at all".

This value is deliberately not part of the build fingerprint — it changes no edge in the graph, and folding it in would mean that raising a timeout rebuilt the whole project, which is the opposite of what someone raising a timeout wants.

The compile phase is not bounded, only the build program. See 30-build-mcpp.md for why that asymmetry is deliberate.

The C++ runtime contract (cxx_runtime)

Moved to 20 — Toolchain Management.

File names outside the host code page

Globs are narrow strings, and so are compile commands and build.ninja. On Windows those strings are produced in the process's ANSI code page, so a file whose name has no spelling in that code page cannot be matched by a glob, named on a compile command, or written into a build file.

Such entries are skipped, and the skip is reported once per directory:

warning: 'C:/.../pkg/test/www' contains names this system's active code page cannot represent
  impact: those files take no part in the build
  hint: Windows only: this is the process ANSI code page, which `chcp` does not change. ...

The reported path is the nearest ancestor whose name the code page can spell, in generic (/) spelling. The offending name itself is never printed: rendering it would throw the same exception the message is reporting.

chcp sets the console code page and has no effect here. Names that are only test data or documentation are harmless — an upstream tarball carrying a Japanese-named fixture directory builds fine on an en-US host. Sources are not: they need renaming, or a host whose code page covers them.

Linux and macOS perform no such conversion, so nothing is skipped there. A package that builds on one and not the other, with an internal: unhandled exception from a code-page message, was mcpp#516.

2.3.1 [build] accel — the accelerator this build targets

[build]
accel = "cuda12.8+{sm_80,sm_90f} ptx>=90"

Which device backends and architectures this build compiles for. Overridden for one build by --accel, the relationship --target has with [toolchain]; --no-accel requests none explicitly, which is how a CPU-only variant of a package that also publishes device builds is selected.

The value is compared against the accel field of any prebuilt artifact the build consumes, and a build asking for none is satisfied by every artifact. See 42 — Heterogeneous Builds.

2.4 [lib] — Library Root Module Convention

[lib]
path = "src/capi/lua.cppm"    # Override the default lib-root location

Default convention: src/<last segment of package name>.cppm (e.g. package name mcpplibs.cmdlinesrc/cmdline.cppm).

2.5 [dependencies], [dev-dependencies], [build-dependencies]

Moved to 05 — Dependencies and Resolution.

2.7 [toolchain] — Toolchain Configuration

[toolchain]
default = "gcc@16.1.0"

# Cross-compilation target override
[target.x86_64-linux-musl]
toolchain = "gcc@16.1.0"
linkage   = "static"

2.7.1 [target.*] — Platform-Conditional Dependencies & Flags

Moved to 22 — The Target Side.

2.7.2 Bare metal (os = none) — freestanding targets

riscv64-none-elf and riscv32-none-elf are targets with no operating system underneath. They need no per-host cross toolchain: clang and lld are cross-compilers by construction, so any host that can install the llvm payload can produce them.

This section is the manifest reference. The worked examples — scaffolding, running, testing on the target, the freestanding standard-library subset and writing a board-support package — are in 40 — Bare-Metal and Freestanding Targets.

mcpp build --target riscv64-none-elf
mcpp run   --target riscv64-none-elf     # via [target.<triple>].runner

Starting from a board package

Almost nothing below has to be written by hand. A board-support package carries the C library, the startup code, the memory layout and the emulator, so the shortest path to a booting image is:

mcpp new blinky --template riscv-virt-rt
cd blinky && mcpp run

The generated manifest names no linker script, load address, libc or emulator — it has no [target.*] section at all. The rest of this section describes what such a package supplies, which is what to reach for when writing one for a board that has none.

What changes on a freestanding target

Link line -nostdlib -nostartfiles -static, and nothing hosted — no crt files, no dynamic linker, no C++ runtime. The linker is addressed by absolute path (-fuse-ld=<payload>/bin/ld.lld), because -fuse-ld=lld resolves through PATH and finds GNU ld on any machine with binutils earlier on it.
ISA flags -march / -mabi / -mcmodel come from the target table, so --target <triple> alone is enough to produce a correct object file.
C library The target's, resolved by mcpp from the target's own row exactly as the compiler is — a bare-metal project declares no libc, just as a hosted one declares no glibc. Its headers reach every translation unit and its directory is on the link search path, so a board package selects out of it by bare name (-lc, -lcrt0-semihost). Which objects and which linker script remain board decisions.
Exceptions and RTTI Off, on every translation unit including a dependency's. There is no unwinder and no libc++abi, so nothing can throw; std::optional::value() alone would otherwise pull in __cxa_throw and three more undefined symbols. It belongs to the target rather than to a project's cxxflags because a BMI records it — a dependency compiled with exceptions cannot be imported by a unit without them.
import std Unavailable. std is one module over the entire library — threads, filesystem and iostreams included — so there is no subset of it to build without an OS. Two ordinary dependencies replace it: the board package wraps the target's C library, and std-freestanding carries the parts of the standard library that need no OS (103 of libc++'s 110 headers, measured).
Entry point int main() works as long as something supplies a crt0 — a board package normally does, and then a firmware's entry point is an ordinary main whose return value reaches the host through semihosting. Only a zero-libc board needs an explicit target whose main points at the file carrying _start.

A minimal firmware

[package]
name    = "fw"
version = "0.1.0"

[build]
ldflags = ["-T", "/abs/path/to/link.ld"]

[targets.firmware]
kind = "bin"
main = "src/start.S"          # the entry lives in assembly, not in main()

[target.riscv64-none-elf]
runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic",
          "-no-reboot", "-bios", "default", "-kernel"]

runner — how mcpp run executes something this machine cannot run

A bare-metal image has the wrong ISA, no loader, and expects to own the address space; exec'ing it directly gives "Exec format error". runner is the argv template that stands in front of it. The artifact path is appended, or substituted for {} when the template contains it.

mcpp ships no default runner, deliberately. Which emulator, which machine model and which firmware mode are board facts — two boards on the same ISA need different argv (-bios default for an OpenSBI boot, -bios none -semihosting for a picolibc image) — and an engine that guesses one is an engine the other board has to fight. A board-support package normally supplies it.

2.7.3 runner on a hosted target (2026.9.2.1+)

[target.<triple>].runner applies to every exact triple, not only to bare metal. A hosted cross artifact — aarch64-linux-musl built on an x86_64 machine — is executable by some hosts (binfmt_misc with qemu-user registered) and refused by others with Exec format error, and which of the two applies is a property of the machine, not of the triple. mcpp does not predict it. It either executes the artifact through the runner the project declared, or it attempts direct execution and reports what the kernel answered.

[target.aarch64-linux-musl]
runner = ["qemu-aarch64-static"]

The rules, for mcpp run and mcpp test alike:

  • A declared runner is used. Its first element is located by mcpp: first in the bin/ directory of each payload declared under [xlings.workspace] (§2.13), then on PATH. A bare name on PATH resolves to an xvm shim, which answers for the current SubOS rather than for the package; the payload lookup is what lets a runner name a program the project declared.
  • A declared runner that cannot be found or started is an error, with the program, the directories searched and the errno. There is no fallback to direct execution: running the artifact under a different interpreter with different arguments is the failure the key exists to prevent.
  • No runner, and the kernel refuses the artifact: mcpp run reports the refusal and the key to write, and exits 2. mcpp test reports every test as not run, with the reason once, and exits 2 (§2.7.3.1).
  • --no-runner executes the artifact directly and ignores a declared runner. It states a fact about this host — the triple is native here — that the manifest has no axis to carry; a project whose runner was written for x86_64 developers is still readable on an aarch64 machine.

Provisioning the emulator through [xlings.workspace] is the form for a CI job or a project built on one host class. qemu-user-aarch64 in the index is built for x86_64 Linux only, and the table provisions on every host that builds the project, so the entry is written per platform (§2.13):

[xlings.workspace]
"xim:qemu-user-aarch64" = { linux = "" }   # present on Linux, any version

[target.aarch64-linux-musl]
runner = ["qemu-aarch64-static"]

A package the host cannot install is a hard build error, so an entry without the platform form would make the project unbuildable on macOS and Windows. The Linux/aarch64 host, where the package does not exist either, passes --no-runner.

2.7.3.1 mcpp test and tests that were not run

A test whose artifact this host cannot execute has neither passed nor failed. mcpp test reports it as not run, prints the reason once when it is established, repeats the first line of the reason in the summary, and exits 2:

warning: this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); declare [target.aarch64-linux-musl].runner, or pass --no-runner on a host that can
smoke ... not run
error: test result: NOT RUN. 0 passed; 0 failed; 1 not run (this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); ...); finished in 0.41s (build 0.39s + run 0.00s)

Exit code 1 keeps its meaning — a test ran and failed — and 0 means every test ran and passed. --message-format json carries "status":"not_run" and a reason on each record, and not_run / not_run_reason on the summary record (see 50 — Machine-Readable Output).

2.8 [features] — Features

Moved to 06 — Features and Capabilities, with provides / requires and [feature-deps.<name>].

2.8.3 [scan_overrides."<glob>"] — Author-Asserted Scan Results

The default module scanner is a text-level pass that (deliberately) rejects import statements inside conditional preprocessor blocks. Some legitimate module units carry them — e.g. fmt's official src/fmt.cc guards import std; behind #ifdef FMT_IMPORT_STD. When the file's import set is known and stable, declare it instead of scanning:

[modules]
sources = ["src/**/*.cppm", "vendor/fmt.cc"]

[scan_overrides."vendor/fmt.cc"]
provides = ["fmt"]      # at most one provided module per unit
imports  = ["std"]

Files matched by the glob skip the text scan; the declared unit enters the module graph directly. The declaration is audited every build: the compiler's own P1689 scan of the file (the .ddi dyndep input) is compared against it, and any divergence fails that compile edge with both sides printed — a stale declaration cannot silently corrupt the graph. An override glob that matches no source file is an error.

The same key exists in xpkg descriptors (index packages):

mcpp = {
    sources  = { "*/src/fmt.cc" },
    cxxflags = { "-DFMT_IMPORT_STD" },
    scan_overrides = {
        ["*/src/fmt.cc"] = { provides = { "fmt" }, imports = { "std" } },
    },
}

To extend the plan-vs-ddi audit to every module unit (not just overrides), set MCPP_VERIFY_MODGRAPH=1 when generating the build.

2.8.4 What The Default Scanner Reads

The scanner answers two questions per file — what does this unit provide, and what does it require — and nothing else decides them.

Three module declarations, and they are distinct productions.

module M;          // implementation unit:      requires M, provides nothing
module M:part;     // implementation partition: provides M:part
module : private;  // private module fragment:  declares neither

The third is not a partition whose name begins with a colon. It contributes no edge in either direction, and what follows it is still part of the same unit. Whether the compiler implements it is the compiler's answer: GCC 16.1 reports sorry, unimplemented: private module fragment, and mcpp adds nothing to that.

A module-extension file need not provide a module. An implementation unit is a legal inhabitant of a .cppm, and module_extensions says which files to scan, not what each one is. The compile mode follows the scan: a unit that provides a module is compiled as an interface and given somewhere to write its BMI; one that does not is compiled as an ordinary translation unit. Both compilers therefore receive the same instruction for the same file, which they did not before mcpp 2026.9.9.1 — Clang infers c++-module from the extension and rejected the file, while GCC built it.

Source is read as UTF-8. A UTF-8 byte-order mark is consumed and is not part of the text, which is what every compiler does with one and what MSVC writes by default. A UTF-16 or UTF-32 mark is refused by name rather than misread. The same rule applies to mcpp.toml.

A name that no source could have declared is refused. A module identity is a dot-separated sequence of identifiers, optionally followed by : and one more such sequence. Anything else fails the scan rather than entering the build graph, where it would become a BMI path that nothing reports.

2.9 [profile.<name>] — Build Profiles

[profile.dist]
opt      = 3              # -O level (a number, or the string "s"/"z")
debug    = false          # -g
lto      = true           # -flto (note: some packaged gcc builds ship without the LTO plugin)
strip    = true           # -s at link time
# passthrough escape hatch (fixed keys, open values):
cflags   = ["-fno-plt"]
cxxflags = ["-fno-plt"]
ldflags  = []
  • Selection & default: a bare mcpp build uses the dev profile (-O0 -g) — the mainstream convention (cf. Cargo/Meson/CMake/Zig/Bazel). Release is opt-in: mcpp build --release (shorthand) or --profile release. --dev is the explicit shorthand for dev. Same applies to mcpp test --profile <name> (builds the code-under-test plus the test binaries under that profile).
  • Per-project default[build].default-profile = "<name>" (alias: profile) sets the project's own default when no flag is passed. The typical use is a tool/library that should build optimized by default: [build] default-profile = "release". Precedence: --profile/--release/--dev flag > [build].default-profile > global dev. (A project that defaults to dev should pass --release when producing a distributable.)
  • Built-in profiles: release (-O2) / dev, debug (-O0 -g) / dist (-O3 + strip; LTO is not enabled by default). [profile.<built-in name>] can override a built-in definition wholesale.
  • Each profile owns its own build directory. The resolved profile knobs participate in the fingerprint, so target/<triple>/ holds one hash directory per profile and switching between them is incremental instead of a full rebuild. It also means the disk cost scales with the number of profiles actually use.

2.10 [build] cache — The Global Dependency Cache

Compiled artifacts for dependencies fetched from an index are cached across projects under $MCPP_HOME/build-cache/v1/. A dependency's artifacts do not depend on who consumes them, so two projects with the same toolchain, profile and dependency versions reuse one entry.

[build]
cache = "global"   # "global" (default) | "local" | "off"
Mode Reads the cache Writes the cache Clears the build dir first
global (default) yes yes no
local no no no
off no no yes

local builds every dependency inside this project's target/ — useful to rule the cache out while diagnosing something, and to give CI a no-sharing baseline. off additionally clears this build's target/<triple>/<fp>/ for a cold rebuild; --no-cache is a deprecated alias for it.

Precedence: --cache <mode> > MCPP_BUILD_CACHE > [build] cache > global. An unrecognized value is reported (an error under --strict) rather than silently falling back to global.

What is not cached: path and git dependencies, at any depth, and workspace members. Their sources can change without their name@version changing, so no key over that identity could notice the change.

Inspection and reclamation:

mcpp cache dir                      # where the cache lives
mcpp cache list [--json]            # entries, sizes, last use
mcpp cache info <pkg>@<ver>         # one entry, including the key inputs it was built with
mcpp cache verify                   # every entry's file list against the disk
mcpp cache gc --max-size 5GiB       # LRU-collect package entries to a budget
mcpp cache gc --older-than 30d      # ...or by how long since they were last used
mcpp cache clean [--deps|--std|--all|--legacy]

The on-disk entry layout is versioned. An mcpp release that changes it retires every older entry at once, so the first build after such an upgrade rebuilds its dependencies and repopulates — nothing to clean by hand. 2026.8.3.4 did exactly that: an entry's object paths are now addressed relative to the package, never to the build directory of whichever project happened to populate the entry first. mcpp cache verify additionally reports any entry whose recorded addresses escape it, so a recurrence is auditable offline.

2.11 [runtime] — Provider-neutral runtime contract

[runtime]
requirements = [
  { kind = "capability", value = "display.present", phase = "run", required = true,
    discovery = "rpath-of-dispatch" },
  { kind = "soname", value = "libwidget.so.1", phase = "link", required = false },
]
provides = ["display.present"]
artifacts = [
  { role = "library", path = "runtime/libwidget.so.1", provenance = "payload", abi = "elf-x86_64", digest = "sha256:...", host_fingerprint = "host-1" },
]

# Platform-neutral LinkIntent. Paths are relative to this package root.
libraries                = ["widget"]
link_library_dirs        = ["lib"]
transitive_needed_dirs   = ["runtime/closure"]
runtime_search_dirs      = ["runtime"]
frameworks               = ["WindowKit"]
deploy_files             = ["bin/widget.dll"]

# Use an exact canonical identity when multiple providers exist.
[runtime."display.present"]
provider = "acme.widget-runtime@2.0.0"

An unsupported key in this table is reported and ignored, and the message lists the keys it checked against. A [runtime.<capability>] sub-table is a provider override rather than a key, so it is not swept. The same rule applies to [target.<predicate>.runtime], whose vocabulary is libraries and link_library_dirs only (22 — The Target Side).

requirements records a non-empty kind/value, a link or run phase, and whether the requirement is mandatory (required defaults to true).

discovery is optional and says how the loader finds whatever satisfies the requirement — e.g. rpath-of-dispatch, json-dir, glvnd-dispatch. It is declared, never inferred: which mechanism a capability uses is the provider's property and changes without mcpp, so mcpp carries the value and reports an undeclared one as unknown rather than guessing. It earns a field because the mechanisms are not interchangeable — one may be a search path baked into a dispatch library, another a JSON file holding an absolute path, so "copy the directory across" satisfies one and not the other. mcpp pack writes it into the bundle's HOST-REQUIREMENTS and mcpp publish projects it into the descriptor, from one derivation. Optional requirements remain visible provenance but do not become hard ABI or doctor inputs. A libraries entry that is an explicit relative file path is resolved against the declaring package root; a bare logical name remains a platform-spelled library name. artifacts requires role, path, and provenance; abi, digest, and host_fingerprint are optional evidence. The resolver, not the descriptor, stamps every requirement with the exact requester PackageId and every artifact with the exact declaring provider PackageId, including namespace, version, and source/index provenance. A descriptor therefore cannot spoof another package, and alpha.backend never collapses into beta.backend.

Only provides creates a descriptor-owned provider fact. Merely requiring a capability never makes the requester its own provider. An explicit [runtime.<capability>] provider= override accepts a canonical namespace.name@version (or an unambiguous compatibility spelling); missing or same-short-name ambiguous providers are hard errors. Provider/artifact facts already selected by the xlings SubOS precede descriptor fallbacks. xlings/xim owns graphics-stack, driver, ICD, WSL, and host provenance selection; mcpp records and consumes the generic result and never probes GPU hardware.

Link intent keeps discovery stages separate:

Field ELF Mach-O PE/Windows
link_library_dirs -L -L -L or /LIBPATH:
transitive_needed_dirs -Wl,-rpath-link no flag no flag
runtime_search_dirs RUNPATH/rpath only, never -L rpath only no flag
frameworks no flag -framework no flag
deploy_files copy edge copy edge copy beside the output; never a linker flag

For one compatibility train, library_dirs maps only to runtime search, dlopen_libs maps to required run-phase soname requirements, and capabilities maps to required run-phase capability requirements. None of these legacy fields creates a provider.

target/<triple>/<fp>/resolution.json schema 2 stores the RuntimeBinding, canonical requirements/providers/artifacts, LinkIntent, platform search mechanism, and post-link verdict. mcpp why runtime is a pure interpreter of the latest stored file: it neither re-resolves the manifest nor launches a graphics/hardware probe. Use xlings doctor when the selected host provider itself needs re-diagnosis.

Each artifact also carries an identity verdict, computed from paths alone:

identity meaning
ok the declared path resolves (through symlinks) into the declared version
mismatch it resolves somewhere else — the binding is stale, a later install repointed it
missing declared, but nothing is at that path
unverified declared without a version to check against

This is the rule mcpp already applies to the private libc (glibc@2.44 resolves that one payload; stale or missing is an error, never "whichever installed version looks usable"), generalised. It needs no knowledge of what the artifact does. unverified is deliberately not ok: a resolved provider with no artifact behind it has not been checked, and mcpp why runtime says (not declared by the environment — nothing to verify) rather than (none).

Capability names use layered lowercase domain.sub.role (for example display.present) and prefix-style abi:<name> (for example abi:glibc, which participates in toolchain ABI enforcement).

2.12 [package] platforms — Platform Declaration

[package]
platforms = ["linux", "macos", "windows"]

Declares the platforms the package supports (a CI matrix hint, shown via mcpp why). The vocabulary is fixed by mcpp (which owns the target/triple system): linux | macos | windows; unknown values produce a warning, and an error under --strict.

mcpp pack on a library target checks the claim against the legs it actually produced, because that is the first moment there is evidence to check it against:

situation result
a leg was packed for a platform not listed here warning — the manifest disclaims a platform the package demonstrably serves
a listed platform has no leg, and this host could have built one warning — consumers there will resolve the package and find no artifact
a listed platform has no leg and this host cannot build for it silent

The third row is why the check is usable at all. The normal release flow is one mcpp pack per platform in CI, so a Linux runner never produces a macOS leg — warning about it would fire on every run of every cross-platform package, and a warning that always fires hides the one that matters. What "this host could have built" means is the same question --target answers (docs/08 §7.4).

Both are warnings, never errors: coverage is release discipline, and the person who can judge it is looking at the release, not at this build.

2.12b [package] accelerators — Accelerator Declaration

[package]
accelerators = ["cuda", "rocm"]

Declares the accelerator backends the package supports. Mirrors platforms: a statement of intent and a CI-matrix hint, shown by mcpp why, never a gate.

Distinct from an artifact's accel field on purpose. A declaration is written by hand and may be aspirational; accel is measured from the build that produced a binary and is what a consumer is refused against. See 42 — Heterogeneous Builds.

2.13 [xlings] — the project's environment

Moved to 23 — The Project Environment.

2.14 Host tools from a dependency

Moved to 30 — build.mcpp.

2.15 [resources] — Metadata and Assets Embedded in the Artifact (2026.8.7.1+)

An exe icon and the version metadata Windows shows in a file's Properties dialog are a path in mcpp.toml, nothing more:

[resources]
icon = "assets/app.ico"

That is the whole common case. FILEVERSION, ProductName, FileDescription, CompanyName and LegalCopyright all default from [package], and mcpp generates the resource script automatically.

Key Type Meaning
icon path Embedded as the application icon (resource ordinal 1)
files list of paths Your own .rc scripts, compiled and tracked as build inputs
extra-inputs list of paths Inputs the .rc scanner could not see (see below)
version-info bool false opts out of the generated version resource
[resources.version-info] table company, product, description, copyright, original-filename, internal-name

Only PE targets compile this. On Linux and macOS the section is inapplicable: no resource units, no diagnostics, byte-identical build. You do not need (and cannot use) a cfg(windows) predicate — write it once, unconditionally.

A declared file that does not exist fails the build — on every target. A resource is a build input like a source file; mcpp will not quietly ship a binary without it. Validation is deliberately not PE-gated: whether a path exists is a fact about the working tree, not about the target, so a typo in icon = "assets/app.ico" is caught by the Linux or macOS build (and by their CI jobs) instead of waiting for the Windows one. To omit the icon, delete the line.

Version fields. FILEVERSION takes the four numeric segments of [package].version, each of which must fit in 16 bits; the string fields keep the version verbatim, so a form the numeric fields cannot hold (1.0.0-rc1) still shows up in the Properties dialog.

Supplying a hand-written .rc

[resources]
files = ["res/app.rc"]

With files set, mcpp stops generating a version resource, and the resource ID space belongs to the project. Set version-info = true alongside it for both (and mind the collision: there can be only one RT_VERSION at ordinal 1).

To start from the generated script instead of a blank file, copy it out of the build directory (target/<triple>/<fp>/res/<target>.mcpp.rc) and list it in files. The result is byte-identical, so moving from generated to hand-written never changes what ships.

VS_VERSION_INFO needs <windows.h>. In a hand-written script, VS_VERSION_INFO VERSIONINFO without #include <windows.h> files the version resource under a string name instead of ordinal 1. Every tool still reports Type: VERSIONINFO, but GetFileVersionInfo looks up the ordinal, so PowerShell's FileVersionInfo shows every field as empty. Either include <windows.h> or write 1 VERSIONINFO. mcpp warns when it sees this shape; the script it generates uses the literal 1.

Tracked inputs

mcpp reads the .rc for quoted #includes and for the files named by resource statements (ICON, RCDATA, MANIFEST, …), and makes them build inputs, so editing the icon relinks. Angled includes (<windows.h>) are the toolchain's and are covered by the toolchain fingerprint instead.

A file name reached through a macro (1 ICON APP_ICON) is invisible to that scan. mcpp names what it could not resolve and requires an explicit declaration:

extra-inputs = ["assets/app.ico"]

Anything else: role = "object"

For inputs that are not resource scripts — a blob embedded with objcopy, a generated .def, a pre-built object — a build program can declare a build-graph node whose outputs join the link:

mcpp::action o;
o.id = "blob"; o.role = "object";
o.arg("./mkblob.sh").arg("blob.bin").arg("${mcpp.out_dir}/blob.o")
 .input("blob.bin")
 .output("${mcpp.out_dir}/blob.o")
 .target("myapp")        // omit: every image, test binaries included
 .submit();

See 30 — build.mcpp. Naming such a file in [build].ldflags also "works", but ldflags is a flat string in the link command: nothing tracks it, and editing the file produces ninja: no work to do.

2.16 [hooks] — Project Build Lifecycle Commands

Moved to 09 — Commands by Scenario.

3. Worked Examples

Four of these are runnable projects rather than snippets, and the project is the better answer: it builds, and it is checked by CI.

shape run
a hello world examples/01-hello
a module library with tests examples/11-features
an application with dependencies examples/02-with-deps
a cross-compiled static release examples/03-pack-static

Two shapes have no example yet and stay here as manifests.

3.4 Pure C Library

[package]
name    = "myc"
version = "0.1.0"

[build]
c_standard   = "c99"
include_dirs = ["include"]
sources      = ["src/**/*.c"]

[targets.myc]
kind = "lib"

3.5 Mixed C / C++23 Module Project

[package]
name    = "hybrid"
version = "0.1.0"

[build]
include_dirs = ["include"]
c_standard   = "c11"

[dependencies]
lua = "5.4.7"     # Pure C library; mcpp compiles .c files with the C compiler automatically

[targets.hybrid]
kind = "bin"

4. Conventions and Defaults Cheat Sheet

Item Default Notes
Source files src/**/*.{cppm,cpp,cc,c,S,s,asm} Scanned recursively and automatically
Entry point src/main.cpp If this file exists, a bin target is inferred
Library root src/<pkg-tail>.cppm Override with [lib].path
C++ standard c++23 Configure with [package].standard; supports c++20 / c++26 / c++2a / c++2c / gnu++NN / c++latest / c++fly (experimental playground)
C standard c11 .c files go through the C compiler automatically
Static stdlib true Portable binary
Headers include/ (if present) Added to -I automatically
Tests tests/**/*.cpp Discovered automatically by mcpp test
Dependency namespace mcpplibs (default) A bare selector means only this exact namespace

4.1 Legacy [language] Compatibility Layer

The old configuration is still readable:

[language]
standard = "c++26"

New projects should use [package].standard. If both locations are present, [package].standard is authoritative.