Skip to content

2026.10.1.3: a source for every tool a build uses, declared, programmable and observable (#755) - #758

Merged
speak-agent merged 20 commits into
mainfrom
feat/build-sources
Oct 1, 2026
Merged

speak-agent merged 20 commits into
mainfrom
feat/build-sources

Conversation

@speak-agent

@speak-agent speak-agent commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Summary

A build uses a toolchain, the payloads its plugins declare, and the tools those plugins run. Each of them now has one source: a project can state it, a build program can decide it, and anyone can read it back. A project that writes none of the new keys builds exactly as before, and its output is unchanged byte for byte.

The problem this closes is a timing one. A payload a plugin declares is provisioned before any build.mcpp runs, so a build program that names its own tool downloaded the payload anyway, and offline the build was refused before the program could run at all.

Payloads

  • [xlings.overrides] in the root manifest (also under [target.'cfg(..)']), MCPP_XLINGS_OVERRIDE_<NS>_<NAME>, and ~/.mcpp/config.toml state where a declared payload comes from. An overridden payload is not provisioned and does not reach the offline gate; mcpp::xpkg_dir answers the root it implies, and the new xpkg_program / xpkg_source answer the program it named and override. A stated version is checked against every requirement the graph made. A dependency that writes the table is refused: which payloads a package needs is its own statement, where they come from is the project's.
  • provision = "on-request" installs a payload when a build program asks for it with mcpp::xpkg_request. Every request of one invocation is installed together, only the programs that asked run again, and the run that asked is discarded. mcpp emit build-database installs nothing and records MCPP_BUILD_DATABASE_PAYLOAD_DEFERRED.

Toolchains

  • [toolchain] <key> = { path = "<dir>", prefix, sysroot, family, launcher, tools } and MCPP_TOOLCHAIN=path:<dir> name a toolchain this machine already has. mcpp probes the drivers, identifies them, drives them with its own link model and hermetic check, and writes nothing into the tree. The driver and each stated tool enter the fingerprint by content, the fast paths decline when one changed, and mcpp.lock records it as local. This is not = "system": it is a tree the project names, in the shape msvc@system already had.
  • bootstrap names the toolchain that compiles and runs build programs when it should not be the one building the project.
  • { configure = "build.mcpp" } hands the build toolchain to the root build program: it runs once in a toolchain phase (mcpp::phase() is "toolchain") and states the toolchain, and may state nothing else there.

Observability

A source that is not the ecosystem's gets a line of its own, the Finished line summarises them, and the record is written to resolution.json:

   Bootstrap llvm@22.1.8 → @mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++
       Using toolchain clang 23.0.0git ← /opt/acme-llvm   [program · build.mcpp:9]
       Using xim:cmake ← /usr/bin/cmake                   [custom · mcpp.toml:22]
    Finished dev [unoptimized + debuginfo] in 2.51s · custom: xim:cmake; program: toolchain

mcpp why sources, mcpp why tool <name> and mcpp why payload <ns:name> report it, including as mcpp.why.sources under --format json. --managed-only / MCPP_MANAGED_ONLY=1 refuses a build whose sources are not the ecosystem's, naming each.

Protocol 15 adds xpkg_source, xpkg_program, xpkg_request, xpkg_pending, phase, decision and toolchain.

Specifications and documentation

SPEC-004 §4.7, SPEC-006 §2.2.1 and §3.3, SPEC-007 R6.2/R6.5/R6.6/R9.9 and the protocol table, SPEC build-database 1.5; docs/09, 20, 23, 30, 31, 32, 50 and their 简体中文 mirrors. The design record is .agents/docs/2026-10-01-tool-and-toolchain-sources-design.md.

Also fixed, each found by a platform or a measurement rather than by review

  • A build program's compile command goes through a response file when it outgrows its channel. The command carries one -fmodule-file=<name>=<path> per host module the program imports; mcpp-plugins' all-rules-compile imports fifteen and crossed the 8191 bytes the Windows shell tolerates the moment the collection gained one more module, reporting only The command line is too long. The file is written in the grammar its driver reads — single quotes for clang and GCC, which treat a backslash as an escape, Windows quoting for cl and clang-cl — with a unit case per grammar. This repository's own CI cannot reach the path (the POSIX budget is 128 KiB); the plugins fixture does.
  • A manifest key this engine does not know says which engine the package needs. mcpp-plugins 0.19.0 on the released 2026.10.1.2 was refused with unknown key 'provision' in a scoped entry and nothing about the version, because the engine floor is checked on the document that very parse failed to produce — and raising the floor is what every plugin release does. The floor is now read from the file's text in that path, and seven cases pin the shapes read.
  • which() resolves a name that is also a shell builtin. command -v true prints true, not a path, so a bare-name payload override of such a name was refused as not found on a machine carrying /usr/bin/true.

Closes #755

Test plan

  • mcpp test: 144 passed, 0 failed, including the new tests/unit/test_sources.cpp (15 cases).
  • New e2e: 873 (overrides), 874 (on-request), 875 (a toolchain named by path), 876 (the toolchain phase), 877 (mcpp why and its machine output). Each uses MCPP_NO_AUTO_INSTALL=1 as the criterion — a build that succeeds asked for no download — and each has a control that must refuse.
  • 62 existing e2e cases selected by keyword (xpkg_dir, feature-xlings, mcpp why, protocol=, MCPP_TOOLCHAIN, fast-path, emit build-database) pass. 219_runtime_search_farm_is_last fails identically on the released 2026.10.1.2 on this machine, and 658 needs an attached Android device.
  • Guards: check_version_pins.sh, check_docs_style.sh, check_docs_structure.sh, check_reason_tokens.sh, check_file_lengths.sh, check_modules_wiring.sh, test_protocol_table.py and the other tests/scripts/*.py.
  • Ecosystem: mcpp-plugins 0.19.0 against this engine — 11 consumer fixtures and 27 plugin-logic cases pass, and tests/cmake-consumer builds under MCPP_NO_AUTO_INSTALL=1 when its build program names its own cmake, which is the behaviour this change exists for.

…d observable

A build uses a toolchain, the payloads its plugins declare, and the tools those
plugins run. Each of them now has one source that a project can state, a build
program can decide, and anyone can read back; a project that writes none of the
new keys builds exactly as before, with the same output.

- `[xlings.overrides]`, `MCPP_XLINGS_OVERRIDE_<NS>_<NAME>` and config.toml state
  where a declared payload comes from. An overridden payload is not provisioned
  and does not reach the offline gate; `xpkg_program` and `xpkg_source` answer
  the program it named and `override`. A stated version is checked against every
  requirement the graph made, and a dependency that writes the table is refused.
- `provision = "on-request"` installs a payload when a build program asks for it
  with `xpkg_request`: one batch per invocation, only the programs that asked run
  again, and planning records MCPP_BUILD_DATABASE_PAYLOAD_DEFERRED instead.
- `[toolchain] <key> = { path = ... }` and `MCPP_TOOLCHAIN=path:<dir>` name a
  toolchain this machine already has; mcpp probes it, drives it with its own link
  model, writes nothing into the tree, keys the fingerprint and the fast paths on
  its programs' content, and records it in mcpp.lock as local. `bootstrap` names
  the toolchain that builds build programs, and `{ configure = "build.mcpp" }`
  hands the build toolchain to the root program's toolchain phase.
- A build reports a source that is not the ecosystem's on its own line, sums them
  on the `Finished` line, and writes the record to resolution.json; `mcpp why
  sources|tool|payload` and the `mcpp.why.sources` kind read it, and
  `--managed-only` refuses a build that uses any.
- Protocol 15: xpkg_source, xpkg_program, xpkg_request, xpkg_pending, phase,
  decision, toolchain.

Specifications and both documentation trees are updated: SPEC-004 §4.7,
SPEC-006 §2.2.1 and §3.3, SPEC-007 R6.2/R6.5/R6.6/R9.9 and the protocol table,
docs/09, 20, 23, 30, 31, 32, 50 and their 简体中文 mirrors.

Closes #755

Test plan
- `mcpp test`: 144 passed, including the new tests/unit/test_sources.cpp (15).
- New e2e 873 (overrides), 874 (on-request), 875 (a toolchain by path),
  876 (the toolchain phase), 877 (`mcpp why` and its machine output), each with
  MCPP_NO_AUTO_INSTALL=1 as the criterion and a control that must refuse.
- 62 existing e2e cases selected by keyword pass; 219 fails identically on the
  released 2026.10.1.2 on this machine, and 658 needs an attached Android device.
- mcpp-plugins 0.19.0 against this engine: 11 consumer fixtures and 27
  plugin-logic cases pass; tests/cmake-consumer builds under
  MCPP_NO_AUTO_INSTALL=1 when its build program names its own cmake.
…gn record carries front matter

`std::to_string` over a filesystem clock's rep and over `uintmax_t` is
ambiguous on libc++, so the three sites that write or compare the stamp of a
toolchain named by path format the two values through `std::format` with an
explicit type. The design record gains the `subject`/`status` front matter
every record dated 2026-09-08 or later carries.
…air specialization

Exporting `std::vector<std::pair<std::string, std::filesystem::path>>` from
mcpp.toolchain.model made clang 20.1.7 on Windows crash while generating code
for `mcpp::pack::interface_set_digest`, a function of another module that
instantiates the same specialization and sorts by a pointer to its `first`. The
report named a file this branch never touched, which is how this hazard always
reads; `modules/manifest/src/types.cppm` records a GCC 16 case of the same
shape. The field is now a `ToolOverride` of two named members, and every reader
uses the same structured binding it used before.
…member

A `std::ranges` projection spelled as a pointer-to-member into a type the
module imports makes clang 20.1.7 crash in code generation, and the report names
an unrelated function: first `mcpp::pack::interface_set_digest`, then
`mcpp::doctor::why_report`, both on windows-2022. Every lookup this branch added
over the decision record now passes a predicate, which reads the same and
compiles on every host.
…p.lock is not claimed to hold it

The chapter, its mirror, SPEC-006 and the release notes said a build records such
a toolchain in mcpp.lock as `local`. It does not: the lock holds the result of
dependency resolution, a toolchain is not a resolved dependency, and nothing in
this branch writes one there. What is true is that the driver and each stated
tool enter the fingerprint and are recorded beside the build, so the fast paths
decline once one of them changed, and a machine without the tree is refused where
the declaration is read. The design record states the correction.
The third place an override may be stated was parsed inside `load_or_init`,
which bootstraps a home, so no unit test could reach it. `parse_payload_overrides`
is now a pure function of the parsed document, and three cases cover the two
shapes it accepts, the key it refuses, and a config with no such table.
… when it outgrows the channel

A build program that imports many host modules carries one
`-fmodule-file=<name>=<path>` per module, with absolute paths, and on Windows
`capture_exec` reaches a shell that tolerates 8191 bytes. mcpp-plugins'
all-rules-compile fixture imports fifteen and crossed that line the moment the
collection gained one more module, reporting only

    The command line is too long.
    build.mcpp failed to compile (exit 1)

which names neither the length nor the cause -- the family mcpp.build.cmdlimits
exists to make legible. The command now goes through `@file` when it is over
the budget that module states, which every driver mcpp supports reads, and the
file stays beside the program for a failed compile to show. Its quoting is
`response_file_body`, exported and covered by a unit case, because the command
it serves cannot be run on a host whose limit it does not cross.
`command -v true` prints `true`, not a path: a shell answers with what it would
run, and for a builtin that is the word. which() then found no file and reported
the name missing, so a bare-name payload override of such a name was refused with
`'true' is not found on PATH` on a machine carrying /usr/bin/true -- an answer
that sends the reader to the wrong place. Found while verifying the host class of
mcpp#755. PATH is now walked for a bare word the shell returned, and two cases
cover it: the builtin name resolves to its program, and a name no host has still
answers nothing.
A response file is not one format. clang and GCC tokenize it the GNU way, where
a backslash escapes the next character, so the Windows paths written plainly came
back with their separators eaten:

    clang++: error: no such file or directory:
      'D:amcpp-pluginsmcpp-pluginstestsall-rules-compiletarget.build-mcpp...'

Each argument is therefore wrapped in single quotes for those drivers, inside
which nothing is special, and an embedded single quote is closed, escaped and
reopened; cl and clang-cl keep Windows quoting, where a backslash is literal.
One case per grammar.
…ace manifest where a member is built

Both are what the code reads (prepare's runtime owner is the workspace manifest
when there is one); the table named only the root.
…ge needs

Measured with mcpp-plugins 0.19.0 on the released 2026.10.1.2: the reader is told

    error: mcpp.toml: error: [feature-xlings.deps-archive] xim:cmake:
    unknown key 'provision' in a scoped entry; expected 'version' and 'when'

and nothing about the version, because the floor check needs the document that
this very parse failed to produce. Every release of a plugin collection raises
its floor, so this is the first thing a user on an older engine meets.

The floor is therefore read from the file's text in the parse-failure path --
`mcpp` inside `[package]`, nothing else -- and when this engine is below it, the
refusal says so and names the upgrade, in the words the floor check already uses.
`stated_mcpp_floor` is exported and seven cases pin the shapes it reads.
…er escapes them inside quotes too

The first form single-quoted each argument, which a POSIX shell would take
literally and this tokenizer does not: LLVM's GNU tokenizer escapes a backslash
inside quotes as well as outside, so the Windows paths still arrived with their
separators eaten. Measured with clang 22.1.8 -- a response file holding
`'-DX=a\b'` yields `X=ab`, one holding `-DX=a\\b` yields `X=a\b` -- so every
backslash is doubled, every quote escaped, and whitespace handled by quoting the
whole argument. The case states the measurement.
…ays it cannot

`--ld-path` was appended inside the Linux clang branch, the only one that
consumes `link_toolchain_flags`. On macOS the stated linker therefore entered the
fingerprint -- touching the wrapper declined the fast path -- and took no part in
the link, with nothing said; the toolchain lab measured it on macos-15, where
build.ninja held no `--ld-path`. e2e 875 asserts that flag but skips on a host
without the llvm payload, which macOS CI is.

The flag belongs to the driver, not to a platform, so it is added once after every
shape has built its line. For a gcc toolchain the declaration is now refused where
it is read: gcc selects a linker by the name `ld` inside a `-B` directory, so a
program named anything else could not be chosen, and a silent `-B` would be the
same defect in the other direction.
…its gcc example no longer states one

The cross example named `tools = { ld = ... }` on a gcc tree, which the engine now
refuses, and nothing said which trees read that role.
Every refusal this feature adds has a case; this one did not. It runs against an
installed gcc payload and says so when none is present, rather than passing
silently on a host without one.
…ppends itself

A shell on Windows answers `C:/Program Files/CMake/bin/cmake` for a `cmake.exe`,
and process creation there appends `.exe`, so a path stated without it names a
program the machine would run; refusing it answers about spelling rather than
about the machine. The plugin-side resolver gained the same rule in
mcpp-plugins 0.19.0, where CI measured the refusal.

Verified against a declared payload overridden by `{ program = "bin/faketool" }`
with only `bin/faketool.exe` present: the build reports
`Using xim:prec-absent <- .../bin/faketool.exe  [custom . mcpp.toml:13]`.
…generate

`00_fixture_path_hygiene.sh` caught three of them: 873 and 874 interpolated `$work`
into a `[build-dependencies]` path, and 875 wrote the toolchain root, the linker
wrapper and a driver straight from shell variables. MSYS rewrites paths in argv
and the environment but not in file content, so on Windows a native mcpp would
read `/d/a/...` and resolve it against the current drive -- the failure the
helper's own header records costing a day. Each path now goes through
`host_path`, and the assertions that compare mcpp's output compare host
spellings too.
@speak-agent
speak-agent merged commit 4d81d06 into main Oct 1, 2026
45 of 48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: declared, programmable and observable sources for toolchains, payloads and plugin tools

1 participant