Skip to content

0.19.0: one resolver for a member's tool, payloads installed on request, and a toolchain a build program states (#41) - #43

Merged
speak-agent merged 25 commits into
mainfrom
feat/0.19.0-tool-sources
Oct 1, 2026
Merged

speak-agent merged 25 commits into
mainfrom
feat/0.19.0-tool-sources

Conversation

@speak-agent

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

Copy link
Copy Markdown
Member

Summary

A member that runs a program answers one question before it can plan anything: which program. Each member answered it its own way — an option here, a variable there, a silent PATH fallback in two members and a refusal of PATH in two others — and a tool named in build.mcpp did not stop the declared payload from being downloaded. On the engine of mcpp-community/mcpp#755 this release answers it once, for every member.

mcpp.plugins.tool (in plugins-core)

One order, written once:

  1. the build program's choice — o.cmake = "/usr/bin/cmake", tool::root(dir), tool::on_path();
  2. the variable that member has always read (MCPP_SLANGC, MCPP_GLSLC, MCPP_GLSLANG);
  3. the engine's override — [xlings.overrides], MCPP_XLINGS_OVERRIDE_<NS>_<NAME>, config.toml;
  4. the declared payload, asked for here when it is declared provision = "on-request".

A choice never asks for the payload, which is what makes "the build program names its own tool" mean "the payload is not downloaded". Every answer is recorded with mcpp::decision, so a build reports the source and mcpp why tool <name> can answer, and describe_missing is the one refusal text that lists every way to name the tool.

An option field is a tool::choice, which a string constructs: o.cmake = "/usr/bin/cmake" keeps working, and the build.mcpp line that wrote it is what the build reports.

Members

  • deps-cmake, deps-archive, deps-vcpkg, rules-spirv, rules-slang, rules-sycl, dist-appimage, dist-wix and rules-qt resolve their tools through the resolver.
  • rules-cuda, rules-hip, rules-ascendc and dist-apk gain the options they had none of: toolkit, and for dist-apk the six payloads it runs.
  • deps-cmake, deps-archive, deps-vcpkg, dist-appimage and dist-apk's bundletool declare their payloads provision = "on-request": an ordinary Linux build no longer installs appimagetool, an --format apk build no longer installs bundletool, and a project that names its own cmake downloads none.
  • rules-spirv and rules-slang keep their PATH fallback behind a warning that names the choice to write instead, until 2027-04-01 (SPEC-007 R6.2).

mcpp.plugins.toolchain (feature plugins-toolchain)

The builders a root build program states the build toolchain with, in the toolchain phase of a project whose [toolchain] says configure = "build.mcpp": layout, prefixed, compose, from_env_script (a vendor SDK's environment-setup-*), with_launcher, with_sysroot, with_tool, managed, env, configure(fn) and use(d).

Engine floor

2026.10.1.3, the release whose protocol 15 states where each tool and payload comes from. The per-member floors in the README and the member documents are raised for the members that use it.

Closes #41

Test plan

  • tests/plugin-logic: 27 cases pass, five of them new for the resolver — a choice asks for no payload, an override answers, a pending payload is requested and plans nothing, the payload answers last, and the refusal lists every way to name the tool. mcpp.plugins.testing gained xpkg_source, xpkg_program and phase so a case can state them.

  • 11 consumer fixtures build against the engine branch; all-rules-compile compiles every rule and dist member; a default build's output is unchanged.

  • New CI criterion tool-sources on all three platforms: with nothing named the member asks for the payload; with the build program naming a cmake the payload is not asked for and the source is reported; an override says the same and is reported as custom; --managed-only refuses both. It reads resolution.json, so it holds on a runner that already has the payload installed.

  • A defect this found in the resolver: it used the compile-time executable suffix, so a case stating a Windows row on Linux could not find vcpkg.exe. Both spellings are tried on every host now, as rules-spirv has always done.

  • Two defects the other platforms found, each with a measurement:

    • mcpp.plugins.testing left the real build's MCPP_XPKG_* in the environment. These cases run inside a build program, which the engine gives one _DIR, _PROGRAM and _SOURCE per payload its package declares — pending among them, for a payload declared provision = "on-request" that nothing asked for. Eight vcpkg cases therefore asked for xim:vcpkg and planned nothing wherever the payload was not installed: 19 of 27 on macOS arm64 and on Windows, 27 of 27 on a machine that had it. Measured both ways on one binary with the inherited value present: 19 of 27 before, 27 of 27 after. The keys are enumerated, not listed, because they are derived from package names.
    • Darwin exports environ to a main program only, so the first fix failed to compile there; _NSGetEnviron() is used instead.
  • The spirv fixture's step asserted on a status line while capturing stdout alone. mcpp writes narration to standard error from 2026.10.1.1, so the log was empty and the step reported that the rule had not run while it had.

  • An ecosystem reading this release produces: xim:vcpkg no longer appears in the macOS or Windows rules job logs, where main provisions xim:vcpkg@>=2026.7.27. The saving is real, and it is also what exposed the first defect above — installing one payload less makes a test environment's assumption visible.

Order

This PR needs mcpp 2026.10.1.3, which is mcpp-community/mcpp#758. CI here is pinned to that release and goes green once it is published; until then it is validated by dispatching this workflow with mcpp_source_ref=feat/build-sources, which builds the engine from that branch.

…nstalled on request, and a toolchain a build program states

A member that runs a program answers one question before it can plan anything:
which program. Each member answered it its own way -- an option here, a variable
there, a silent PATH fallback in two members and a refusal of PATH in two others
-- and a tool named in build.mcpp did not stop the declared payload from being
downloaded. On the engine of mcpp#755 this release answers it once.

- `mcpp.plugins.tool` (in `plugins-core`) resolves a member's tool in one order:
  the build program's choice, the member's legacy variable, the engine's
  override, then the declared payload. A choice never asks for the payload,
  which is what makes "the build program names its own tool" mean "the payload
  is not downloaded". Every answer is recorded with `mcpp::decision`, so a build
  reports the source and `mcpp why tool <name>` can answer, and one refusal text
  lists every way to name the tool.
- Every member that runs a payload tool takes its option as a `tool::choice`,
  which a string constructs, so `o.cmake = "/usr/bin/cmake"` keeps working and
  the line that wrote it is what the build reports. `rules-cuda`, `rules-hip`,
  `rules-ascendc` and `dist-apk` gain the options they had none of.
- `deps-cmake`, `deps-archive`, `deps-vcpkg`, `dist-appimage` and `dist-apk`'s
  `bundletool` declare their payloads `provision = "on-request"`: a build that
  names its own tool, or never reaches the tool, downloads nothing.
- `rules-spirv` and `rules-slang` keep their PATH fallback behind a warning that
  names the choice to write instead, until 2027-04-01 (SPEC-007 R6.2).
- `mcpp.plugins.toolchain` (feature `plugins-toolchain`) builds the statement a
  root build program makes in its toolchain phase: `layout`, `prefixed`,
  `compose`, `from_env_script`, `with_launcher`, `managed`, `env`, `configure`
  and `use`.
- `mcpp.plugins.testing` states a payload's source, its program and the phase, so
  a case can describe an override or a pending payload.

The engine floor is 2026.10.1.3, the release whose protocol 15 states where each
tool and payload comes from.

Closes #41

Test plan
- `tests/plugin-logic`: 27 cases pass, including five for the resolver (a choice
  asks for no payload, an override answers, a pending payload is requested, the
  payload answers last, and the refusal lists every way to name the tool).
- 11 consumer fixtures build against the engine branch, `all-rules-compile`
  compiles every rule and dist member, and the default build's output is
  unchanged.
- New CI criterion `tool-sources` on all three platforms: with nothing named the
  member asks for the payload; with the build program naming a cmake the payload
  is not asked for and the source is reported; an override says the same; and
  `--managed-only` refuses both. The criterion reads `resolution.json`, so it
  holds on a runner that already has the payload.
…n be measured before it is released

`MCPP_VERSION` states the engine this collection needs, and that release may
not exist yet: 0.19.0 is written against mcpp#755 and names 2026.10.1.3 while it
is still a branch. Every fetch and every sandbox cache key now reads
`MCPP_BOOTSTRAP`, the release that is published, and `mcpp_source_ref` builds the
engine under review with it. The two meet again when that release exists.
…n reads narration from stderr

Two failures from the other platforms, with one cause each.

The kit left the real build's `MCPP_XPKG_*` in the environment. These cases run
inside a build program, which the engine gives one `_DIR`, `_PROGRAM` and
`_SOURCE` per payload its package declares -- including `pending`, for a payload
declared `provision = "on-request"` that nothing has asked for. Eight vcpkg cases
therefore asked for `xim:vcpkg` and planned nothing wherever the payload was not
installed, and passed wherever it was: 19 of 27 on macOS arm64 and on Windows, 27
of 27 here. The keys are derived from package names, so they are enumerated
rather than listed. Measured both ways on the same binary with the inherited
value present: 19 of 27 before, 27 of 27 after.

The spirv fixture's step asserted on a status line while capturing stdout alone.
mcpp writes narration to standard error from 2026.10.1.1, so the log was empty
and the step reported that the rule had not run while it had.
…s it

Darwin exports `environ` to a main program only and offers `_NSGetEnviron()`
instead, so the macOS host-module compile failed with `use of undeclared
identifier 'environ'`. Elsewhere the symbol is declared here rather than taken
from <unistd.h>, whose declaration a module purview does not see.
…which of its payloads is asked for

The examples named 0.8.0 and 0.11.0, which a reader copies into a project that
then resolves a release without what the page describes. The reason dist-apk's
tools are features also predated on-request provisioning: `xim:bundletool` is now
asked for while a bundle is planned, and `xim:kotlin` is still installed with the
feature that compiles Kotlin.
… values

While this rule took the payload directory and appended `/bin/clang++`, one
string served as both. A resolver answers the program, and the migration left
`-L` pointing at it: the SYCL consumer's build failed with

    ld.lld: error: unable to find library -lsycl

in `compat.opencl`, because the search directory had become
`<root>/bin/clang++/lib`. The link search now uses the resolved root. Every other
migrated member was audited for the same swap: the toolkit members take `.root`
and the tool members take `.program`, and no other site appends a directory to a
program path.
The page named `compose`, which does not exist, and left out `newest_under`,
`managed` and `with_family`. Every name on that line and in the tool section was
then checked against the sources.
…file carries

A build program writes the path a shell gave it, and on Windows `command -v cmake`
answers `C:/Program Files/CMake/bin/cmake` for a `cmake.exe`. The resolver
insisted on the exact spelling and refused a program the host itself would have
run -- measured in CI, where the cmake consumer reported
`options::cmake = "C:/Program Files/CMake/bin/cmake" (not found)` on a runner
carrying cmake, and then failed to compile because the subproject it configures
never built. A stated path now follows the same suffix rule as a discovered one,
which `program_in` has always applied, and a case states it on a Linux row
because the rule is the same on every host.
Inserting the toolchain feature put it between that comment and the feature it
describes.
…o inline the iterator here

The range-for walked a `std::string` through `__gnu_cxx::__normal_iterator`, whose
`operator*` and `operator++` are `always_inline` and reach this module from the
`std` module. Once this file imported one module more, gcc 16.1.0 refused both:

    error: inlining failed in call to 'always_inline'
      'constexpr __gnu_cxx::__normal_iterator<...>::operator*() const'
    note: called from here      for (char c : message)

in `warn@mcpp.rules.qt` alone, while every other range-for in the collection
compiled. Measured on this machine with `MCPP_TOOLCHAIN=gcc@16.1.0`: two errors
before, none after, and the qt consumer builds. An index touches no iterator, so
it does not depend on what a BMI carries across a module boundary -- the same
hazard class the engine records for a clang 20.1.7 crash, where the error names a
file the change never touched.
…ection's floor

The pin existed because the floor named a release that did not exist yet; it does
now, so the ordinary path runs the engine this collection states and no dispatch
is needed to validate it.
Both sides added a `plugin-logic` case at the same point, and the conflict
boundary fell inside one of them. The resolution keeps every case from both:
the six tool cases of this branch and the qrc case from #42, whose fix in
`qrc_files` merged untouched. 29 of 29 cases pass with the released engine.
…mes from

The page told the reader to comment the declaration out, which was the only way
out while every declared payload was provisioned. An override states the same
thing without removing the declaration, so a machine without that SDK still gets
the ecosystem's.
…d users

The mechanism was documented from each side separately -- the engine's keys in
mcpp docs/23, the module's API in plugin-development.md, each member's options in
its own page -- and nothing stated the two halves that have to meet: a
`tool::choice` in a build program avoids a download only if the plugin declared
that payload `provision = "on-request"`, because an eager payload is provisioned
before any build program runs. That pairing is the question both audiences
actually ask, and it was the original symptom behind mcpp#755.

The page is written as scenarios: six for a project (write nothing, name the
machine's tool, state it in the manifest, replace it for one job, share it across
projects, build offline and audit), seven for an author (the three pieces, the
pair, a branch-only payload, a tree rather than a program, a legacy variable, a
member that only looks, and how to prove it without installing anything), and the
design decisions behind them. It closes with a table of every official member
that drives a tool, how it declares it, and why -- so the eager ones are not read
as oversights.

Figures are the lab's measurements, including that the saving is the one-time
provisioning cost rather than a per-build one.
…re being written down

The page had the three spellings as one trailing comment, which is not an account
of them. Section 3.2 now gives each as code on `deps-vcpkg` -- a program, a tree,
PATH, and the `vcpkg_root` spelling kept since 0.18.1 -- plus the relative-path
rule, deciding from the environment inside the build program, and the fact that a
stated choice which fails is refused rather than replaced.

Compiling the example as a real build program found a defect in it: a feature
makes a module available, not visible, so `tool::root` needs
`import mcpp.plugins.tool;` while a plain string assignment does not. The page
states that, with the compiler's own words. The refusal text it quotes was copied
from a run.
It was a subsection beside "state it in the manifest" and "replace it for one CI
job", so a reader had to recognise which of the six scenarios was theirs. It is now
chapter 3, in the order the question is actually asked: the four spellings, where a
relative path points, deciding inside the build program, what happens when a stated
choice fails, why this avoids the download at all, which option each member reads
and what it looks for under a root, and how to confirm nothing was downloaded. The
former chapter 3 becomes chapter 4, the other ways.

The per-member table is read from each spec: the option name, the program names,
the directories searched under a root, and the variable the member still reads.
Three members name a root rather than a program, which the table says so that a
reader does not pass them a path to an executable.
@speak-agent
speak-agent marked this pull request as ready for review October 1, 2026 20:41
@speak-agent
speak-agent merged commit 7f5f0ed into main Oct 1, 2026
4 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.

0.19.0: one tool-source resolver for every member, on-request payloads, and a toolchain description library

1 participant