Context
A downstream validation project is used: a five-member Windows workspace with a core library of 76 translation units, a Qt GUI, 22 vcpkg ports and one CMake project, built with mcpp 2026.9.28.2 and mcpp:plugins 0.16.0. The project is evidence, not a requirement. An item is listed here only if its need survives the removal of that project; project-specific items are listed at the end with the reason they stay with the project.
The design, with alternatives, compatibility and one criterion per item, is recorded in .agents/docs/2026-09-28-build-cost-foreign-toolsets-and-library-surface-design.md (under review).
Principles applied:
- The engine provides general mechanisms only and never learns CMake, vcpkg or Qt.
- A plugin behaviour is an option set from
build.mcpp, with a stated default.
- A check is made by the reader of the property it protects.
- No silent wrong output.
- Every change states its upgrade cost.
Readings
| Reading |
Value |
Source |
mcpp build --workspace, vcpkg binaries cached |
1726 s |
run 36378870254 (windows-2025, 4 vCPU, fast-release) |
| of which the core member |
285 s |
same |
of which cli (compiles core again) |
315 s |
same |
of which gui (compiles core a third time; plus GUI, ElaWidgetTools, Qt code generation) |
1039 s |
same |
| core's 76 compile commands in the three positions |
identical except the output directory (76/76) |
mcpp emit build-database, same run |
| the same build with no vcpkg cache |
69.7 min, of which about 43 min are the 22 ports |
run 36324593343 |
mcpp pack --format release, two members |
168 s, of which about 70 s are 2675 single-file copy actions |
run 36378870254 |
no-op mcpp run -p cli on Windows |
about 3 s; the fast path is not taken |
same |
build systems of the 22 ports (baseline ee6a47d) |
18 CMake, 3 header-only, 1 make under msys (icu), none MSBuild |
the ports' portfile.cmake |
mcpp pack of a library exporting Alpha and Beta with no lib root |
exit 0, "Interface (headers only)", "Withheld (nothing)", sources = [] |
local, mcpp 2026.9.28.2 |
Engine (mcpp)
E1. A path dependency is compiled once per consuming member
--workspace and separate -p invocations build each member as its own graph, so a shared library member is compiled once per consumer: 3 times here, about 570 s of 1726 s. The global cache excludes path packages (their sources are mutable), and a source stamp of the package root cannot key them either, because a library's inputs are not confined to its root: core compiles ../3rdParty/... modules.
Proposal. Build a path dependency as a keyed sub-build, the way host tools already are:
- It lives in
<workspace>/target/.members/<package>/<key>/, keyed by the existing per-package build key, which excludes the consumer.
- Every consumer runs the sub-build's ninja first, so ninja's time stamps and depfiles decide staleness, including for inputs outside the root.
- Every consumer takes the outputs through the existing cache stage edges.
- Concurrent consumers take a lock on the sub-build directory.
The alternative is one workspace graph (Cargo's model), recorded in the design.
Criterion.
--workspace compiles each library unit once, and a following -p <second program> compiles none of them.
- A header outside the root changed: the next build recompiles only its includers.
- Two concurrent
-p builds of different programs both succeed.
E2. The resolved toolset, stated to build programs
Build programs can read toolchain_dir() and compiler(), but not the tools of the row or their environment. Plugins therefore let vcpkg and CMake detect Visual Studio on Windows, and on Linux they reconstruct mcpp's clang privately (program_compilers).
The engine already holds the facts:
- the cl.exe row has
envOverrides (INCLUDE/LIB/PATH);
- the llvm row's MSVC sysroot has
msvcToolsDir, windowsSdkRoot and their versions.
Proposal. Build-system-neutral accessors, carried as MCPP_* variables:
| Accessor |
Answers |
mcpp::tool(role) |
the row's tool for cc, cxx, ld, ar, rc, as |
mcpp::abi_tool(role) |
the ABI's native tool for the same roles: cl, link, lib, rc of the resolved toolset and SDK on the MSVC ABI, and the same as tool(role) elsewhere |
mcpp::tool_env() |
the ABI tools' environment: INCLUDE/LIB/PATH on the MSVC ABI (synthesised for the llvm row by the cl.exe row's function), empty elsewhere |
mcpp::toolset_identity() |
a path-free identity, e.g. msvc 14.44.35207; sdk 10.0.26100.0 |
mcpp translates nothing into any foreign build system's terms. Upgrade cost: every build program runs once more.
Criterion.
- With Visual Studio masked and
msvc@14.44.35207 resolved, abi_tool("cxx") and tool_env() name the managed toolset.
- On the llvm MSVC-ABI row,
tool("cxx") is clang++ and abi_tool("cxx") is the sysroot's cl.exe.
- On Linux both accessors name the payload's tools.
E2b. The C++ runtime contract, stated to build programs
Objects a plugin produces must follow the program's CRT contract. Today:
- no accessor states the contract;
- deps-vcpkg's generated triplet writes
VCPKG_CRT_LINKAGE dynamic, and the standard triplets are dynamic;
- deps-cmake keeps CMake's
/MD.
A project with cxx_runtime = "self-contained" is therefore expected to mismatch. This is read from the sources and not yet measured.
Proposal. mcpp::cxx_runtime() and mcpp::msvc_crt_linkage() (static / dynamic / empty), the values place-dlls --crt already receives.
Criterion. First a reading with plugins 0.16.0: a self-contained project plus one vcpkg port on Windows. The item is withdrawn if it links.
E3. mcpp pack -p
build, run and test take -p; pack must be run from the member directory.
Criterion. mcpp pack -p <member> --format <f> at the root equals the same command in the member directory.
E4. Placing a directory tree as one edge
mcpp stage copies one file per action. This costs 2675 processes here, and the validation project's upstream wrote its own --copy-tree tool.
Proposal. mcpp stage --tree <src> --output <dir> --manifest <f> --depfile <f>:
- one action per tree;
- removes the files it placed that the tree no longer holds;
- reports every source in the depfile.
The alternative, a layout stated in the pack format, is recorded in the design. The recommendation is the primitive, because it also serves builds.
Criterion.
- 1000 files are placed by one action.
- A no-change rebuild runs nothing.
- A removed source loses its copy.
E5. The project fast path on PE and Mach-O
try_fast_build requires a stored ELF run-time Pass verdict for every artifact (validated_artifact_snapshot), so on Windows and macOS every build and run plans again (#400; e2e 645 and 821).
Proposal. Record NotApplicable for formats without a validator and accept it on the fast path. This is sound on PE because the check that matters there is the place-dlls edge, which ninja runs on every relink.
Criterion.
- e2e 645 reads MEASURED on Windows and macOS.
- An A-B-A test in the form of e2e 611 passes on both.
E6. The library's exported surface: a specification, and warnings at its readers
The lib root (src/<tail>.<ext> or [lib].path) has two readers:
- host-module resolution;
mcpp pack <lib>, which publishes the lib root's module closure.
A source consumer may import any exported module, so the build-time warning lib target without conventional lib root has no reader in the build. Meanwhile mcpp pack of a library exporting modules without a lib root publishes it as headers-only and exits 0, and its "Withheld (nothing)" row is false.
An error is not proposed now:
- a library may legitimately use modules internally and publish only headers;
- a key declaring that would be a new key in
[lib], which older engines refuse.
Phase 1.
- A specification section covers the surface as the lib root's closure, the default location and
[lib].path, what is published and withheld, the legitimacy of a headers-only interface, and the recommended facade (one primary interface that re-exports with export import).
mcpp pack warns and names each exported module the package will not contain.
- The "Withheld" row lists every unpublished unit.
- The
mcpp build warning stays for the package being built, reworded to state the consequence at pack time.
Phase 2 (conditions only). An error with an explicit headers-only declaration becomes possible when two conditions hold:
- an mcpp-index sweep has counted the affected libraries;
- the index
min_mcpp reads the new key.
Criterion (phase 1).
- The
Alpha/Beta library packs with a warning naming both modules, and "Withheld" lists both.
- With a facade
[lib].path, both modules are published and no warning appears.
Plugins (mcpp-plugins, to be filed there as P1 to P5 once E2 is settled)
-
P1. A toolset option for deps-vcpkg and deps-cmake.
- Options, set from
build.mcpp:
toolset = resolved | detected;
compiler = abi_native | row;
- for deps-cmake,
generator = ninja | default.
- Under
resolved:
- deps-vcpkg generates a triplet with
VCPKG_CHAINLOAD_TOOLCHAIN_FILE (vcpkg then does not load vcvars);
- deps-cmake uses Ninja with
CMAKE_<LANG>_COMPILER;
- the Linux
program_compilers becomes the Linux instance of resolved.
- Proposed default:
resolved with abi_native.
- It changes in a minor version, and the changelog states that every port rebuilds once.
detected stays selectable.
- A row that cannot provide E2 falls back once, with a note; an explicit
resolved there is an error.
-
P2. CRT linkage from the contract. VCPKG_CRT_LINKAGE and CMAKE_MSVC_RUNTIME_LIBRARY come from E2b and can be overridden. Proceed only if E2b's reading confirms the mismatch.
-
P3. vcpkg ABI-hash hygiene under resolved. The hash covers the triplet, the compilers, the chain-loaded file and the values of VCPKG_ENV_PASSTHROUGH, and paths contain the user's home. Therefore:
- the environment goes into
VCPKG_ENV_PASSTHROUGH_UNTRACKED;
toolset_identity() goes into a triplet comment;
- the toolchain file refers to paths only through
$ENV{}.
Criterion: two homes with the same pinned toolset compute the same hash.
-
P4. Binary sources. No change. VCPKG_BINARY_SOURCES already passes through, and the plugin documentation states it.
-
P5. Reuse of a CMake dependency's build across runs. First measure ElaWidgetTools' share of gui's 690 s. No design until that reading exists.
Outside mcpp and the plugins
| Item |
Why it stays with the project |
| Hosting a vcpkg binary cache |
a project decides whether first builds justify a feed; vcpkg already reads the sources |
Two -p instead of --workspace, Updater as an artifacts dependency, hoisted [target.windows.build] values |
available in the project's manifests today |
A CI job for the release profile |
the project's CI |
Re-running build programs under mcpp pack |
by design: the pack context is an input of the build program; the recompilation it prints costs about 1 s |
Flat module names (Tool, Dictionary) in core |
the project's naming; E6's facade form is the remedy if the library is published |
Order
- E2, E2b (after its reading), E3, E5 and E6 phase 1 go into the next mcpp release. E1 and E4 go into the same or a following release.
- P1 to P4 follow in plugins 0.17.0, whose mcpp floor is that release.
- The validation project validates by using
toolset = resolved on the Visual Studio-masked row and pack -p.
- The mcpp-index sweep then provides the input to E6 phase 2.
Context
A downstream validation project is used: a five-member Windows workspace with a core library of 76 translation units, a Qt GUI, 22 vcpkg ports and one CMake project, built with mcpp 2026.9.28.2 and mcpp:plugins 0.16.0. The project is evidence, not a requirement. An item is listed here only if its need survives the removal of that project; project-specific items are listed at the end with the reason they stay with the project.
The design, with alternatives, compatibility and one criterion per item, is recorded in
.agents/docs/2026-09-28-build-cost-foreign-toolsets-and-library-surface-design.md(under review).Principles applied:
build.mcpp, with a stated default.Readings
mcpp build --workspace, vcpkg binaries cachedcli(compiles core again)gui(compiles core a third time; plus GUI, ElaWidgetTools, Qt code generation)mcpp emit build-database, same runmcpp pack --format release, two membersmcpp run -p clion Windowsee6a47d)portfile.cmakemcpp packof a library exportingAlphaandBetawith no lib rootsources = []Engine (mcpp)
E1. A path dependency is compiled once per consuming member
--workspaceand separate-pinvocations build each member as its own graph, so a shared library member is compiled once per consumer: 3 times here, about 570 s of 1726 s. The global cache excludes path packages (their sources are mutable), and a source stamp of the package root cannot key them either, because a library's inputs are not confined to its root: core compiles../3rdParty/...modules.Proposal. Build a path dependency as a keyed sub-build, the way host tools already are:
<workspace>/target/.members/<package>/<key>/, keyed by the existing per-package build key, which excludes the consumer.The alternative is one workspace graph (Cargo's model), recorded in the design.
Criterion.
--workspacecompiles each library unit once, and a following-p <second program>compiles none of them.-pbuilds of different programs both succeed.E2. The resolved toolset, stated to build programs
Build programs can read
toolchain_dir()andcompiler(), but not the tools of the row or their environment. Plugins therefore let vcpkg and CMake detect Visual Studio on Windows, and on Linux they reconstruct mcpp's clang privately (program_compilers).The engine already holds the facts:
envOverrides(INCLUDE/LIB/PATH);msvcToolsDir,windowsSdkRootand their versions.Proposal. Build-system-neutral accessors, carried as
MCPP_*variables:mcpp::tool(role)cc,cxx,ld,ar,rc,asmcpp::abi_tool(role)cl,link,lib,rcof the resolved toolset and SDK on the MSVC ABI, and the same astool(role)elsewheremcpp::tool_env()INCLUDE/LIB/PATHon the MSVC ABI (synthesised for the llvm row by the cl.exe row's function), empty elsewheremcpp::toolset_identity()msvc 14.44.35207; sdk 10.0.26100.0mcpp translates nothing into any foreign build system's terms. Upgrade cost: every build program runs once more.
Criterion.
msvc@14.44.35207resolved,abi_tool("cxx")andtool_env()name the managed toolset.tool("cxx")is clang++ andabi_tool("cxx")is the sysroot'scl.exe.E2b. The C++ runtime contract, stated to build programs
Objects a plugin produces must follow the program's CRT contract. Today:
VCPKG_CRT_LINKAGE dynamic, and the standard triplets are dynamic;/MD.A project with
cxx_runtime = "self-contained"is therefore expected to mismatch. This is read from the sources and not yet measured.Proposal.
mcpp::cxx_runtime()andmcpp::msvc_crt_linkage()(static/dynamic/ empty), the valuesplace-dlls --crtalready receives.Criterion. First a reading with plugins 0.16.0: a
self-containedproject plus one vcpkg port on Windows. The item is withdrawn if it links.E3.
mcpp pack -pbuild,runandtesttake-p;packmust be run from the member directory.Criterion.
mcpp pack -p <member> --format <f>at the root equals the same command in the member directory.E4. Placing a directory tree as one edge
mcpp stagecopies one file per action. This costs 2675 processes here, and the validation project's upstream wrote its own--copy-treetool.Proposal.
mcpp stage --tree <src> --output <dir> --manifest <f> --depfile <f>:The alternative, a layout stated in the pack format, is recorded in the design. The recommendation is the primitive, because it also serves builds.
Criterion.
E5. The project fast path on PE and Mach-O
try_fast_buildrequires a stored ELF run-timePassverdict for every artifact (validated_artifact_snapshot), so on Windows and macOS every build and run plans again (#400; e2e 645 and 821).Proposal. Record
NotApplicablefor formats without a validator and accept it on the fast path. This is sound on PE because the check that matters there is theplace-dllsedge, which ninja runs on every relink.Criterion.
E6. The library's exported surface: a specification, and warnings at its readers
The lib root (
src/<tail>.<ext>or[lib].path) has two readers:mcpp pack <lib>, which publishes the lib root's module closure.A source consumer may import any exported module, so the build-time warning
lib target without conventional lib roothas no reader in the build. Meanwhilemcpp packof a library exporting modules without a lib root publishes it as headers-only and exits 0, and its "Withheld (nothing)" row is false.An error is not proposed now:
[lib], which older engines refuse.Phase 1.
[lib].path, what is published and withheld, the legitimacy of a headers-only interface, and the recommended facade (one primary interface that re-exports withexport import).mcpp packwarns and names each exported module the package will not contain.mcpp buildwarning stays for the package being built, reworded to state the consequence at pack time.Phase 2 (conditions only). An error with an explicit headers-only declaration becomes possible when two conditions hold:
min_mcppreads the new key.Criterion (phase 1).
Alpha/Betalibrary packs with a warning naming both modules, and "Withheld" lists both.[lib].path, both modules are published and no warning appears.Plugins (mcpp-plugins, to be filed there as P1 to P5 once E2 is settled)
P1. A toolset option for deps-vcpkg and deps-cmake.
build.mcpp:toolset = resolved | detected;compiler = abi_native | row;generator = ninja | default.resolved:VCPKG_CHAINLOAD_TOOLCHAIN_FILE(vcpkg then does not loadvcvars);CMAKE_<LANG>_COMPILER;program_compilersbecomes the Linux instance ofresolved.resolvedwithabi_native.detectedstays selectable.resolvedthere is an error.P2. CRT linkage from the contract.
VCPKG_CRT_LINKAGEandCMAKE_MSVC_RUNTIME_LIBRARYcome from E2b and can be overridden. Proceed only if E2b's reading confirms the mismatch.P3. vcpkg ABI-hash hygiene under
resolved. The hash covers the triplet, the compilers, the chain-loaded file and the values ofVCPKG_ENV_PASSTHROUGH, and paths contain the user's home. Therefore:VCPKG_ENV_PASSTHROUGH_UNTRACKED;toolset_identity()goes into a triplet comment;$ENV{}.Criterion: two homes with the same pinned toolset compute the same hash.
P4. Binary sources. No change.
VCPKG_BINARY_SOURCESalready passes through, and the plugin documentation states it.P5. Reuse of a CMake dependency's build across runs. First measure ElaWidgetTools' share of gui's 690 s. No design until that reading exists.
Outside mcpp and the plugins
-pinstead of--workspace, Updater as anartifactsdependency, hoisted[target.windows.build]valuesreleaseprofilemcpp packTool,Dictionary) in coreOrder
toolset = resolvedon the Visual Studio-masked row andpack -p.