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

  1. changed the title [-]Build cost measured on a five-member Windows workspace: a path dependency compiled once per member, no toolset description for build programs, pack without -p, no tree stage, no PE fast path[/-] [+]Build cost and foreign toolsets, measured on a Windows workspace: engine items E1-E6 and plugin items P1-P5[/+] on Sep 28, 2026
  2. speak-agent commented on Sep 29, 2026

    @speak-agent
    MemberAuthor

    Closing: every engine item is released, and the plugin items are released in mcpp.plugins 0.17.0. The validation project builds, runs and packs with the released engine.

    Engine

    Item Released in Outcome
    E1. A path dependency compiled once 2026.9.29.1 (#738); corrections in 2026.9.29.2 (#739), 2026.9.29.3 (#740), 2026.9.29.4 (#741) The approach of 2026.9.28.3 (a shared member planned as the root of a nested build) is removed: it cost 2^n plans on a chain of members. A workspace is now one build graph per configuration (.agents/docs/2026-09-29-workspace-build-graph-design.md): the selected members are planned together under a virtual root, a member used by several members is compiled once, and each member's products are in bin/<member>/. The three patch releases correct what such a plan reads from its members (design section 17.1).
    E2, E2b. The resolved toolset and the C++ runtime contract, stated to build programs 2026.9.28.3 (#734's pull request) mcpp::tool(role), abi_tool(role), tool_env(), toolset_identity(), msvc_instance_dir(), ninja_program(), cxx_runtime(), msvc_crt_linkage() (protocol 14).
    E3. mcpp pack -p 2026.9.28.3 A member is packed from the workspace root.
    E4. Placing a directory tree as one edge 2026.9.28.3 One process places the files beside a program.
    E5. The project fast path on PE and Mach-O 2026.9.28.3 The fast path runs on macOS, Windows and SDK-sysroot targets.
    E6. The library's exported surface 2026.9.28.3 Phase 1 of SPEC-008: mcpp pack names the library interface; W1 to W3 are warnings.

    Plugins (mcpp.plugins 0.17.0, mcpp-community/mcpp-plugins#37)

    • P1: mcpp.plugins.toolset passes the resolved toolset to deps-vcpkg and deps-cmake (a Visual Studio instance, a managed MSVC toolset, or the clang rows through a derived triplet).
    • P2: a self-contained program's ports use the static CRT triplet; naming the dynamic triplet under it is refused.
    • P3: the derived triplet and its chain-loaded toolchain file hold no path, so the vcpkg ABI hash does not depend on the user's home.
    • P4: no change, as stated in the issue.
    • P5: remains a measurement, as stated in the issue; no design was made.

    Readings (the validation project on windows-2025, fast-release, vcpkg binaries cached)

    2026.9.28.2 (this issue) 2026.9.29.4
    mcpp build --workspace, the CI step 1726 s (run 36378870254) 1124 s (the released 2026.9.29.4, Sunrisepeak/GalTranslPP run 36549033403); of which the build after planning 1020.7 s
    times the core library is compiled 3 (core, cli and gui each compile it) 1
    build directories in the members' own directories each member builds in its own 0

    With the released 2026.9.29.4 the same workflow also passes mcpp run -p GPPCLI, mcpp pack -p GPPCLI/GPPGUI --format release, the release layout check and the CLI started from Release; each member's resource script (GPPCLI.rc, GPPGUI.rc, Updater.rc) is compiled for that member, and the updater GPPGUI ships through artifacts is in bin/gui/ beside the GUI program. The run on the pull request's branch before the release (36537600583) gave the same results, with the step at 1131 s.

    Ecosystem

    Open elsewhere: #732 (two members that provide a module of one name cannot be built in one --workspace plan).

    The post-release acceptance also showed two defects outside this issue's items, recorded for the next patch release: a build program's cache key varies with the selection, because the graph document lists every requester in the plan, the virtual root included, so -p, mcpp pack and mcpp emit build-database rerun the build programs a --workspace build ran; and mcpp emit build-database plans each member separately, so the core library appears in three sets with three different argument lists.

    From 2026.9.29.4 on, a change to the engine is cross-verified with the validation project before it merges: the project's CI builds mcpp from the pull request's branch, and the pull request merges only when that run and its own CI both pass.

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