Skip to content

Build cost and foreign toolsets, measured on a Windows workspace: engine items E1-E6 and plugin items P1-P5 #734

Description

@speak-agent

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

  1. 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.
  2. P1 to P4 follow in plugins 0.17.0, whose mcpp floor is that release.
  3. The validation project validates by using toolset = resolved on the Visual Studio-masked row and pack -p.
  4. The mcpp-index sweep then provides the input to E6 phase 2.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions