From 93e59b5a2627d5e3b32dbb4e100deec7f7187cb3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 1 Oct 2026 22:22:18 +0800 Subject: [PATCH 01/24] 0.19.0: one resolver for where a member's tool comes from, payloads installed 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 ` 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. --- ...1-ecosystem-build-plugin-framework-plan.md | 79 +++++ .github/scripts/check-deps-and-qt.sh | 92 ++++- .github/workflows/ci.yml | 21 +- README.md | 42 ++- deps/archive.cppm | 31 +- deps/cmake.cppm | 34 +- deps/deps.cppm | 10 + deps/vcpkg.cppm | 32 +- dist/apk.cppm | 53 ++- dist/appimage.cppm | 54 ++- dist/wix.cppm | 43 ++- docs/deps.md | 25 +- docs/dist-apk.md | 2 +- docs/dist.md | 4 +- docs/plugin-development.md | 53 ++- docs/rules-qt.md | 4 +- docs/rules.md | 12 +- mcpp.toml | 50 ++- rules/ascendc.cppm | 39 +- rules/cuda.cppm | 48 ++- rules/hip.cppm | 38 +- rules/qt.cppm | 29 +- rules/slang.cppm | 48 ++- rules/spirv.cppm | 115 ++++-- rules/sycl.cppm | 25 +- src/plugins.cppm | 2 +- src/testing.cppm | 27 +- src/tool.cppm | 335 ++++++++++++++++++ src/toolchain.cppm | 200 +++++++++++ tests/plugin-logic/build.mcpp | 97 +++++ 30 files changed, 1414 insertions(+), 230 deletions(-) create mode 100644 .agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md create mode 100644 src/tool.cppm create mode 100644 src/toolchain.cppm diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md new file mode 100644 index 0000000..b4214e9 --- /dev/null +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -0,0 +1,79 @@ +# mcpp 构建插件框架:实施计划与任务依赖 + +日期:2026-10-01。状态:执行中。 + +依据:`2026-10-01-ecosystem-build-plugin-framework-design.md` v3(下称“设计”)及其 §0 的已定事项。本记录覆盖 2026-10-01 /goal 的要求: +- D6 一步到位,不分期交付; +- 每个仓库单 PR,标题带版本号; +- CI 全绿; +- 自我 review,包括生态级 review; +- 发布:GitHub 加 GitCode(本地 gtc),mcpp-index 与 xim-pkgindex 登记; +- 在 xlings subos 沙箱中以 CN 镜像做真实验证; +- 关闭已完成的 issue; +- 文档与代码注释不含表情符号。 + +## 1. 交付物与版本 + +| 仓库 | PR | 版本 | 内容 | +|---|---|---|---| +| mcpp-community/mcpp | 1 个 | 发布当日的 `YYYY.M.D.N` | 核心:决策记录与输出语义、E1、E2、决策指令、自定义工具链(路径与 toolchain 阶段)、规范与文档(中英)、测试 | +| mcpp-community/mcpp-plugins | 1 个 | 0.19.0 | `mcpp.plugins.tool`、`mcpp.plugins.toolchain`、成员迁移、按需条目、文档、fixture、CI | +| speak-agent/llvm-macos27-lab | 1 个以上 | — | LLVM `arm64e.x1` 修复在 macOS 27 / Xcode 27 上的可用性;裁剪后的工具链资产 | +| speak-agent/mcpp-framework-lab | 1 个以上 | — | 工具来源、输出、可观察性的验收 | +| speak-agent/mcpp-toolchain-lab | 1 个以上 | — | 自定义工具链的验收(含 macOS 27) | +| mcpplibs/mcpp-index | 1 个 | — | 登记 plugins 0.19.0 | +| openxlings/xim-pkgindex | release 自动开出的 bump PR | — | 登记 mcpp 新版本 | + +设计 §10 中的 S4(xim `llvm` 23.1.3 打包)取决于上游发布。它不在本 goal 的关键路径上:若上游在收尾前发布了 23.1.3,就按设计执行;否则在 mcpp#669 中记录 lab 的结果,并留作后续。 + +## 2. 任务与依赖 + +``` +T0 issues(mcpp、plugins)与 lab 仓库 +T1 核心:来源类、决策记录、渲染(status 行、标签、Finished 汇总)、resolution.json、 + mcpp why tool/payload、--managed-only +T2 E1 覆盖(清单、target cfg、全局配置、环境变量;供给过滤;统一校验; + fillXpkgDirs 的 DIR/PROGRAM/SOURCE;xpkg_program、xpkg_source) ← T1 +T3 E2 按需(provision 键;xpkg_request/xpkg_pending;xpkg-request 指令; + 批量供给与选择性重跑;离线拒绝;plan_only note) ← T1, T2 +T4 decision 指令(插件把工具决定写进记录) ← T1 +T5 自定义工具链按路径([toolchain] 表、MCPP_TOOLCHAIN=path:、bootstrap 键; + 探测;不做 post-install;指纹含内容 hash;lock 记为 local) ← T1 +T6 toolchain 阶段(configure = "build.mcpp";mcpp:toolchain 指令;mcpp::phase) ← T4, T5 +T7 mcpp 规范与文档(中英)、CHANGELOG、设计记录 ← T1–T6 的语义 +T8 mcpp 单测与 e2e ← 各项随实现 +T9 plugins 0.19.0(tool、toolchain、成员迁移、on-request、文档、fixture、CI) + ← T2, T3, T4, T6 的宿主接口 +T10 llvm-macos27-lab (独立,最早开始) +T11 framework-lab、toolchain-lab ← T9(以及 T10 的资产) +T12 两个 PR 的 CI 全绿;以 mcpp_source_ref 指向 mcpp 分支做 plugins 预验证 +T13 自我 review(含生态级) +T14 发布 mcpp → xim-pkgindex bump PR 合入 → bootstrap pin +T15 发布 plugins 0.19.0 → gtc → mcpp-index +T16 沙箱验证(xlings subos,CN 镜像);lab 改指已发布版本复跑 +T17 issue 评论与关闭;设计记录追加状态行 +``` + +**并行**: +- T10 不依赖任何实现,最早开始; +- T1 完成后,T2/T4/T5 可以并行; +- T7 与 T9 的文档部分在接口冻结后与实现并行; +- 后台 agent 同时最多 3 个。 + +## 3. 质量门(每个视角在哪里被检查) + +| 视角 | 检查 | +|---|---| +| 架构 | 核心只承担语义与接口;具体工具的知识留在 L2 与成员中(设计 §3 原则 4);新模块的 import 指向类型的提供者 | +| 稳定性 | 默认构建的输出与行为逐字节不变(framework-lab `default` case 与 e2e);已有 e2e 全部通过 | +| 简洁 | 两个入口共用一个描述 schema;一份决策记录驱动全部输出 | +| 用户体验 | 显式选择一行 `Using`;错误首行写明来源;报错给出四种指定方式 | +| 兼容性 | 旧插件不改代码即可从 E1 受益;`tool::choice` 可由字符串隐式构造;新键在旧引擎上会被拒或忽略,提 floor | +| 跨平台 | 单测与 e2e 覆盖 Linux、macOS、Windows;Windows 路径与 `.exe`;macOS 27 由 lab 覆盖 | +| 一致性 | 优先级只写一份(核心一份、L2 一份);文档中英结构一一对应 | +| 无感升级 | 不写任何新键的项目在新版本上行为不变;插件 0.19.0 的成员在默认路径上下载与输出不变 | +| 测试覆盖 | 每个新清单键、指令、查询函数与命令都有单测或 e2e;每条拒绝都有一个反例用例 | + +## 4. 执行记录 + +(随执行追加:PR、run、发布与验证结果。) diff --git a/.github/scripts/check-deps-and-qt.sh b/.github/scripts/check-deps-and-qt.sh index 0f41de8..9ab9af6 100644 --- a/.github/scripts/check-deps-and-qt.sh +++ b/.github/scripts/check-deps-and-qt.sh @@ -576,7 +576,97 @@ vcpkg_make_port() { echo "ok: a make-based port builds with the managed toolset first on the kept PATH" } +# 0.19.0 (mcpp#755): a tool the build program names is used, and the payload the +# member declares `provision = "on-request"` is NOT asked for. +# +# THE CRITERION IS THE ENGINE'S OWN RECORD, NOT THE STORE. A runner may already +# hold `xim:cmake` -- every other case here installs it -- so "nothing was +# downloaded" cannot be read from the store, and a clean `MCPP_HOME` would cost +# a download per case. `resolution.json` states what each payload's source was +# and whether this build asked for it, which is the decision itself. The pair is +# the criterion: the same project asks for the payload when nothing names a +# tool, and does not when the build program names one. +tool_sources() { + cd "$ROOT/tests/cmake-consumer" + local cmake; cmake=$(command -v cmake || true) + [ -n "$cmake" ] || { echo "SKIP: no cmake on this host to name"; return 0; } + + # `considered` of the payload entry, and the class of each subject. + record() { + python3 - <<'PYEOF' +import glob, json, sys +paths = glob.glob("target/*/*/resolution.json") +if not paths: + print("NO-RECORD"); raise SystemExit(0) +doc = json.load(open(sorted(paths)[0])) +for d in doc.get("sources", []): + print(d["subject"], d["class"], "|", "; ".join(d.get("considered", [])), sep="\t") +PYEOF + } + + cp build.mcpp build.mcpp.bak + restore() { mv -f build.mcpp.bak build.mcpp 2>/dev/null || true; } + trap restore EXIT + + # The control: nothing names a tool, so the member asks for the payload. + rm -rf target + NO_COLOR=1 "$MCPP" build >/dev/null 2>&1 || fail "the control build failed" + record | grep -q "payload:xim:cmake.*installed on request" \ + || fail "the control did not ask for xim:cmake: $(record)" + + # The build program names the host's cmake, through the member's option. + python3 - "$cmake" <<'PYEOF' +import pathlib, sys +p = pathlib.Path("build.mcpp") +t = p.read_text() +marker = " o.shared = true;" +assert marker in t, t +p.write_text(t.replace(marker, marker + '\n o.cmake = "%s";' % sys.argv[1].replace("\\", "/"), 1)) +PYEOF + rm -rf target + local out + out=$(MCPP_NO_AUTO_INSTALL=1 NO_COLOR=1 "$MCPP" build 2>&1) \ + || fail "a named cmake still needed the payload: $out" + case "$out" in + *"Using cmake (mcpp.deps.cmake)"*"[program · build.mcpp:"*) ;; + *) fail "the build did not report the tool's source: $out" ;; + esac + case "$out" in + *"program: cmake (mcpp.deps.cmake)"*) ;; + *) fail "the Finished line did not summarise the source: $out" ;; + esac + record | grep -q "payload:xim:cmake.*not requested by this build" \ + || fail "the payload was asked for although the program named a cmake: $(record)" + MCPP_NO_AUTO_INSTALL=1 "$MCPP" run | grep -q '^cmake-consumer: greet says 42$' \ + || fail "the program built with the named cmake does not run" + + # The same statement as an engine override, with the payload untouched. + restore + rm -rf target + out=$(MCPP_XLINGS_OVERRIDE_XIM_CMAKE="$cmake" MCPP_NO_AUTO_INSTALL=1 NO_COLOR=1 "$MCPP" build 2>&1) \ + || fail "an override still needed the payload: $out" + case "$out" in + *"Using xim:cmake"*"[custom · env MCPP_XLINGS_OVERRIDE_XIM_CMAKE]"*) ;; + *) fail "the build did not report the override: $out" ;; + esac + record | grep -q "payload:xim:cmake.*custom.*overridden" \ + || fail "the record does not state the override: $(record)" + + # `--managed-only` refuses the same build, naming the payload. + rm -rf target + out=$(MCPP_XLINGS_OVERRIDE_XIM_CMAKE="$cmake" MCPP_NO_AUTO_INSTALL=1 NO_COLOR=1 \ + "$MCPP" build --managed-only 2>&1 || true) + case "$out" in + *managed-only*xim:cmake*) ;; + *) fail "--managed-only did not refuse the override: $out" ;; + esac + trap - EXIT + rm -rf target + echo "OK: a named tool is used, the payload is not asked for, and both are reported" +} + case "${1:-}" in + tool-sources) tool_sources ;; vcpkg-consumer) vcpkg_consumer ;; vcpkg-libcxx) vcpkg_libcxx ;; archive-consumer) archive_consumer ;; @@ -592,5 +682,5 @@ case "${1:-}" in vcpkg-managed) vcpkg_managed ;; vcpkg-msbuild-refused) vcpkg_msbuild_refused ;; vcpkg-make-port) vcpkg_make_port ;; - *) echo "usage: $0 vcpkg-consumer|vcpkg-libcxx|archive-consumer|vcpkg-workspace|cmake-consumer|cmake-cache|qt-consumer|qt-widgets-consumer|qt-sdk-consumer|qt-import-only|plugin-logic|vcpkg-crt|vcpkg-managed|vcpkg-msbuild-refused|vcpkg-make-port"; exit 2 ;; + *) echo "usage: $0 vcpkg-consumer|vcpkg-libcxx|archive-consumer|vcpkg-workspace|cmake-consumer|cmake-cache|qt-consumer|qt-widgets-consumer|qt-sdk-consumer|qt-import-only|plugin-logic|tool-sources|vcpkg-crt|vcpkg-managed|vcpkg-msbuild-refused|vcpkg-make-port"; exit 2 ;; esac diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa66d43..a75e4c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,14 @@ env: # 2026.9.28.3 IS WHAT 0.17.0 NEEDS, and the package floor states it: the # build information `mcpp.plugins.toolset` reads, `mcpp::report`, and one # placement edge for many files (mcpp#734). - MCPP_VERSION: 2026.9.28.3 + # + # 2026.10.1.3 IS WHAT 0.19.0 NEEDS (mcpp#755): protocol 15 states where each + # tool and payload comes from -- `xpkg_source`, `xpkg_program`, + # `xpkg_request`, `mcpp::decision`, `mcpp::toolchain`, `mcpp::phase` -- and + # it is the release that honours the `provision = "on-request"` entries this + # collection now declares, so a consumer that names its own tool downloads + # nothing. An older engine refuses the manifest at the package floor. + MCPP_VERSION: 2026.10.1.3 # AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE. # # Empty on every push and pull request, so the steps run the release above. @@ -1690,6 +1697,12 @@ jobs: - name: deps-cmake builds a CMake subproject as an action and links it run: bash .github/scripts/check-deps-and-qt.sh cmake-consumer + # 0.19.0 (mcpp#755): a tool the build program names is used and the + # payload is not asked for; an override says the same and is reported; + # `--managed-only` refuses both. + - name: a tool the build program names is used, and its payload is not asked for + run: bash .github/scripts/check-deps-and-qt.sh tool-sources + # 0.18.0: an installation is kept under a key with no path of the # machine, and taken by a build without target/, by a checkout at another # path; an edit, other CFLAGS and an installation that names its own @@ -2388,6 +2401,12 @@ jobs: - name: deps-cmake builds a CMake subproject as an action and links it run: bash .github/scripts/check-deps-and-qt.sh cmake-consumer + # 0.19.0 (mcpp#755): a tool the build program names is used and the + # payload is not asked for; an override says the same and is reported; + # `--managed-only` refuses both. + - name: a tool the build program names is used, and its payload is not asked for + run: bash .github/scripts/check-deps-and-qt.sh tool-sources + # 0.18.0: an installation is kept under a key with no path of the # machine, and taken by a build without target/, by a checkout at another # path; an edit, other CFLAGS and an installation that names its own diff --git a/README.md b/README.md index a31479c..6e146f4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ module name the member declares, and configures it there. ```toml [build-dependencies.mcpp] -plugins = { version = "0.18.1", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.19.0", features = ["rules-spirv"], host-module = true } ``` ```cpp @@ -48,30 +48,31 @@ rule, is in [docs/engine-and-rules.md](docs/engine-and-rules.md). ## Members -From 0.17.0 the package as a whole needs mcpp 2026.9.28.3 (`[package] mcpp`); -the column states the release each member's behaviour first needed. +From 0.19.0 the package as a whole needs mcpp 2026.10.1.3 (`[package] mcpp`), +whose protocol 15 states where each tool and payload comes from; the column +states the release each member's behaviour first needed. | feature | module | mcpp floor | what it does | doc | |---|---|---|---|---| -| `rules-ascendc` | `mcpp.rules.ascendc` | 2026.9.6.6 | Compiles Ascend C (`*.asc`) with BiSheng in mixed mode, so the object joins the ordinary link. | [rules](docs/rules.md#rules-ascendc) | -| `rules-cuda` | `mcpp.rules.cuda` | 2026.9.6.6 | Compiles CUDA (`*.cu`) through clang with an LLVM toolchain or nvcc with a GCC one. | [rules](docs/rules.md#rules-cuda) | -| `rules-hip` | `mcpp.rules.hip` | 2026.9.6.6 | Compiles HIP (`*.hip`) with the project's clang on the NVIDIA platform. | [rules](docs/rules.md#rules-hip) | +| `rules-ascendc` | `mcpp.rules.ascendc` | 2026.10.1.3 | Compiles Ascend C (`*.asc`) with BiSheng in mixed mode, so the object joins the ordinary link. | [rules](docs/rules.md#rules-ascendc) | +| `rules-cuda` | `mcpp.rules.cuda` | 2026.10.1.3 | Compiles CUDA (`*.cu`) through clang with an LLVM toolchain or nvcc with a GCC one. | [rules](docs/rules.md#rules-cuda) | +| `rules-hip` | `mcpp.rules.hip` | 2026.10.1.3 | Compiles HIP (`*.hip`) with the project's clang on the NVIDIA platform. | [rules](docs/rules.md#rules-hip) | | `rules-metal` | `mcpp.rules.metal` | 2026.9.8.1 | Compiles `.metal` shaders into Metal libraries with the host's Xcode and deploys them beside the program. | [rules](docs/rules.md#rules-metal) | -| `rules-qt` | `mcpp.rules.qt` | 2026.9.27.1 | Runs `moc`, `uic`, `rcc` and Qt's Linguist tools as actions, links the Qt modules and places their runtime. | [rules-qt](docs/rules-qt.md) | -| `rules-slang` | `mcpp.rules.slang` | 2026.9.7.1 | Compiles Slang (`*.slang`) and embeds or places the result. | [rules](docs/rules.md#rules-slang) | -| `rules-spirv` | `mcpp.rules.spirv` | 2026.9.6.6 | Compiles GLSL and HLSL shader stages to SPIR-V and embeds or places the result. | [rules](docs/rules.md#rules-spirv) | +| `rules-qt` | `mcpp.rules.qt` | 2026.10.1.3 | Runs `moc`, `uic`, `rcc` and Qt's Linguist tools as actions, links the Qt modules and places their runtime. | [rules-qt](docs/rules-qt.md) | +| `rules-slang` | `mcpp.rules.slang` | 2026.10.1.3 | Compiles Slang (`*.slang`) and embeds or places the result. | [rules](docs/rules.md#rules-slang) | +| `rules-spirv` | `mcpp.rules.spirv` | 2026.10.1.3 | Compiles GLSL and HLSL shader stages to SPIR-V and embeds or places the result. | [rules](docs/rules.md#rules-spirv) | | `rules-swift` | `mcpp.rules.swift` | 2026.9.8.1 | Compiles a package's `.swift` sources into one module the C and C++ sources call. | [rules](docs/rules.md#rules-swift) | -| `rules-sycl` | `mcpp.rules.sycl` | 2026.9.6.6 | Compiles SYCL (`*.sycl`) with DPC++. | [rules](docs/rules.md#rules-sycl) | +| `rules-sycl` | `mcpp.rules.sycl` | 2026.10.1.3 | Compiles SYCL (`*.sycl`) with DPC++. | [rules](docs/rules.md#rules-sycl) | | `tools-embed` | `mcpp.tools.embed` | 2026.9.5.4 | Writes a data file into a header the program compiles in. | [tools-embed](docs/tools-embed.md) | | `tools-island` | `mcpp.tools.island` | 2026.9.7.1 | Generates the `extern "C"` boundary and the C++ module of a code island. | [tools-island](docs/tools-island.md) | -| `dist-appimage` | `mcpp.dist.appimage` | 2026.9.11.1 | Turns the tree `mcpp pack` stages into an AppImage (Linux). | [dist](docs/dist.md#dist-appimage) | -| `dist-wix` | `mcpp.dist.wix` | 2026.9.11.1 | Builds an MSI of the staged tree, and a Burn bundle chaining it (Windows). | [dist](docs/dist.md#dist-wix) | +| `dist-appimage` | `mcpp.dist.appimage` | 2026.10.1.3 | Turns the tree `mcpp pack` stages into an AppImage (Linux). | [dist](docs/dist.md#dist-appimage) | +| `dist-wix` | `mcpp.dist.wix` | 2026.10.1.3 | Builds an MSI of the staged tree, and a Burn bundle chaining it (Windows). | [dist](docs/dist.md#dist-wix) | | `dist-apple` | `mcpp.dist.apple` | 2026.9.14.2 | Lays out a macOS or iOS application bundle, signs it and writes a disk image. | [dist-apple](docs/dist-apple.md) | | `dist-web` | `mcpp.dist.web` | 2026.9.13.1 | Copies a `wasm32-emscripten` program and its files into a web directory with an `index.html`. | [dist](docs/dist.md#dist-web) | -| `dist-apk` | `mcpp.dist.apk` | 2026.9.14.2 | Packs the native closure into a signed APK or App Bundle, with Java, Kotlin and Maven libraries. | [dist-apk](docs/dist-apk.md) | -| `deps-vcpkg` | `mcpp.deps.vcpkg` | 2026.9.28.3 | Installs a `vcpkg.json` manifest as an action with the toolset mcpp resolved, and maps the prefix into the build. | [deps](docs/deps.md#deps-vcpkg) | -| `deps-cmake` | `mcpp.deps.cmake` | 2026.9.28.3 | Builds and installs a CMake subproject as an action with Ninja and the toolset mcpp resolved, keeps the installation for any build with the same key, and maps the prefix into the build. | [deps](docs/deps.md#deps-cmake) | -| `deps-archive` | `mcpp.deps.archive` | 2026.9.26.2 | Extracts a zip archive the project keeps and places its tree beside the program. | [deps](docs/deps.md#deps-archive) | +| `dist-apk` | `mcpp.dist.apk` | 2026.10.1.3 | Packs the native closure into a signed APK or App Bundle, with Java, Kotlin and Maven libraries. | [dist-apk](docs/dist-apk.md) | +| `deps-vcpkg` | `mcpp.deps.vcpkg` | 2026.10.1.3 | Installs a `vcpkg.json` manifest as an action with the toolset mcpp resolved, and maps the prefix into the build. | [deps](docs/deps.md#deps-vcpkg) | +| `deps-cmake` | `mcpp.deps.cmake` | 2026.10.1.3 | Builds and installs a CMake subproject as an action with Ninja and the toolset mcpp resolved, keeps the installation for any build with the same key, and maps the prefix into the build. | [deps](docs/deps.md#deps-cmake) | +| `deps-archive` | `mcpp.deps.archive` | 2026.10.1.3 | Extracts a zip archive the project keeps and places its tree beside the program. | [deps](docs/deps.md#deps-archive) | Some features add a sub-capability to a member: `dist-apk-kotlin` and `dist-apk-maven` (Kotlin sources, a Maven graph), and `deps`, which the deps @@ -79,12 +80,15 @@ members imply. **The general library (0.17.0).** `plugins-core` gives a build program `mcpp.plugins.declare`, `mcpp.plugins.toolset` (the resolved toolchain, -translated for a foreign build system) and `mcpp.plugins.fs` (deterministic file -generation and placement). Every member implies it; a build program that uses +translated for a foreign build system), `mcpp.plugins.fs` (deterministic file +generation and placement) and `mcpp.plugins.tool` (where a tool a member runs +comes from, one order for every member, 0.19.0). Every member implies it; a build program that uses only these modules, and a third-party plugin built on them, name it on the dependency edge. `plugins-testing` adds `mcpp.plugins.testing`, which runs a plugin function against a stated build context and compares what it declared. -Both need mcpp 2026.9.28.3. `surface`, the name of `plugins-core` before +`plugins-toolchain` adds `mcpp.plugins.toolchain`, the builders a root build +program states the build toolchain with (0.19.0). +All three need mcpp 2026.10.1.3. `surface`, the name of `plugins-core` before 0.17.0, is kept until 2027-03-28. A feature states a mechanism and the tools that mechanism runs; the libraries and SDKs a program links are the project's declaration (0.15.0 removed `rules-qt-xim*`; see [rules-qt](docs/rules-qt.md)). diff --git a/deps/archive.cppm b/deps/archive.cppm index 74eef89..1ca1abd 100644 --- a/deps/archive.cppm +++ b/deps/archive.cppm @@ -45,8 +45,9 @@ struct options { // Names the action and the directory the archive is extracted into. Empty // takes the archive's file name without its extension. std::string name; - // The `cmake` executable. Empty is the `xim:cmake` payload. - std::string cmake; + // The `cmake` executable (0.19.0: a `tool::choice`, so a path still + // assigns). Default: the `xim:cmake` payload, or an override of it. + mcpp::plugins::tool::choice cmake; }; struct result { @@ -159,24 +160,18 @@ inline result unpack(const options& opt) { } // ── the tool ── - std::string cmake; - if (!opt.cmake.empty()) { - cmake = mcpp::deps::generic(mcpp::deps::absolute_from_root(opt.cmake)); - } else if (const std::string root = mcpp::xpkg_dir("xim", "cmake"); !root.empty()) { - const bool win = std::string(mcpp::host()).find("windows") != std::string::npos; - for (auto const& sub : { fs::path("bin"), fs::path("CMake.app") / "Contents" / "bin" }) { - const auto exe = fs::path(root) / sub / (win ? "cmake.exe" : "cmake"); - if (fs::is_regular_file(exe, ec)) { cmake = mcpp::deps::generic(exe); break; } - } - } + const auto tool = mcpp::plugins::tool::resolve(mcpp::deps::cmake_spec("mcpp.deps.archive"), + opt.cmake); + const std::string cmake = tool.program; if (cmake.empty()) { // Nothing is extracted, so nothing can be deployed: a deployed file - // must be an output of some action. - mcpp::deps::warn(std::format( - "{}: cmake is not installed (xpkg_dir(\"xim\", \"cmake\") answered \"{}\"), so this plan " - "extracts nothing from {}. The `deps-archive` feature declares `xim:cmake`; " - "`mcpp build` provisions it before this program runs.", - who, std::string(mcpp::xpkg_dir("xim", "cmake")), mcpp::deps::generic(archive))); + // must be an output of some action. A requested payload is installed + // and this program runs again; anything else is reported. + if (!tool.pending()) + mcpp::deps::warn(mcpp::plugins::tool::describe_missing( + mcpp::deps::cmake_spec("mcpp.deps.archive"), tool) + + std::format("\n So this plan extracts nothing from {}.", + mcpp::deps::generic(archive))); r.files.clear(); return r; } diff --git a/deps/cmake.cppm b/deps/cmake.cppm index cc3467f..b96290f 100644 --- a/deps/cmake.cppm +++ b/deps/cmake.cppm @@ -86,8 +86,10 @@ struct options { // and whether the prefix's shared-library directory reaches `mcpp run` // and `mcpp pack`. bool shared = false; - // The `cmake` executable. Empty is the `xim:cmake` payload. - std::string cmake; + // The `cmake` executable (0.19.0: a `tool::choice`, so a path still + // assigns). Default: the `xim:cmake` payload, or an override of it. A + // choice made here is not downloaded: the payload is declared on request. + mcpp::plugins::tool::choice cmake; // Files of the prefix placed beside the program, as `mcpp.deps.vcpkg` // takes them (`{"bin/tool.cfg", "."}`). std::vector deploy; @@ -167,18 +169,8 @@ struct prefix { explicit operator bool() const { return !root.empty(); } }; -inline std::string cmake_exe(const options& opt) { - namespace fs = std::filesystem; - if (!opt.cmake.empty()) return mcpp::deps::generic(mcpp::deps::absolute_from_root(opt.cmake)); - const std::string dir = mcpp::xpkg_dir("xim", "cmake"); - if (dir.empty()) return {}; - const bool win = std::string(mcpp::host()).find("windows") != std::string::npos; - std::error_code ec; - for (auto const& sub : { fs::path("bin"), fs::path("CMake.app") / "Contents" / "bin" }) { - const auto exe = fs::path(dir) / sub / (win ? "cmake.exe" : "cmake"); - if (fs::is_regular_file(exe, ec)) return mcpp::deps::generic(exe); - } - return {}; +inline mcpp::plugins::tool::found cmake_exe(const options& opt) { + return mcpp::plugins::tool::resolve(mcpp::deps::cmake_spec("mcpp.deps.cmake"), opt.cmake); } namespace detail { @@ -420,12 +412,14 @@ inline prefix use(const options& opt) { p.mechanism = std::string(ts::name(named ? ts::mechanism::chain : instance ? ts::mechanism::instance : ts::mechanism::detected)); - const std::string cmake = cmake_exe(opt); - if (cmake.empty()) { - mcpp::deps::warn(std::format( - "{}: no cmake (xpkg_dir(\"xim\", \"cmake\") answered \"{}\"), so this plan builds " - "nothing. The `deps-cmake` feature declares `xim:cmake`; `mcpp build` provisions it " - "before this program runs.", who, std::string(mcpp::xpkg_dir("xim", "cmake")))); + const auto tool = cmake_exe(opt); + const std::string cmake = tool.program; + if (tool.pending()) { + // Asked for: the engine installs `xim:cmake` and runs this program + // again, with it. Nothing is planned in this run. + } else if (cmake.empty()) { + mcpp::deps::warn(mcpp::plugins::tool::describe_missing( + mcpp::deps::cmake_spec("mcpp.deps.cmake"), tool) + "\n So this plan builds nothing."); } else { // ONE ACTION, THREE STEPS. `cmake -P` runs a script this program // writes: configure (every time -- over an existing cache CMake re-runs diff --git a/deps/deps.cppm b/deps/deps.cppm index e4cfa09..7a55971 100644 --- a/deps/deps.cppm +++ b/deps/deps.cppm @@ -37,9 +37,19 @@ import mcpp.plugins; // The 0.16.0 names of the helpers that moved to `mcpp.plugins.fs` and // `mcpp.plugins.toolset` (a compatibility unit, until 2027-03-28). export import mcpp.deps.compat; +// The one resolver of a member's tools (0.19.0, mcpp#755). +export import mcpp.plugins.tool; export namespace mcpp::deps { +// THE `cmake` deps-cmake AND deps-archive RUN (0.19.0, mcpp#755): the payload +// both features declare, in its two layouts (`bin/` and, on macOS, +// `CMake.app/Contents/bin`), found by the one resolver every member uses. +inline mcpp::plugins::tool::spec cmake_spec(std::string who) { + return { .who = std::move(who), .package = "cmake", .programs = {"cmake"}, + .bin_dirs = {"bin", "CMake.app/Contents/bin"}, .option = "options::cmake" }; +} + // Prints `message` and records it as a `mcpp::warning`, folded onto one line. // The engine discards a build program's output when it exits 0, so a note that // only went to stderr would be invisible on exactly the builds that succeed. diff --git a/deps/vcpkg.cppm b/deps/vcpkg.cppm index e5ac791..7e3f1eb 100644 --- a/deps/vcpkg.cppm +++ b/deps/vcpkg.cppm @@ -78,6 +78,11 @@ struct options { std::vector install_args; // The vcpkg root. Empty is the `xim:vcpkg` payload this feature declares. std::string vcpkg_root; + // The vcpkg tool (0.19.0, mcpp#755): a program, a root, or the default -- + // the `xim:vcpkg` payload, or an override of it. `vcpkg_root` above is + // the same statement as `tool::root(...)`, kept for the projects that + // write it. A tool named here is not downloaded. + mcpp::plugins::tool::choice vcpkg; // Files of the prefix the program reads at run time, placed beside it // (`{"share/opencc/t2s.json", "BaseConfig/opencc"}`): `mcpp run` finds them // and `mcpp pack` carries them. See `mcpp::plugins::fs::deploy_after`. @@ -338,10 +343,18 @@ inline prefix use(const options& opt = {}) { } // ── the tool ── - const std::string vcpkgRoot = opt.vcpkg_root.empty() - ? std::string(mcpp::xpkg_dir("xim", "vcpkg")) : mcpp::deps::generic(mcpp::deps::absolute_from_root(opt.vcpkg_root)); - const fs::path exe = vcpkgRoot.empty() ? fs::path() - : fs::path(vcpkgRoot) / (ts::host_is_windows() ? "vcpkg.exe" : "vcpkg"); + // The tool and the scripts it was released with live in one root: the + // program's directory. + const mcpp::plugins::tool::spec vcpkgSpec{ + .who = "mcpp.deps.vcpkg", .package = "vcpkg", .programs = {"vcpkg"}, + .bin_dirs = {""}, .option = "options::vcpkg" }; + mcpp::plugins::tool::choice vcChoice = opt.vcpkg; + if (vcChoice.is_default() && !opt.vcpkg_root.empty()) + vcChoice = mcpp::plugins::tool::root(opt.vcpkg_root, opt.vcpkg.where); + const auto vcTool = mcpp::plugins::tool::resolve(vcpkgSpec, vcChoice); + const std::string vcpkgRoot = vcTool.program.empty() ? vcTool.root + : mcpp::deps::generic(fs::path(vcTool.program).parent_path()); + const fs::path exe = vcTool.program.empty() ? fs::path() : fs::path(vcTool.program); // ── the overlays: the manifest's own, then the project's extras ── std::vector overlayTriplets = manifest_overlays(manifestRoot, "overlay-triplets"); @@ -494,11 +507,12 @@ inline prefix use(const options& opt = {}) { const fs::path manifestFile = manifestRoot / "vcpkg.json"; const fs::path configFile = manifestRoot / "vcpkg-configuration.json"; mcpp::rerun_if_changed(mcpp::deps::generic(configFile).c_str()); - if (exe.empty() || !fs::is_regular_file(exe, ec)) { - mcpp::deps::warn(std::format( - "{}: the vcpkg tool is not installed (xpkg_dir(\"xim\", \"vcpkg\") answered \"{}\"), " - "so this plan installs nothing. The `deps-vcpkg` feature declares `xim:vcpkg`; " - "`mcpp build` provisions it before this program runs.", who, vcpkgRoot)); + if (vcTool.pending()) { + // Asked for: the engine installs `xim:vcpkg` and runs this program + // again, with it. Nothing is planned in this run. + } else if (exe.empty() || !fs::is_regular_file(exe, ec)) { + mcpp::deps::warn(mcpp::plugins::tool::describe_missing(vcpkgSpec, vcTool) + + "\n So this plan installs nothing."); } else { // THE ACTION IS vcpkg ITSELF. Everything an installation needs is an // argument: `--vcpkg-root` pairs the tool with the scripts it was diff --git a/dist/apk.cppm b/dist/apk.cppm index ce359ad..3650aa2 100644 --- a/dist/apk.cppm +++ b/dist/apk.cppm @@ -107,6 +107,7 @@ export module mcpp.dist.apk; import std; import mcpp; import mcpp.plugins; +import mcpp.plugins.tool; // `std::format` is header-only and used throughout; `std::println` is not -- // see `rules/spirv.cppm` for the libc++-on-macOS-14 measurement that this @@ -203,6 +204,12 @@ struct options { // `keystore_alias` and `keystore_password_env` are then both required, // because a private key has no convention this member may assume. std::string keystore; + // THE PAYLOADS THIS MEMBER RUNS (0.19.0, mcpp#755): each a root holding + // that part of the Android pipeline, or the default -- the payload this + // member declares, or an override of it. One named here is not downloaded. + // `platform` is a root carrying `android.jar`; `keystore_package` above + // stays the way a keystore package is named. + mcpp::plugins::tool::choice build_tools, platform, jdk, bundletool_dir, kotlin, coursier; std::string keystore_alias; // The NAME of an environment variable `apksigner` itself reads // (`--ks-pass env:`) -- this member never reads the secret; only @@ -1228,7 +1235,30 @@ inline bool unpack_aar(const std::string& aar, const fs::path& dest, const std:: // reports only "no action claimed --format 'apk'" (measured on 0.9.3 with two // triples). The warning channel is one line per directive, so line breaks in // the message are folded into spaces. +// THE ROOT OF ONE OF THIS MEMBER'S PAYLOADS (0.19.0, mcpp#755): what the +// build program named, an override, or the payload. `pending` is set when the +// payload was asked for, and the engine then installs it and runs this program +// again -- the member returns without planning. +inline std::string payload_root(std::string_view package, + const mcpp::plugins::tool::choice& named, + bool& pending) { + pending = false; + mcpp::plugins::tool::spec s{ .who = "mcpp.dist.apk", .package = std::string(package), + .programs = {}, .option = "options::" + std::string(package) }; + if (!named.is_default()) return mcpp::plugins::tool::resolve(s, named).root; + if (std::string_view(mcpp::xpkg_source("xim", s.package.c_str())) == "pending") { + (void)mcpp::xpkg_request("xim", s.package.c_str()); + pending = true; + return {}; + } + auto f = mcpp::plugins::tool::resolve(s); + return f.root; +} + inline plan& refuse(plan& p, std::string reason, const std::string& message) { + // A reason with no message: the payload was asked for and the engine runs + // this program again (0.19.0), so there is nothing to tell anyone yet. + if (message.empty()) { p.reason = std::move(reason); return p; } std::cerr << message << '\n'; std::string folded; folded.reserve(message.size()); @@ -1530,14 +1560,17 @@ inline plan plan_for(options opt = {}) { } // ── the payloads this member declared ────────────────────────────── - const std::string buildTools = mcpp::xpkg_dir("xim", "android-build-tools"); + bool payloadPending = false; + const std::string buildTools = payload_root("android-build-tools", opt.build_tools, payloadPending); + if (payloadPending) return refuse(p, "android-build-tools requested", {}); if (buildTools.empty()) { return refuse(p, "android-build-tools not found", "mcpp.dist.apk: xim:android-build-tools was not found. Declare it " "under [target.'cfg(env = \"android\")'.feature-xlings.dist-apk] in " "the consuming project, or install it directly."); } - const std::string platformDir = mcpp::xpkg_dir("xim", "android-platform"); + const std::string platformDir = payload_root("android-platform", opt.platform, payloadPending); + if (payloadPending) return refuse(p, "android-platform requested", {}); if (platformDir.empty()) { return refuse(p, "android-platform not found", "mcpp.dist.apk: xim:android-platform was not found (declare it under " @@ -1589,7 +1622,8 @@ inline plan plan_for(options opt = {}) { // android-build-tools' runtime dependency, which provisions the JDK for // ITS OWN wrappers and does not make it visible to a consumer's build // program (docs/31, "declare the tool where it will be looked up"). - const std::string jdkHome = mcpp::xpkg_dir("xim", "jdk-temurin"); + const std::string jdkHome = payload_root("jdk-temurin", opt.jdk, payloadPending); + if (payloadPending) return refuse(p, "jdk-temurin requested", {}); if (jdkHome.empty()) { return refuse(p, "jdk-temurin not found", "mcpp.dist.apk: xim:jdk-temurin was not found (declare it under " @@ -1611,7 +1645,8 @@ inline plan plan_for(options opt = {}) { // writes runs the JDK it was installed against. std::string bundletool; if (bundle) { - const std::string btDir = mcpp::xpkg_dir("xim", "bundletool"); + const std::string btDir = payload_root("bundletool", opt.bundletool_dir, payloadPending); + if (payloadPending) return refuse(p, "bundletool requested", {}); bundletool = btDir.empty() ? std::string() : (fs::path(btDir) / "bin" / "bundletool").string(); if (!is_file(bundletool)) { @@ -1716,7 +1751,8 @@ inline plan plan_for(options opt = {}) { "names into {}.", mode, opt.maven_lock, cacheRoot)); } const auto coursier = [&]() -> std::string { - const std::string dir = mcpp::xpkg_dir("xim", "coursier"); + bool pend = false; + const std::string dir = payload_root("coursier", opt.coursier, pend); return dir.empty() ? std::string() : (fs::path(dir) / "bin" / "cs").string(); }; const auto no_coursier = [&](plan& pl) -> plan& { @@ -1888,7 +1924,8 @@ inline plan plan_for(options opt = {}) { } std::string kotlinc, kotlinStdlib; if (needsKotlin) { - const std::string kdir = mcpp::xpkg_dir("xim", "kotlin"); + const std::string kdir = payload_root("kotlin", opt.kotlin, payloadPending); + if (payloadPending) return refuse(p, "kotlin requested", {}); if (!kdir.empty()) { kotlinc = (fs::path(kdir) / "bin" / "kotlinc").string(); kotlinStdlib = (fs::path(kdir) / "kotlinc" / "lib" / "kotlin-stdlib.jar").string(); @@ -1911,7 +1948,9 @@ inline plan plan_for(options opt = {}) { if (!opt.sign) { // Unsigned: no keystore is resolved, so none has to be declared. } else if (opt.keystore.empty()) { - const std::string ksDir = mcpp::xpkg_dir("xim", "android-debug-keystore"); + const std::string ksDir = payload_root("android-debug-keystore", + mcpp::plugins::tool::choice{}, payloadPending); + if (payloadPending) return refuse(p, "android-debug-keystore requested", {}); if (ksDir.empty()) { return refuse(p, "android-debug-keystore not found", "mcpp.dist.apk: xim:android-debug-keystore was not found (declare " diff --git a/dist/appimage.cppm b/dist/appimage.cppm index 8de79e7..782ce8c 100644 --- a/dist/appimage.cppm +++ b/dist/appimage.cppm @@ -44,6 +44,7 @@ export module mcpp.dist.appimage; import std; import mcpp; import mcpp.plugins; +import mcpp.plugins.tool; // Nothing here uses `std::println`, and that is not a style choice: both of its // overloads reach into the libc++ dylib for symbols macOS 14 does not ship, so @@ -90,9 +91,12 @@ struct options { // tries. std::string icon; - // An explicit `appimagetool` wins over discovery. Set it to pin a build - // other than the one the workspace installed. - std::string tool; + // `appimagetool` (0.19.0: a `tool::choice`, so a path still assigns). + // Default: the `xim:appimagetool` payload this feature declares, or an + // override of it. PATH is not consulted by default -- a host tool would + // make the produced AppImage depend on a machine rather than on a + // declaration -- and `mcpp::plugins::tool::on_path()` asks for it. + mcpp::plugins::tool::choice tool; // The type-2 runtime stub the produced AppImage starts with. // // IT IS DECLARED BECAUSE THE TOOL WOULD OTHERWISE FETCH IT. Measured on @@ -213,20 +217,15 @@ inline std::string payload_dir_of(const std::string& tool) { return std::filesystem::path(tool).parent_path().string(); } -inline std::string discover_tool(const options& opt) { - if (!opt.tool.empty()) return opt.tool; - // THE PAYLOAD THIS MEMBER DECLARED, and nothing else. - // - // `xpkg_dir` answers from `MCPP_XPKG_*_DIR`, which mcpp sets for the - // package being built -- so a member that runs a payload tool declares the - // payload itself rather than relying on a consumer's manifest. There is - // deliberately no PATH fallback: a host `appimagetool` would make the - // produced AppImage depend on a machine rather than on a declaration, and - // an AppImage is the artifact for which that matters most. - const std::string dir = mcpp::xpkg_dir("xim", "appimagetool"); - if (dir.empty()) return {}; - const std::string exe = (std::filesystem::path(dir) / "appimagetool").string(); - return is_file(exe) ? exe : std::string(); +inline const mcpp::plugins::tool::spec& appimagetool_spec() { + static const mcpp::plugins::tool::spec s{ + .who = "mcpp.dist.appimage", .package = "appimagetool", + .programs = {"appimagetool"}, .bin_dirs = {"", "bin"}, .option = "options::tool" }; + return s; +} + +inline mcpp::plugins::tool::found discover_tool(const options& opt) { + return mcpp::plugins::tool::resolve(appimagetool_spec(), opt.tool); } inline std::string discover_runtime(const options& opt, const std::string& tool) { @@ -326,19 +325,16 @@ inline plan plan_for(options opt = {}) { return p; } - const std::string tool = discover_tool(opt); + const auto toolFound = discover_tool(opt); + const std::string tool = toolFound.program; if (tool.empty()) { - // NAMES WHERE IT LOOKED, not a manifest the reader does not own. The - // payload is declared by this package's own feature, so a consumer who - // sees this has an installation problem rather than a declaration to - // add. - std::cerr << std::format( - "mcpp.dist.appimage: appimagetool was not found.\n" - " looked for: {}/appimagetool (the `xim:appimagetool` payload " - "this feature declares)\n" - " set `options::tool` to name one explicitly.", - mcpp::xpkg_dir("xim", "appimagetool")) << '\n'; - p.reason = "appimagetool not found"; + // NAMES WHAT WAS CONSULTED, not a manifest the reader does not own. + // The payload is declared by this package's own feature, so a consumer + // who sees this has an installation problem rather than a declaration + // to add. + if (!toolFound.pending()) + std::cerr << mcpp::plugins::tool::describe_missing(appimagetool_spec(), toolFound) << '\n'; + p.reason = toolFound.pending() ? "appimagetool requested" : "appimagetool not found"; return p; } const std::string runtime = discover_runtime(opt, tool); diff --git a/dist/wix.cppm b/dist/wix.cppm index ccbbeb9..bc9f5dc 100644 --- a/dist/wix.cppm +++ b/dist/wix.cppm @@ -101,6 +101,7 @@ export module mcpp.dist.wix; import std; import mcpp; import mcpp.plugins; +import mcpp.plugins.tool; // Nothing here uses `std::println`, and that is not a style choice: both of // its overloads reach into the libc++ dylib for symbols macOS 14 does not @@ -169,9 +170,11 @@ struct options { // ``. std::string wxs; - // An explicit `wix` wins over the declared payload. Set it to pin a build - // other than `xim:wix`, for instance one the project compiled itself. - std::string tool; + // The `wix` CLI (0.19.0: a `tool::choice`, so a path still assigns). + // Default: the `xim:wix` payload this feature declares, or an override of + // it. PATH is not consulted by default, for the reason the header gives; + // `mcpp::plugins::tool::on_path()` asks for it. + mcpp::plugins::tool::choice tool; // Where the produced file lands. Empty means // `/-.msi`. @@ -266,6 +269,9 @@ inline bool write_if_different(const std::filesystem::path& path, // --format 'msi'", names no reason. The warning channel is one line per // directive, so line breaks are folded into spaces. inline plan& refuse(plan& p, std::string reason, const std::string& message) { + // A reason with no message: the tool was asked for and the engine runs + // this program again (0.19.0), so there is nothing to tell anyone yet. + if (message.empty()) { p.reason = std::move(reason); return p; } std::cerr << message << '\n'; std::string folded; folded.reserve(message.size()); @@ -484,9 +490,17 @@ inline std::string wix_payload_exe() { return is_file(exe.string()) ? exe.string() : std::string(); } -inline std::string discover_tool(const options& opt) { - if (!opt.tool.empty()) return opt.tool; - return wix_payload_exe(); +// The payload keeps its tool at `tool/tools/net6.0/any/wix.exe`, so the +// resolver is given that directory under the root. +inline const mcpp::plugins::tool::spec& wix_spec() { + static const mcpp::plugins::tool::spec s{ + .who = "mcpp.dist.wix", .package = "wix", .programs = {"wix"}, + .bin_dirs = {"tool/tools/net6.0/any"}, .option = "options::tool" }; + return s; +} + +inline mcpp::plugins::tool::found discover_tool(const options& opt) { + return mcpp::plugins::tool::resolve(wix_spec(), opt.tool); } // The stock bootstrapper application's extension: what the project named, else @@ -695,18 +709,13 @@ inline plan plan_for(options opt = {}) { "`options::target` to the program target's name."); } - const std::string tool = discover_tool(opt); + const auto toolFound = discover_tool(opt); + const std::string tool = toolFound.program; if (tool.empty()) { - return refuse(p, "wix not found", std::format( - "mcpp.dist.wix: the wix CLI was not found.\n" - " xpkg_dir(\"xim\", \"wix\") answered \"{}\"; the payload's tool is " - "tool/tools/net6.0/any/wix.exe beneath it.\n" - " `xim:wix` is declared by this feature under " - "[target.windows.feature-xlings.dist-wix], so an empty answer means " - "the payload is not installed for this build (mcpp provisions it " - "when the feature is active on a Windows target)\n" - " or set `options::tool` to name one explicitly.", - mcpp::xpkg_dir("xim", "wix"))); + if (toolFound.pending()) + return refuse(p, "wix requested", {}); + return refuse(p, "wix not found", + mcpp::plugins::tool::describe_missing(wix_spec(), toolFound)); } const std::string hostArch = mcpp::target_arch(); diff --git a/docs/deps.md b/docs/deps.md index 1ba40fb..d9fcd74 100644 --- a/docs/deps.md +++ b/docs/deps.md @@ -4,27 +4,27 @@ The `deps-*` members answer where a library comes from: a vcpkg manifest, a CMak ## `deps-vcpkg` -Module `mcpp.deps.vcpkg`; engine floor: 2026.9.28.3 (0.17.0, mcpp#734; 2026.9.26.2 before). +Module `mcpp.deps.vcpkg`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.28.3 before). -**Needs and behaviour.** `xim:vcpkg` (the tool and the scripts released with it), which this feature declares on the host axis. From 0.13.0. Installs a `vcpkg.json` manifest as a `prepare` action and maps `//` into the build by name, its shared-library directory a runtime search directory; `mcpp emit build-database` installs nothing. See [the section below](#deps-vcpkg-the-libraries-a-vcpkg-manifest-names) +**Needs and behaviour.** `xim:vcpkg` (the tool and the scripts released with it), which this feature declares on the host axis, installed when this member asks for it (`provision = "on-request"`, 0.19.0) -- a project that names its own vcpkg with `options::vcpkg` or `[xlings.overrides]` downloads none. From 0.13.0. Installs a `vcpkg.json` manifest as a `prepare` action and maps `//` into the build by name, its shared-library directory a runtime search directory; `mcpp emit build-database` installs nothing. See [the section below](#deps-vcpkg-the-libraries-a-vcpkg-manifest-names) ## `deps-cmake` -Module `mcpp.deps.cmake`; engine floor: 2026.9.28.3 (0.17.0, mcpp#734; 2026.9.26.2 before). +Module `mcpp.deps.cmake`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.28.3 before). -**Needs and behaviour.** `xim:cmake`, which this feature declares on the host axis. From 0.13.0. Configures, builds and installs a CMake subproject as one `prepare` action whose inputs are the subproject's files, with the Ninja generator and the toolset mcpp resolved, and maps the prefix as `deps-vcpkg` does; an installation is kept outside the package and taken by any later build with the same key (0.18.0) +**Needs and behaviour.** `xim:cmake`, which this feature declares on the host axis, installed when this member asks for it (`provision = "on-request"`, 0.19.0) -- a project that names its own cmake with `options::cmake` or `[xlings.overrides]` downloads none. From 0.13.0. Configures, builds and installs a CMake subproject as one `prepare` action whose inputs are the subproject's files, with the Ninja generator and the toolset mcpp resolved, and maps the prefix as `deps-vcpkg` does; an installation is kept outside the package and taken by any later build with the same key (0.18.0) ## `deps-archive` -Module `mcpp.deps.archive`; engine floor: 2026.9.26.2. +Module `mcpp.deps.archive`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.26.2 before). -**Needs and behaviour.** `xim:cmake`, which this feature declares on the host axis. From 0.14.0. Extracts a zip archive the project keeps and places its tree beside the program: one action names every member as an output, read from the archive's central directory while the build program runs, and each is deployed, so `mcpp run` finds the files and `mcpp pack` carries them. See [the section below](#deps-vcpkg-the-libraries-a-vcpkg-manifest-names) +**Needs and behaviour.** `xim:cmake`, which this feature declares on the host axis, installed when this member asks for it (`provision = "on-request"`, 0.19.0). From 0.14.0. Extracts a zip archive the project keeps and places its tree beside the program: one action names every member as an output, read from the archive's central directory while the build program runs, and each is deployed, so `mcpp run` finds the files and `mcpp pack` carries them. See [the section below](#deps-vcpkg-the-libraries-a-vcpkg-manifest-names) ## `deps-vcpkg`: the libraries a vcpkg manifest names ```toml [build-dependencies.mcpp] -plugins = { version = "0.18.1", features = ["deps-vcpkg"], host-module = true } +plugins = { version = "0.19.0", features = ["deps-vcpkg"], host-module = true } ``` ```cpp @@ -75,9 +75,17 @@ states every path it can, and the build is where the absence fails. | `install_root` | empty is vcpkg's default, `/vcpkg_installed`. Each triplet is its own vcpkg installation, `/`, because vcpkg's manifest mode removes from an installation the packages of every other triplet (0.15.0; 0.14.0's prefix `/` is no longer read) | | `overlay_triplets` | further overlay-triplet directories | | `install_args` | arguments appended to `vcpkg install` | -| `vcpkg_root` | a vcpkg root other than the `xim:vcpkg` payload | +| `vcpkg` | the vcpkg tool: a program, `tool::root(dir)`, or `tool::on_path()`; empty is the `xim:vcpkg` payload, or an override of it (0.19.0) | +| `vcpkg_root` | a vcpkg root other than the `xim:vcpkg` payload; the 0.18.1 spelling of `vcpkg = tool::root(...)`, still read | | `deploy` | files of the prefix the program reads at run time, each `{file, to}`: `file` relative to the prefix root, `to` the directory beside the program (`{"share/opencc/t2s.json", "BaseConfig/opencc"}`) | +**Where the tool comes from (0.19.0).** Each member resolves its tool through +`mcpp.plugins.tool`, in one order: the option above, then an override +(`[xlings.overrides]`, `MCPP_XLINGS_OVERRIDE__`, `config.toml`), then +the declared payload — which is installed only when the member asks for it, so a +project that names its own tool downloads none. The build reports the source it +used, and `mcpp why tool ` answers from the same record. + The payload `xim:vcpkg` is vcpkg-tool's release binary with the standalone bundle published beside it, so the scripts a port calls are the ones that tool was released with. A `builtin-baseline` manifest resolves through vcpkg's git @@ -215,6 +223,7 @@ takes it. | `toolset`, `crt_linkage` | as `deps-vcpkg` takes them (0.17.0) | | `generator` | `default` (and `ninja`, its 0.17.0 spelling): the Ninja generator with the toolset named, wherever the toolset is resolved; `visual_studio`: CMake's Visual Studio generator on the instance mcpp resolved, for a subproject that needs MSBuild (0.18.0) | | `cache` | where installations are kept: a directory, `"off"`, or empty for `MCPP_DEPS_CMAKE_CACHE` and then the user's cache directory (0.18.0) | +| `cmake` | the `cmake` program: a path (as before), `tool::root(dir)` or `tool::on_path()`; empty is the `xim:cmake` payload, or an override of it. A tool named here is not downloaded (0.19.0) | ### The toolset and the generator diff --git a/docs/dist-apk.md b/docs/dist-apk.md index e738663..d9ba284 100644 --- a/docs/dist-apk.md +++ b/docs/dist-apk.md @@ -4,7 +4,7 @@ ## `dist-apk` -Module `mcpp.dist.apk`; engine floor: 2026.9.14.2 from 0.10.0, which stages the closure this member reads; before it 2026.9.13.1, raised alongside `dist-web` in the same 0.9.0 release: this member's own manifest-template and Java-array changes ask nothing new of the engine, but this collection publishes one package at one version, and this is the release CI verifies it under from here on. +Module `mcpp.dist.apk`; engine floor: 2026.10.1.3 from 0.19.0 (mcpp#755), whose protocol 15 states where a tool comes from and installs `xim:bundletool` only when a bundle asks for it; 2026.9.14.2 from 0.10.0, which stages the closure this member reads; before it 2026.9.13.1, raised alongside `dist-web` in the same 0.9.0 release: this member's own manifest-template and Java-array changes ask nothing new of the engine, but this collection publishes one package at one version, and this is the release CI verifies it under from here on. **Needs and behaviour.** `xim:android-build-tools`, `xim:android-platform` (versioned by API level, read back for `targetSdkVersion`), `xim:jdk-temurin` (`javac`/`jar`/`jarsigner`; `android-build-tools`' own runtime dependency provisions a JDK for its OWN wrappers only), `xim:android-debug-keystore`, `xim:bundletool` (0.10.0, for `--format aab`), all on the `cfg(env = "android")` axis. Generates `AndroidManifest.xml` and signs with the published Android debug key by default. Level 0 needs no Java (`hasCode="false"`, `android.app.NativeActivity`); `options::java_sources` adds `javac` + `d8` and a real ``. `options::manifest_template` renders a project manifest with six tokens substituted verbatim; `{{application_id}}` and `{{activity}}` are required always and `{{lib_name}}` at level 0, each refused by name at plan time when missing (naming `assets/mcpp-run.json`, which `adb-run` reads them from too) or when the template names an unknown token; empty renders 0.8.0's manifest byte-identically. From 0.9.3 the manifest template gains `{{version_name}}` / `{{version_code}}` (the package version, and `major * 1000000 + minor * 1000 + patch`). `options::resources` is a project's own `res/`, linked as the application's base resources from 0.9.1 (0.9.0 linked it as an aapt2 overlay, which refuses every resource the base does not already define -- a launcher icon could not be supplied). `options::java_sources` is an array: one `javac` over every root's `.java` files and one `d8` over the result, so a project's own sources and a path dependency's join without being merged into one directory first, and `rerun_if_changed_glob` is declared only for a root under `mcpp::manifest_dir()` -- a dependency root's files are already inputs of the `javac` action and its version is already in the build's fingerprint. Android only -- an `app` target is a shared object on this row (#622 A3). From 0.10.0, with mcpp 2026.9.14.2, the member reads the native closure the engine stages -- the application object, the graph's shared libraries and `libc++_shared.so` under `lib/` for one `--target`, `lib//` for several -- and packs every ABI the tree carries into one APK; its own `NEEDED` walk and the stamp that lost a dependency's library on a second pack are gone. A stage manifest without `needs` lines comes from an engine below 2026.9.14.2 and is refused naming that floor, as is an incomplete closure, naming the unresolved libraries; every refusal is also a `mcpp::warning`, because the engine discards a build program's output when it exits 0. `--format aab` shares every step but the last three: `aapt2 link --proto-format` (with `--version-code` / `--version-name`, which bundletool requires), a base module in the layout bundletool reads, `bundletool build-bundle` from `xim:bundletool` (declared on the same axis), and `jarsigner` with the same keystore. CI packages both level 0 and level 1 and checks the archive (`mcpp::deploy`'d files under `assets/`), and on `tests/apk-consumer-shared` two packs in a row, a two-ABI APK (`aapt2` reports both, `apksigner` verifies it), an App Bundle (`bundletool validate`, `jarsigner -verify`, a universal APK from `bundletool build-apks`), a refusal's reason in `mcpp pack`'s output, and the floor refusal; the runner has no emulator or device, so the two rows that actually run were measured locally on 2026-09-12, through `adb-run`, against a KVM-accelerated x86_64 emulator and a physical arm64-v8a phone, both printing `1-2-3` and exiting 0. From 0.11.0: Kotlin sources (`dist-apk-kotlin`, which declares `xim:kotlin`), R classes, Android libraries from source, local AARs and JARs, a Maven graph through a lock file (`dist-apk-maven`, which declares `xim:coursier`), a manifest merge, and an unsigned package -- see [`dist-apk`: Kotlin, libraries and a Maven graph](#dist-apk-kotlin-libraries-and-a-maven-graph). From 0.11.1 the native libraries are packed as the Android Gradle plugin packs them: each is stripped with the build's own `llvm-strip --strip-unneeded` (the NDK's, beside the compiler `mcpp::toolchain_dir()` reports; `options::keep_debug_symbols` packs them as staged), and a manifest stating `android:extractNativeLibs="false"` gets them stored uncompressed on a 16 KB page, which loading them from the APK in place requires; CI checks both, and the symbol table `keep_debug_symbols` keeps. From 0.12.0 the engine's strip decision governs these libraries as well: under mcpp 2026.9.16.1, which publishes it to build programs as `MCPP_PACK_STRIP` and `MCPP_PACK_DEBUG_SYMBOLS_DIR`, that engine strips and separates the libraries it stages itself and the member packs them as staged, while the libraries the engine did not stage -- an archive's `jni/` libraries -- follow the same decision inside the member: `--no-strip` packs them as they are, and `--debug-symbols ` separates each one's debug information into `//.debug` with `llvm-objcopy` before stripping it and linking the packed copy to that file (`.gnu_debuglink`). A library is stripped unless either `keep_debug_symbols` or the engine says to keep it, and under an older engine, which publishes neither variable, the member strips every library as before. Because that engine strips before the member reads the tree, `keep_debug_symbols` keeps only the member's own strip off and `--no-strip` is what ships the symbols of the libraries the graph built; the member warns when the option is set and the engine stripped. From 0.12.0 a library in the resolved graph contributes libraries and archives through `[package.metadata.dist-apk]`, ranked below the application's own -- see [A library states its contribution](#a-library-states-its-contribution-packagemetadatadist-apk) diff --git a/docs/dist.md b/docs/dist.md index 893fa4f..21da78f 100644 --- a/docs/dist.md +++ b/docs/dist.md @@ -4,13 +4,13 @@ Three `dist-*` members that turn the tree `mcpp pack` stages into an installable ## `dist-appimage` -Module `mcpp.dist.appimage`; engine floor: 2026.9.11.1. +Module `mcpp.dist.appimage`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.11.1 before). Its `xim:appimagetool` is installed when a pack asks for it (`provision = "on-request"`), so an ordinary Linux build downloads none. **Needs and behaviour.** `xim:appimagetool`, which this feature declares on the `cfg(linux)` axis. Linux only. Turns the tree `mcpp pack` staged into one AppImage: the staged bundle is already an AppDir bar three files, so the member writes an `AppRun`, a `.desktop` entry and an icon into it and invokes one tool -- it never copies or re-lays-out a tree that can be hundreds of megabytes. From 0.11.1 `options::icon` may be an SVG as well as a PNG: the image carries `.svg`, because the desktop entry names the icon without an extension and a reader finds it by the file's, and any other format is refused by name ## `dist-wix` -Module `mcpp.dist.wix`; engine floor: 2026.9.11.1. +Module `mcpp.dist.wix`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.11.1 before). **Needs and behaviour.** `xim:wix` 5.0.2-1, which this feature declares on the Windows target axis; the .NET 6 runtime the tool needs is a Windows component the payload does not carry, and `wix --version` names it when it is missing. Windows only. `--format msi` renders a `.wxs` and passes the program in as a preprocessor variable, because a bind path that resolves to nothing is silent. From 0.10.0 `--format setup` is a Burn bundle chaining that MSI, with WiX's stock bootstrapper application (`bal:WixStandardBootstrapperApplication`, theme `hyperlinkLicense`, `options::license_url` its link) loaded through the `WixToolset.BootstrapperApplications.wixext` extension the 5.0.2-1 payload carries (`options::extension` names another). The bundle is written as `-.exe` beside the MSI, with an UpgradeCode of its own, and `options::bundle_output` naming `setup.exe` is refused before `wix` runs, because `wix` refuses that name (WIX0388). A project with its own bootstrapper application supplies `options::bundle_wxs`, which receives the MSI as `$(Msi)`. From 0.10.1 the MSI installs the staged tree, not the program alone: every file `mcpp pack` stages beside the program (a deployed file, a resolved DLL) is named file by file in a `StagedFiles` component group written beside the definition -- enumerated while the tree exists, never harvested from a directory -- placed at its staged path under `INSTALLFOLDER`, and declared an input of the action; the generated definition references the group, and a project's own `options::wxs` installs it with ``. `options::inputs` and `options::bundle_inputs` declare the files a project's own definitions name beyond that (an icon, a bootstrapper application and its payloads). CI builds the bundle on `windows-2022` and compares the MSI `wix burn extract` takes out of it with the one the first action wrote, and installs the MSI administratively and compares both the program and a deployed file diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 9860d6c..294f0c4 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -34,7 +34,7 @@ mcpp is a general build engine with a framework for build plugins. Plugins come | layer | modules | provided by | |---|---|---| | L1 `mcpp.core` | `mcpp.core`, spelled `mcpp` as well (the two are permanently equivalent) | the engine | -| L2 general library | `mcpp.plugins.declare`, `mcpp.plugins.toolset`, `mcpp.plugins.fs`; `mcpp.plugins.testing` | this package, feature `plugins-core` (`plugins-testing` for the kit) | +| L2 general library | `mcpp.plugins.declare`, `mcpp.plugins.toolset`, `mcpp.plugins.fs`, `mcpp.plugins.tool`; `mcpp.plugins.testing`, `mcpp.plugins.toolchain` | this package, feature `plugins-core` (`plugins-testing` for the kit, `plugins-toolchain` for the toolchain builders) | | L3 plugins | `mcpp.deps.*`, `mcpp.rules.*`, `mcpp.dist.*`, `mcpp.tools.*` here; `mcpp..*` elsewhere | this package, by feature; any package | A layer depends only on the layers below it. L2 knows no foreign tool: it turns @@ -52,7 +52,7 @@ mcpp-index refuses such a package. ```toml [build-dependencies.mcpp] -plugins = { version = "0.18.1", features = ["plugins-core"], host-module = true, reexport = true } +plugins = { version = "0.19.0", features = ["plugins-core"], host-module = true, reexport = true } ``` `reexport = true` is needed when the plugin's consumers import L2 modules in @@ -60,13 +60,56 @@ their own build programs. **The engine floor.** A plugin states the first mcpp release it needs in `[package] mcpp = ">="`; an older engine stops before any other work -and names the upgrade. This package's floor is 2026.9.28.3, the release that -states the build information L2 reads. +and names the upgrade. This package's floor is 2026.10.1.3, the release that +states where each tool and payload comes from (protocol 15). + +**Where a member's tool comes from.** A member that runs a program resolves it +through `mcpp.plugins.tool`, which answers in one order for every member: + +1. the build program's own choice — `o.cmake = "/usr/bin/cmake"`, or + `tool::root(dir)`, `tool::on_path()`; +2. the variable that member has always read (`MCPP_SLANGC`), kept for the + projects that write it; +3. the engine's override (`[xlings.overrides]`, `MCPP_XLINGS_OVERRIDE_*`, + `config.toml`) — the payload is then not installed at all; +4. the declared payload, asked for here when it is declared + `provision = "on-request"`. + +A member writes one `tool::spec` per tool (the package, the program names, the +directories under a root, the option's name, a legacy variable) and calls +`resolve`: + +```cpp +inline const mcpp::plugins::tool::spec& cmake_spec() { /* … */ } + +auto t = mcpp::plugins::tool::resolve(cmake_spec(), opt.cmake); +if (t.pending()) return true; // asked for; the engine runs this program again +if (!t) { mcpp::deps::warn(mcpp::plugins::tool::describe_missing(cmake_spec(), t)); return false; } +run(t.program); +``` + +A choice **never asks for the payload**, which is what makes "the build program +names its own tool" mean "the payload is not downloaded". `resolve` records the +answer with `mcpp::decision`, so the build reports the source and +`mcpp why tool ` can answer. `describe_missing` is the one refusal text, +listing every way to name the tool. + +An option field is a `tool::choice`, which a string constructs, so +`o.cmake = "/usr/bin/cmake"` keeps working and the line that wrote it is what a +build reports. + +**Stating the build toolchain.** `mcpp.plugins.toolchain` (feature +`plugins-toolchain`) builds the statement a root build program makes in its +toolchain phase, for a project whose `[toolchain]` says +`configure = "build.mcpp"`: `layout`, `prefixed`, `compose`, +`from_env_script`, `with_launcher`, `with_sysroot`, `with_tool`, `managed`, +`env`, and `configure(fn)` / `use(d)`. **Testing a plugin.** `mcpp.plugins.testing` runs a plugin function in a child process against a stated build context (`row::windows_visual_studio()`, `row::windows_managed()`, `row::linux_gcc()`, `row::linux_libcxx()`, or a -context built with `set` and `file`) and hands the lines it emitted and the +context built with `set`, `file`, `xpkg`, `xpkg_source`, `xpkg_program` and +`phase`) and hands the lines it emitted and the files it wrote to a check. The test is a build program; a failed case fails the build and prints the report. `tests/plugin-logic` is this package's own use. diff --git a/docs/rules-qt.md b/docs/rules-qt.md index 72ea5d1..e95ab0e 100644 --- a/docs/rules-qt.md +++ b/docs/rules-qt.md @@ -1,12 +1,12 @@ # `rules-qt` -`mcpp.rules.qt` runs Qt's code generators (`moc`, `uic`, `rcc`) and Linguist tools (`lupdate`, `lrelease`, `lconvert`) as build actions, links the Qt modules and places their runtime beside the program. The rule declares no SDK and pins no version: the project names the Qt it builds with, in `build.mcpp` or in its own `[xlings]` table. Module `mcpp.rules.qt`; engine floor 2026.9.27.1 (mcpp#704, mcpp#715) from 0.16.0, 2026.9.26.2 (mcpp#702) before; from 0.13.0. +`mcpp.rules.qt` runs Qt's code generators (`moc`, `uic`, `rcc`) and Linguist tools (`lupdate`, `lrelease`, `lconvert`) as build actions, links the Qt modules and places their runtime beside the program. The rule declares no SDK and pins no version: the project names the Qt it builds with, in `build.mcpp` or in its own `[xlings]` table. Module `mcpp.rules.qt`; engine floor 2026.10.1.3 (mcpp#755) from 0.19.0, 2026.9.27.1 (mcpp#704, mcpp#715) from 0.16.0, 2026.9.26.2 (mcpp#702) before; from 0.13.0. ## Use ```toml [build-dependencies.mcpp] -plugins = { version = "0.18.1", features = ["rules-qt"], host-module = true } +plugins = { version = "0.19.0", features = ["rules-qt"], host-module = true } # The SDK and its version are the project's declaration. [target.'cfg(any(windows, linux, macos))'.xlings.workspace] diff --git a/docs/rules.md b/docs/rules.md index de5d6b8..d8b5517 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -4,19 +4,19 @@ The rules that compile one kind of translation unit with a compiler mcpp does no ## `rules-ascendc` -Module `mcpp.rules.ascendc`; engine floor: 2026.9.6.6. +Module `mcpp.rules.ascendc`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.6.6 before). **Needs and behaviour.** `[build] accel = "ascend8.5+{dav-c220}"`, a constrained glob for `*.asc`. Compiles with BiSheng in MIXED mode, so the object carries the device binary and a host-callable launcher and joins the ordinary link -- no registration file and no device-link step. Its own engine needs are `.asc` in the device-source table and `mcpp::link_flag` for the `-rpath-link` the toolkit's shared libraries require, both 2026.9.6.5 ## `rules-cuda` -Module `mcpp.rules.cuda`; engine floor: 2026.9.6.6. +Module `mcpp.rules.cuda`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.6.6 before). **Needs and behaviour.** `[build] accel = "cuda…"`, a constrained glob for `*.cu`; the clang route with an LLVM toolchain, the nvcc route with a GCC one ## `rules-hip` -Module `mcpp.rules.hip`; engine floor: 2026.9.6.6. +Module `mcpp.rules.hip`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.6.6 before). **Needs and behaviour.** `[build] accel = "hip, cuda12.9+{sm_89}"`, a constrained glob for `*.hip`. On the NVIDIA platform HIP is a header layer over the CUDA runtime, so the compiler is the project's own clang and there is no ROCm on the machine @@ -28,13 +28,13 @@ Module `mcpp.rules.metal`; engine floor: 2026.9.8.1. ## `rules-slang` -Module `mcpp.rules.slang`; engine floor: 2026.9.7.1. +Module `mcpp.rules.slang`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.7.1 before). **Needs and behaviour.** `[build] accel = "vulkan1.2"`, a constrained glob for `*.slang`. Slang is a different language from GLSL rather than a second driver for it -- its own module system, generics, and targets beyond SPIR-V -- so it is a rule of its own. `.slang` is **not** in the engine's device-source table: this feature declares `device_extensions = [".slang"]` and `rule_module = "mcpp.rules.slang"`, and the engine routes it from there. That is the criterion for the whole arrangement -- a new device language costs no engine release. Since 0.7.0 it has the same `options::storage` axis as `rules-spirv` (header / object / sidecar), `options::extra_args` for the arguments the rule has no field for, and `options::per_file` for what one shader gets that the others do not -- a project with a `-fvk-use-gl-layout` and one shader needing `-emit-spirv-via-glsl` writes both without leaving one `compile()` call ## `rules-spirv` -Module `mcpp.rules.spirv`; engine floor: 2026.9.6.6. +Module `mcpp.rules.spirv`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.6.6 before). **Needs and behaviour.** `[build] accel = "vulkan1.2"`, a constrained glob for the shader stages; compiles each shader through a `role = "source"` action and states which of the two compilers produced it @@ -46,6 +46,6 @@ Module `mcpp.rules.swift`; engine floor: 2026.9.8.1. ## `rules-sycl` -Module `mcpp.rules.sycl`; engine floor: 2026.9.6.6. +Module `mcpp.rules.sycl`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.6.6 before). **Needs and behaviour.** `[build] accel = "sycl"` or `"sycl, cuda12.9+{sm_89}"`, a constrained glob for `*.sycl`, and `compat:sycl-runtime` so the artifact can reach `libsycl.so.9` at run time. Its own engine need is `.sycl` in the device-source table, 2026.9.6.1 diff --git a/mcpp.toml b/mcpp.toml index 9d10464..4fb130c 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,10 +1,12 @@ [package] name = "plugins" namespace = "mcpp" -version = "0.18.1" -# The first mcpp release that states the build information (protocol 14), -# places files in one edge, and renders structured diagnostics (mcpp#734). -mcpp = ">=2026.9.28.3" +version = "0.19.0" +# The first mcpp release that states where each tool and payload comes from +# (protocol 15, mcpp#755): `xpkg_source`, `xpkg_program`, `xpkg_request`, +# `mcpp::decision`, `mcpp::toolchain`, `mcpp::phase`, and the +# `provision = "on-request"` entries below. +mcpp = ">=2026.10.1.3" description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature" license = "Apache-2.0" authors = ["mcpp-community"] @@ -30,8 +32,12 @@ import_std = true # be that action's declared input # src/declare.cppm, L2, the official general library for build programs # src/toolset.cppm, (mcpp#734): `mcpp.plugins.declare`, -# src/fs.cppm `mcpp.plugins.toolset`, `mcpp.plugins.fs` +# src/fs.cppm `mcpp.plugins.toolset`, `mcpp.plugins.fs`, +# src/tool.cppm `mcpp.plugins.tool` -- where a tool a member runs comes +# from, one order for every member (mcpp#755) # src/testing.cppm `mcpp.plugins.testing`, the test kit for plugins +# src/toolchain.cppm `mcpp.plugins.toolchain`, a build toolchain stated by +# the root build program (feature `plugins-toolchain`) # # The L2 units import `mcpp.core` (spelled `mcpp`), which exists only inside a # build program, so listing them here would break the ORDINARY build of this @@ -83,12 +89,20 @@ default = [] # build system (`instance`, `chain`, `detected`) # mcpp.plugins.fs deterministic file generation and placement [features.plugins-core] -sources = ["src/declare.cppm", "src/toolset.cppm", "src/fs.cppm", +sources = ["src/declare.cppm", "src/toolset.cppm", "src/fs.cppm", "src/tool.cppm", "src/compat/detected_toolset.cppm"] # The test kit: a plugin function runs against a stated build context, and its # directives are compared. Separate from `plugins-core`, so that no build # program compiles it unless it asks. +# `mcpp.plugins.toolchain` -- the builders a root build program states its +# build toolchain with, in the toolchain phase of a project whose `[toolchain]` +# says `configure = "build.mcpp"` (mcpp#755). Its own feature rather than part +# of `plugins-core`: a project that states no toolchain compiles nothing of it. +[features.plugins-toolchain] +sources = ["src/toolchain.cppm"] +implies = ["plugins-core"] + [features.plugins-testing] sources = ["src/testing.cppm"] implies = ["plugins-core"] @@ -487,17 +501,22 @@ implies = ["plugins-core"] # `cmake_minimum_required` of ports that CMake 3 accepted. On Linux and macOS # vcpkg's documented host prerequisites -- git, curl, zip, unzip, tar and a C # compiler -- are the host's, as they are for vcpkg itself. +# PROVISIONED WHEN A BUILD PROGRAM ASKS FOR IT (0.19.0, mcpp#755). A consumer +# whose build program names its own vcpkg -- `options::vcpkg`, or an +# `[xlings.overrides]` entry -- never downloads this one, and a consumer that +# names none asks for it through `mcpp.plugins.tool` and gets it installed +# before the plan is made. [feature-xlings.deps-vcpkg] -"xim:vcpkg" = ">=2026.7.27" +"xim:vcpkg" = { version = ">=2026.7.27", provision = "on-request" } # A floor: CMake's version is coupled to the subproject's # `cmake_minimum_required`, which only rises. [feature-xlings.deps-cmake] -"xim:cmake" = ">=3.31" +"xim:cmake" = { version = ">=3.31", provision = "on-request" } # `cmake -E tar xf --touch` extracts a zip on every host; `--touch` is 3.24+. [feature-xlings.deps-archive] -"xim:cmake" = ">=3.31" +"xim:cmake" = { version = ">=3.31", provision = "on-request" } # ── The environment `dist-appimage` needs ────────────────────────────────── # @@ -520,8 +539,14 @@ implies = ["plugins-core"] # A FLOOR RATHER THAN AN EXACT VERSION. appimagetool's version is coupled to # nothing this rule cannot see; 1.9.1 is where the runtime stub ships beside the # tool, which is what makes an offline build expressible. +# ASKED FOR WHEN A PACK NEEDS IT (0.19.0, mcpp#755). An ordinary Linux build +# of a project that names this member installed the tool and never ran it: +# provisioning happens before the build program learns `--format`, which is the +# cost this file recorded under `dist-apk`'s `bundletool`. The member now asks +# for it while planning the pack, so a build that packs nothing downloads +# nothing, and `mcpp pack --format appimage` installs it then. [target.'cfg(linux)'.feature-xlings.dist-appimage] -"xim:appimagetool" = ">=1.9.1" +"xim:appimagetool" = { version = ">=1.9.1", provision = "on-request" } # WiX IS A PAYLOAD, AND FOR A WHILE THIS FILE SAID IT COULD NOT BE. The # toolset is under the Microsoft Reciprocal License and its own licence text @@ -619,7 +644,10 @@ implies = ["plugins-core"] "xim:android-platform" = "36-r2" "xim:jdk-temurin" = "25.0.4+7" "xim:android-debug-keystore" = "1.0.0" -"xim:bundletool" = "1.18.3" +# `xim:bundletool` BUILDS `--format aab` AND IS ASKED FOR THEN (0.19.0, +# mcpp#755): the member requests it while planning a bundle, so an ordinary +# `--format apk` build installs nothing for it. +"xim:bundletool" = { version = "1.18.3", provision = "on-request" } # The compiler `options::kotlin_sources` needs, and the resolver `options::maven` # needs, each behind its own feature (see `[features.dist-apk-kotlin]`). The diff --git a/rules/ascendc.cppm b/rules/ascendc.cppm index b3ba5f6..304dd44 100644 --- a/rules/ascendc.cppm +++ b/rules/ascendc.cppm @@ -58,6 +58,7 @@ export module mcpp.rules.ascendc; import std; import mcpp; +import mcpp.plugins.tool; // WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. @@ -224,9 +225,34 @@ struct toolkit { } }; -inline std::optional find_toolkit() { +// THE TOOLKIT (0.19.0, mcpp#755): what the build program named, an override, +// or the payload this rule declares, asked for when it is declared on request. +// `pending` then says the engine installs it and runs this program again. +inline const mcpp::plugins::tool::spec& cann_spec() { + static const mcpp::plugins::tool::spec s{ + .who = "mcpp.rules.ascendc", .package = "cann-toolkit", + // `/cann/-linux/ccec_compiler/bin/bisheng` is the layout; + // the root is what this rule reads, so no program is looked for. + .programs = {}, .option = "options::toolkit" }; + return s; +} + +inline std::optional find_toolkit(const mcpp::plugins::tool::choice& named = {}, + bool* pending = nullptr) { + if (pending) *pending = false; toolkit t; - const auto pkg = xpkg("cann-toolkit"); + std::string pkg; + if (!named.is_default()) { + auto f = mcpp::plugins::tool::resolve(cann_spec(), named); + pkg = f.root; + } else if (std::string_view(mcpp::xpkg_source("xim", "cann-toolkit")) == "pending") { + (void)mcpp::xpkg_request("xim", "cann-toolkit"); + if (pending) *pending = true; + return std::nullopt; + } else { + pkg = xpkg("cann-toolkit"); + if (!pkg.empty()) (void)mcpp::plugins::tool::resolve(cann_spec()); + } if (pkg.empty()) { std::cerr << std::format("mcpp.rules.ascendc: the CANN toolkit is not installed.\n" " This rule DECLARES it, so a project normally writes nothing. Check, in " @@ -258,6 +284,10 @@ inline std::optional find_toolkit() { // ─── The rule ────────────────────────────────────────────────────────────── struct options { + // THE TOOLKIT (0.19.0, mcpp#755): a root holding CANN, or the default -- + // the `xim:cann-toolkit` payload this rule declares, or an override of it. + // A toolkit named here is not downloaded. + mcpp::plugins::tool::choice toolkit; // Include directories of the PROJECT, added after the toolkit's own. std::vector includes; // Extra flags, appended last so they win. @@ -304,7 +334,8 @@ inline std::vector plan(std::span sources, options opt " default for it.", mcpp::accel()) << '\n'; return out; } - auto tk = find_toolkit(); + bool tkPending = false; + auto tk = find_toolkit(opt.toolkit, &tkPending); if (!tk) return out; const std::string outDir = opt.out_dir.empty() ? std::string(mcpp::out_dir()) @@ -416,7 +447,7 @@ inline bool compile(options opt = {}) { // translation units, which is right: a consumer of this project has no // business seeing the toolkit's headers. `link_search` reaches the final // link, which is what `-lascendcl` in the manifest then resolves against. - if (auto tk = find_toolkit()) { + if (auto tk = find_toolkit(opt.toolkit)) { if (std::filesystem::is_directory(tk->host_include())) mcpp::include_dir(tk->host_include().c_str()); if (std::filesystem::is_directory(tk->lib64())) { diff --git a/rules/cuda.cppm b/rules/cuda.cppm index e0ccf1b..2c422dc 100644 --- a/rules/cuda.cppm +++ b/rules/cuda.cppm @@ -44,6 +44,7 @@ export module mcpp.rules.cuda; import std; import mcpp; +import mcpp.plugins.tool; // WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. @@ -87,6 +88,12 @@ enum class route { automatic, clang, nvcc }; struct options { route which = route::automatic; + // THE TOOLKIT (0.19.0, mcpp#755): a root holding the whole of it + // (`mcpp::plugins::tool::root("/usr/local/cuda")`), `nvcc` itself, or the + // default -- the payloads this rule declares, or overrides of them. A + // toolkit named here is not downloaded: the payloads are declared on + // request. + mcpp::plugins::tool::choice toolkit; // Header search paths for the island. Relative entries resolve against the // package root; an ABSOLUTE entry is passed through unchanged. // @@ -266,7 +273,43 @@ inline std::string xpkg(const char* name) { return {}; } -inline std::optional find_toolkit() { +// The components this rule declares, in the order they are asked for. +inline constexpr const char* kComponents[] = { + "cuda-nvcc", "cuda-cudart", "cuda-crt", "libcurand", "cuda-cccl", "libcuda-host-link", +}; + +inline const mcpp::plugins::tool::spec& nvcc_spec() { + static const mcpp::plugins::tool::spec s{ + .who = "mcpp.rules.cuda", .package = "cuda-nvcc", .programs = {"nvcc"}, + .option = "options::toolkit" }; + return s; +} + +// `pending` is set when the toolkit was asked for: the engine installs every +// component in one batch and runs the program again. +inline std::optional find_toolkit(const options& opt, bool& pending) { + pending = false; + if (!opt.toolkit.is_default()) { + // ONE ROOT FOR EVERY COMPONENT: an NVIDIA installation keeps nvcc, + // the runtime, CCCL and cuRAND in one tree. The driver library is the + // machine's, unless the payload that links against it is installed. + auto f = mcpp::plugins::tool::resolve(nvcc_spec(), opt.toolkit); + if (!f) { + std::cerr << mcpp::plugins::tool::describe_missing(nvcc_spec(), f) << '\n'; + return std::nullopt; + } + toolkit t; + t.nvcc_root = t.cudart_root = t.crt_root = t.curand_root = t.cccl_root = f.root; + t.driver_dir = xpkg("libcuda-host-link"); + return t; + } + for (auto const* c : kComponents) + if (std::string_view(mcpp::xpkg_source("xim", c)) == "pending") { + (void)mcpp::xpkg_request("xim", c); + pending = true; + } + if (pending) return std::nullopt; + (void)mcpp::plugins::tool::resolve(nvcc_spec()); // the decision record toolkit t; t.nvcc_root = xpkg("cuda-nvcc"); t.cudart_root = xpkg("cuda-cudart"); @@ -552,7 +595,8 @@ inline std::vector plan(std::span sources, options opt mcpp::accel()) << '\n'; return out; } - auto tk = find_toolkit(); + bool tkPending = false; + auto tk = find_toolkit(opt, tkPending); if (!tk) return out; state_driver_relation(*tk, tg); diff --git a/rules/hip.cppm b/rules/hip.cppm index f5eceef..4342a0e 100644 --- a/rules/hip.cppm +++ b/rules/hip.cppm @@ -37,6 +37,7 @@ export module mcpp.rules.hip; import std; import mcpp; +import mcpp.plugins.tool; // WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. @@ -64,6 +65,10 @@ enum class platform { automatic, nvidia, amd }; struct options { platform which = platform::automatic; + // THE TOOLKIT (0.19.0, mcpp#755): a root holding HIP and the CUDA back + // end, or the default -- the payloads this rule declares, or overrides of + // them. A toolkit named here is not downloaded. + mcpp::plugins::tool::choice toolkit; // Header search paths for the island. Relative entries resolve against the // package root; an ABSOLUTE entry is passed through unchanged, which is // the form `mcpp::dep_dir` answers with -- a device compiler is a separate @@ -303,8 +308,37 @@ inline std::vector plan(std::span sources, options opt return out; } - toolkit tk{ payload("hip-nvidia"), payload("cuda-nvcc"), payload("cuda-cudart"), - payload("libcurand"), payload("cuda-cccl"), payload("cuda-profiler-api") }; + // The components this rule declares. A root named by the build program + // supplies every one of them, as an installation of HIP for NVIDIA does; + // otherwise each is the payload, or an override of it, and one declared on + // request is asked for here. + static constexpr const char* kComponents[] = { + "hip-nvidia", "cuda-nvcc", "cuda-cudart", "libcurand", "cuda-cccl", "cuda-profiler-api", + }; + const mcpp::plugins::tool::spec hipSpec{ + .who = "mcpp.rules.hip", .package = "hip-nvidia", .programs = {"hipcc", "clang++"}, + .option = "options::toolkit" }; + toolkit tk; + if (!opt.toolkit.is_default()) { + auto f = mcpp::plugins::tool::resolve(hipSpec, opt.toolkit); + if (!f) { + std::cerr << mcpp::plugins::tool::describe_missing(hipSpec, f) << '\n'; + return out; + } + tk = toolkit{ f.root, f.root, f.root, f.root, f.root, f.root }; + } else { + bool pending = false; + for (auto const* c : kComponents) + if (std::string_view(mcpp::xpkg_source("xim", c)) == "pending") { + (void)mcpp::xpkg_request("xim", c); + pending = true; + } + // Asked for: the engine installs them and runs this program again. + if (pending) return out; + (void)mcpp::plugins::tool::resolve(hipSpec); // the decision record + tk = toolkit{ payload("hip-nvidia"), payload("cuda-nvcc"), payload("cuda-cudart"), + payload("libcurand"), payload("cuda-cccl"), payload("cuda-profiler-api") }; + } // Each missing payload is named with the line that adds it. `hip-nvidia` // is this rule's own; the other four are the CUDA back end the NVIDIA diff --git a/rules/qt.cppm b/rules/qt.cppm index 781b2d8..351dcba 100644 --- a/rules/qt.cppm +++ b/rules/qt.cppm @@ -55,6 +55,7 @@ export module mcpp.rules.qt; import std; import mcpp; import mcpp.plugins; +import mcpp.plugins.tool; export namespace mcpp::rules::qt { @@ -297,10 +298,18 @@ inline sdk_source locate(const options& opt = {}) { auto extras = [&] { for (auto const& r : opt.extra_roots) add(detail::absolute_from_root(r)); }; - // 1. The build program. + // 1. The build program. Recorded as the source of the SDK (0.19.0), so a + // build says where Qt came from the way it says where every tool did. if (!opt.root.empty()) { out.level = "options"; - add(detail::absolute_from_root(opt.root)); + const auto root = detail::absolute_from_root(opt.root); + add(root); + // Recorded only when the SDK is there: `add` keeps a directory that + // exists, and a root that does not is reported by the caller as no SDK + // rather than as the source of one. + if (!out.roots.empty()) + mcpp::decision("tool:mcpp.rules.qt:qt", "choice", root.generic_string().c_str(), + "", 0, "xim:qt"); extras(); return out; } @@ -309,17 +318,27 @@ inline sdk_source locate(const options& opt = {}) { if (const char* env = std::getenv("QT_ROOT_DIR"); env && *env) { out.level = "QT_ROOT_DIR"; add(fs::path(env)); + if (!out.roots.empty()) + mcpp::decision("tool:mcpp.rules.qt:qt", "env", env, "QT_ROOT_DIR", 0, "xim:qt"); extras(); return out; } // 3. A declared payload. `xim:qt` is the full base and `xim:qt-base` its // qtbase + qttools subset; a project that declares both uses the full one. - const std::string full = mcpp::xpkg_dir("xim", "qt"); - const std::string base = full.empty() ? std::string(mcpp::xpkg_dir("xim", "qt-base")) : full; + // Resolved through `mcpp.plugins.tool`, so an `[xlings.overrides]` entry + // for either package answers here and the source is recorded (0.19.0). + auto payload_root = [](const char* package) { + const mcpp::plugins::tool::spec s{ + .who = "mcpp.rules.qt", .package = package, .programs = {}, + .option = "options::root", .request = false }; + return mcpp::plugins::tool::resolve(s).root; + }; + const std::string full = payload_root("qt"); + const std::string base = full.empty() ? payload_root("qt-base") : full; if (!base.empty()) { out.level = "xlings"; add(fs::path(base)); - if (const std::string addons = mcpp::xpkg_dir("xim", "qt-addons"); !addons.empty()) + if (const std::string addons = payload_root("qt-addons"); !addons.empty()) add(fs::path(addons)); } extras(); diff --git a/rules/slang.cppm b/rules/slang.cppm index 8cd659b..bb31324 100644 --- a/rules/slang.cppm +++ b/rules/slang.cppm @@ -51,6 +51,7 @@ import mcpp; // reaches them through one shape. import mcpp.plugins; import mcpp.plugins.declare; +import mcpp.plugins.tool; // `std::println` is avoided here for the reason every file in this package // records: it is not header-only, and the symbols its overloads reach for were @@ -102,8 +103,10 @@ struct options { }; std::map per_file; - // An explicit compiler path wins over discovery. - std::string compiler; + // The Slang compiler (0.19.0: a `tool::choice`, so a path still + // assigns). Default: the `xim:slang` payload this rule declares, or an + // override of it. + mcpp::plugins::tool::choice compiler; std::string out_dir = std::string(mcpp::out_dir()); // ── What a consumer names ──────────────────────────────────────────────── @@ -209,17 +212,31 @@ inline std::string first_on_path(const char* exe) { return {}; } -// Discovery, in the order a project can predict: what it named, what the -// environment named, the payload the rule declared, then the PATH. The PATH -// comes last on purpose -- a host slangc is a fine fallback and a poor default, -// because it makes the SPIR-V depend on a machine rather than on a declaration. -inline std::string find_compiler(const options& opt) { - if (!opt.compiler.empty()) return opt.compiler; - if (const char* e = std::getenv("MCPP_SLANGC"); e && *e) return e; - if (const char* dir = mcpp::xpkg_dir("slang"); dir && *dir) - if (auto p = (std::filesystem::path(dir) / "bin" / (std::string("slangc") + kExeSuffix)).string(); - is_file(p)) return p; - return first_on_path("slangc"); +// DISCOVERY THROUGH `mcpp.plugins.tool` (0.19.0): what the build program +// named, `MCPP_SLANGC`, the engine's override or the declared payload. The +// PATH answers only as a choice (`tool::on_path()`); the old silent fallback +// still answers, with a warning naming that choice, until 2027-04-01. +inline const mcpp::plugins::tool::spec& slangc_spec() { + static const mcpp::plugins::tool::spec s{ + .who = "mcpp.rules.slang", .package = "slang", .programs = {"slangc"}, + .option = "options::compiler", .legacy_env = "MCPP_SLANGC" }; + return s; +} + +inline mcpp::plugins::tool::found find_compiler(const options& opt) { + auto f = mcpp::plugins::tool::resolve(slangc_spec(), opt.compiler); + if (f || f.pending() || !opt.compiler.is_default()) return f; + if (auto p = first_on_path("slangc"); !p.empty()) { + mcpp::warning(std::format( + "mcpp.rules.slang: using '{}' from PATH, because nothing else names slangc. A " + "compiler found on PATH is a choice the build program states: " + "`options::compiler = mcpp::plugins::tool::on_path();`. This fallback is " + "removed on 2027-04-01.", p).c_str()); + mcpp::decision("tool:mcpp.rules.slang:slangc", "path", p.c_str()); + f.program = p; + f.source = mcpp::plugins::tool::from::path; + } + return f; } inline std::string run_and_capture(const std::string& cmd) { @@ -341,7 +358,10 @@ inline std::string key_of(std::string_view path) { inline bool compile(std::span shaders, options opt = {}) { if (shaders.empty()) return true; - const auto cc = find_compiler(opt); + const auto found = find_compiler(opt); + // Asked for: the engine installs `xim:slang` and runs this program again. + if (found.pending()) return true; + const std::string cc = found.program; if (cc.empty()) { std::cerr << "mcpp.rules.slang: no Slang compiler found.\n" diff --git a/rules/spirv.cppm b/rules/spirv.cppm index b8d7c5a..7ed3fe5 100644 --- a/rules/spirv.cppm +++ b/rules/spirv.cppm @@ -63,6 +63,7 @@ import mcpp; // consumer names, written once for every member that embeds a payload. import mcpp.plugins; import mcpp.plugins.declare; +import mcpp.plugins.tool; // WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. @@ -97,9 +98,11 @@ struct options { std::vector defines; // glslang's optimiser (`-Os`), which is spirv-opt linked into it. bool optimize = true; - // An explicit compiler path wins over discovery. Set it when a project - // pins a glslang other than the one the workspace installed. - std::string compiler; + // The shader compiler (0.19.0: a `tool::choice`, so a path still + // assigns): glslang or glslc, told apart by program name. Default: the + // payload this rule declares for the host (`xim:glslang` on Linux, + // `xim:shaderc` on macOS and Windows), or an override of it. + mcpp::plugins::tool::choice compiler; std::string out_dir = std::string(mcpp::out_dir()); // ── What a consumer names ──────────────────────────────────────────────── @@ -227,6 +230,8 @@ struct compiler { // Set when discovery already said why it failed, so the caller does not // follow a precise message with a generic one that contradicts it. bool reported = false; + // The payload was asked for (on request): this program runs again with it. + bool pending = false; explicit operator bool() const { return kind != flavour::none && !path.empty(); } const char* name() const { return kind == flavour::glslc ? "glslc" : "glslang"; } }; @@ -279,39 +284,81 @@ inline std::string first_on_path(const char* exe) { return {}; } -// Discovery, in the order a project can predict: what it named, what the -// environment named, the payload the workspace installed, then the PATH. The -// PATH comes last on purpose -- a host shader compiler is a fine fallback and -// a poor default, because it makes the SPIR-V depend on a machine rather than -// on a declaration. +// DISCOVERY THROUGH `mcpp.plugins.tool` (0.19.0), in the order every member +// uses: what the build program named, the environment variables this rule has +// always read (`MCPP_GLSLC`, then `MCPP_GLSLANG`), the engine's override or +// payload (glslang, then shaderc). A name is classified by program name, so +// `MCPP_GLSLC=/opt/bin/glslc` needs no second variable to say what it is. +// +// THE PATH IS NO LONGER CONSULTED BY ITSELF. Until 0.18 it was the last +// resort, which made the SPIR-V depend on whatever the machine had; it is now +// a choice (`tool::on_path()`). The old fallback still answers, with a warning +// naming that choice, until 2027-04-01. +inline const mcpp::plugins::tool::spec& glslang_spec() { + static const mcpp::plugins::tool::spec s{ + .who = "mcpp.rules.spirv", .package = "glslang", + .programs = {"glslangValidator", "glslang"}, .option = "options::compiler", + .legacy_env = "MCPP_GLSLANG" }; + return s; +} +inline const mcpp::plugins::tool::spec& shaderc_spec() { + static const mcpp::plugins::tool::spec s{ + .who = "mcpp.rules.spirv", .package = "shaderc", .programs = {"glslc"}, + .option = "options::compiler", .legacy_env = "MCPP_GLSLC" }; + return s; +} + +inline compiler from_found(const mcpp::plugins::tool::found& f) { + if (f.pending()) return { .pending = true }; + if (!f) return {}; + auto k = classify(f.program); + if (k == flavour::none) { + std::cerr << std::format("mcpp.rules.spirv: '{}' is neither glslang nor glslc by program\n" + " name, and the two share almost no flags. Rename the program or point at the\n" + " real one.", f.program) << '\n'; + return { .reported = true }; + } + return { f.program, k }; +} + inline compiler find_compiler(const options& opt) { - if (!opt.compiler.empty()) { - auto k = classify(opt.compiler); - if (k == flavour::none) { - // Named but unrecognised: taking it as glslang would pass glslang's - // flags to something that is not glslang, and the error would name - // a flag rather than this decision. - std::cerr << std::format("mcpp.rules.spirv: options::compiler names '{}', which is neither glslang\n" - " nor glslc by program name, and the two share almost no flags. Rename the\n" - " program or point at the real one.", opt.compiler) << '\n'; - return { .reported = true }; + namespace tool = mcpp::plugins::tool; + if (!opt.compiler.is_default()) { + // A program is resolved under the spec its name selects; a root or + // PATH is tried for glslang's names, then glslc's. + if (opt.compiler.how == tool::choice::kind::program) { + const auto k = classify(opt.compiler.value); + if (k == flavour::none) { + std::cerr << std::format("mcpp.rules.spirv: options::compiler names '{}', which is neither glslang\n" + " nor glslc by program name, and the two share almost no flags. Rename the\n" + " program or point at the real one.", opt.compiler.value) << '\n'; + return { .reported = true }; + } + return from_found(tool::resolve(k == flavour::glslc ? shaderc_spec() : glslang_spec(), + opt.compiler)); } - return { opt.compiler, k }; + if (auto c = from_found(tool::resolve(glslang_spec(), opt.compiler)); c) return c; + return from_found(tool::resolve(shaderc_spec(), opt.compiler)); + } + for (auto* s : {&shaderc_spec(), &glslang_spec()}) + if (const char* e = std::getenv(s->legacy_env.c_str()); e && *e) + return from_found(tool::resolve(*s)); + for (auto* s : {&glslang_spec(), &shaderc_spec()}) { + auto quiet = *s; + quiet.legacy_env.clear(); + auto f = tool::resolve(quiet); + if (f || f.pending()) return from_found(f); } - if (const char* e = std::getenv("MCPP_GLSLC"); e && *e) return { e, flavour::glslc }; - if (const char* e = std::getenv("MCPP_GLSLANG"); e && *e) return { e, flavour::glslang }; - - if (const char* dir = mcpp::xpkg_dir("glslang"); dir && *dir) - for (const char* exe : {"glslangValidator", "glslang"}) - if (auto p = program_in(std::filesystem::path(dir) / "bin", exe); !p.empty()) - return { p, flavour::glslang }; - if (const char* dir = mcpp::xpkg_dir("shaderc"); dir && *dir) - if (auto p = program_in(std::filesystem::path(dir) / "bin", "glslc"); !p.empty()) - return { p, flavour::glslc }; - - for (const char* exe : {"glslangValidator", "glslang"}) - if (auto p = first_on_path(exe); !p.empty()) return { p, flavour::glslang }; - if (auto p = first_on_path("glslc"); !p.empty()) return { p, flavour::glslc }; + for (const char* exe : {"glslangValidator", "glslang", "glslc"}) + if (auto p = first_on_path(exe); !p.empty()) { + mcpp::warning(std::format( + "mcpp.rules.spirv: using '{}' from PATH, because nothing else names a shader " + "compiler. A compiler found on PATH is a choice the build program states: " + "`options::compiler = mcpp::plugins::tool::on_path();`. This fallback is " + "removed on 2027-04-01.", p).c_str()); + mcpp::decision("tool:mcpp.rules.spirv:glslang", "path", p.c_str()); + return { p, classify(p) }; + } return {}; } @@ -555,6 +602,8 @@ inline bool compile(std::span shaders, options opt = {}) { if (shaders.empty()) return true; const auto cc = find_compiler(opt); + // Asked for: the engine installs the payload and runs this program again. + if (cc.pending) return true; if (!cc) { if (cc.reported) return false; std::cerr << std::format("mcpp.rules.spirv: no shader compiler found.\n" diff --git a/rules/sycl.cppm b/rules/sycl.cppm index 15f1ccc..61e4116 100644 --- a/rules/sycl.cppm +++ b/rules/sycl.cppm @@ -72,6 +72,7 @@ export module mcpp.rules.sycl; import std; import mcpp; +import mcpp.plugins.tool; // WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. @@ -109,9 +110,11 @@ struct options { // forcing a header into every C++ translation unit puts declarations ahead // of `export module`, which no module interface unit accepts. std::vector flags; - // An explicit compiler path wins over the payload. Set it when a project - // pins a DPC++ other than the one the workspace installed. - std::string compiler; + // The DPC++ compiler (0.19.0: a `tool::choice`, so a path still assigns). + // Default: the `xim:dpcpp` payload, or an override of it. The host C and + // C++ libraries (`xim:gcc`, `xim:glibc`, `xim:linux-headers`) are payloads + // too, and an override in `[xlings.overrides]` names another of each. + mcpp::plugins::tool::choice compiler; std::string out_dir = std::string(mcpp::out_dir()); }; @@ -343,7 +346,13 @@ inline std::vector plan(std::span sources, options opt return out; } - const std::string dpcpp = payload("dpcpp"); + const mcpp::plugins::tool::spec dpcppSpec{ + .who = "mcpp.rules.sycl", .package = "dpcpp", .programs = {"clang++"}, + .option = "options::compiler" }; + const auto dpcppTool = mcpp::plugins::tool::resolve(dpcppSpec, opt.compiler); + // Asked for: the engine installs `xim:dpcpp` and runs this program again. + if (dpcppTool.pending()) return out; + const std::string dpcpp = dpcppTool.program; const std::string gcc = payload("gcc"); const std::string cuda = tg.cuda_archs.empty() ? std::string{} : payload("cuda-nvcc"); // THE C LIBRARY IS THE SAME QUESTION AS THE C++ ONE, ONE LAYER DOWN. @@ -374,7 +383,7 @@ inline std::vector plan(std::span sources, options opt // The payload is needed for the COMPILER, so a project that named its own // does not need it. Asking for both would tell someone who has already // solved this to solve it again. - if (dpcpp.empty() && opt.compiler.empty()) missing += " \"xim:dpcpp\" = \"7.1.0\"\n"; + if (dpcpp.empty() && opt.compiler.is_default()) missing += " \"xim:dpcpp\" = \"7.1.0\"\n"; // THE THREE BELOW ARE THE HOST C AND C++ LIBRARIES, AND ONLY LINUX HAS // THIS PROBLEM. Requiring them on Windows would refuse a build over three // packages that this ecosystem does not publish for it and that the @@ -412,11 +421,9 @@ inline std::vector plan(std::span sources, options opt return out; } - auto exe = opt.compiler; - if (exe.empty()) exe = dpcpp + "/bin/clang++" + kExe; + const auto exe = dpcpp; if (!is_file(exe)) { - std::cerr << std::format("mcpp.rules.sycl: {} is not a file. The dpcpp payload publishes its SYCL\n" - " compiler under clang's own name; set options::compiler to name another.", exe) << '\n'; + std::cerr << mcpp::plugins::tool::describe_missing(dpcppSpec, dpcppTool) << '\n'; return out; } if (auto v = compiler_version(exe); !v.empty()) mcpp::fact("dpcpp", v.c_str()); diff --git a/src/plugins.cppm b/src/plugins.cppm index 16b6080..f4f31d2 100644 --- a/src/plugins.cppm +++ b/src/plugins.cppm @@ -49,7 +49,7 @@ export namespace mcpp::plugins { // // One package, one version: the number lives in mcpp.toml, and the CI step // `the collection states its own version` compares the two. -inline constexpr std::string_view version = "0.18.1"; +inline constexpr std::string_view version = "0.19.0"; } // namespace mcpp::plugins diff --git a/src/testing.cppm b/src/testing.cppm index 6e1f187..bd42334 100644 --- a/src/testing.cppm +++ b/src/testing.cppm @@ -70,6 +70,9 @@ inline constexpr std::string_view kKeys[] = { "MCPP_ABI_TOOL_RC", "MCPP_ABI_TOOL_AS", "MCPP_ABI_TOOL_MT", "MCPP_TOOL_ENV", "MCPP_TOOLSET_IDENTITY", "MCPP_MSVC_INSTANCE_DIR", "MCPP_NINJA", "MCPP_CXX_RUNTIME", "MCPP_MSVC_CRT_LINKAGE", + // Sources (0.19.0, mcpp#755): which phase is running. The per-payload keys + // are stated by `context::xpkg*`, which names them itself. + "MCPP_PHASE", }; inline std::string read_file(const std::filesystem::path& p) { @@ -97,8 +100,9 @@ struct context { files.emplace_back(std::move(path), std::move(content)); return *this; } - // `xpkg_dir(ns, name)`: the directory of a declared payload. - context& xpkg(std::string_view ns, std::string_view name, std::string dir) { + // `MCPP_XPKG___`, spelled as the engine spells it. + std::string xpkg_key(std::string_view ns, std::string_view name, + std::string_view suffix) const { std::string key = "MCPP_XPKG_"; auto put = [&](std::string_view s) { for (std::size_t i = 0; i < s.size(); ++i) { @@ -109,9 +113,24 @@ struct context { }; if (!ns.empty()) { put(ns); key += '_'; } put(name); - key += "_DIR"; - return set(std::move(key), std::move(dir)); + key += '_'; + key += suffix; + return key; + } + // `xpkg_dir(ns, name)`: the directory of a declared payload. + context& xpkg(std::string_view ns, std::string_view name, std::string dir) { + return set(xpkg_key(ns, name, "DIR"), std::move(dir)); + } + // `xpkg_source(ns, name)` (0.19.0): "payload", "override" or "pending". + context& xpkg_source(std::string_view ns, std::string_view name, std::string source) { + return set(xpkg_key(ns, name, "SOURCE"), std::move(source)); + } + // `xpkg_program(ns, name)` (0.19.0): the program an override named. + context& xpkg_program(std::string_view ns, std::string_view name, std::string program) { + return set(xpkg_key(ns, name, "PROGRAM"), std::move(program)); } + // The phase a case runs in (0.19.0): "toolchain" for a toolchain phase. + context& phase(std::string name) { return set("MCPP_PHASE", std::move(name)); } }; // Contexts that describe the rows a plugin meets. Paths are under `{root}`, diff --git a/src/tool.cppm b/src/tool.cppm new file mode 100644 index 0000000..7490fa4 --- /dev/null +++ b/src/tool.cppm @@ -0,0 +1,335 @@ +// mcpp.plugins.tool -- where a tool a plugin runs comes from (L2 of the +// build-plugin architecture; mcpp#755, protocol 15). +// +// A member that runs a program -- cmake, glslangValidator, slangc, nvcc, +// appimagetool -- answers one question before it can plan anything: which +// program. Each member used to answer it its own way: an option here, an +// environment variable there, a silent PATH fallback in two members and a +// refusal of PATH in two others. This module answers it once, in one order: +// +// 1. the build program's choice `o.cmake = "/usr/bin/cmake"`, or +// `tool::on_path()`, `tool::root(dir)` +// 2. a member's legacy variable `MCPP_SLANGC`, kept for compatibility +// 3. the engine's override `[xlings.overrides]`, +// `MCPP_XLINGS_OVERRIDE__`, +// config.toml -- the payload is then not +// installed at all +// 4. the declared payload installed before the program runs, or, +// declared `provision = "on-request"`, +// asked for here and installed then +// +// A CHOICE NEVER ASKS FOR THE PAYLOAD. That is what makes "the build program +// names its own tool" mean "the payload is not downloaded": a member whose +// payload is declared on request calls `resolve`, and only the fourth step +// requests it. +// +// EVERY ANSWER IS RECORDED. `resolve` states a `mcpp:decision=` with the +// source and, for a choice, the `build.mcpp` line that made it, so the build +// reports `Using cmake (mcpp.deps.cmake) ← /usr/bin/cmake [program · +// build.mcpp:9]` and `mcpp why tool cmake` answers from the same record. + +module; +#include + +export module mcpp.plugins.tool; + +import std; +import mcpp; + +namespace mcpp::plugins::tool::detail { + +inline bool is_file(const std::filesystem::path& p) { + std::error_code ec; + return !p.empty() && std::filesystem::is_regular_file(p, ec); +} + +inline std::string generic(const std::filesystem::path& p) { + return p.lexically_normal().generic_string(); +} + +#if defined(_WIN32) +inline constexpr char kPathSep = ';'; +inline constexpr std::string_view kExe = ".exe"; +#else +inline constexpr char kPathSep = ':'; +inline constexpr std::string_view kExe = ""; +#endif + +// `/`, with and without `.exe`. +// +// BOTH SPELLINGS ON EVERY HOST, and the suffix of the machine doing the +// building is only the preferred one. A payload repacked with the other +// convention is then found instead of silently missed, and the cost is one +// `stat`; it is also what lets a plugin's test state a Windows row and run on +// Linux, which `mcpp.plugins.testing` does. +inline std::string program_in(const std::filesystem::path& dir, std::string_view name) { + const std::string bare(name); + const std::string exe = bare + ".exe"; + for (auto const& cand : kExe.empty() ? std::array{bare, exe} + : std::array{exe, bare}) { + auto p = dir / cand; + if (is_file(p)) return generic(p); + } + return {}; +} + +inline std::string find_on_path(std::string_view name) { + const char* path = std::getenv("PATH"); + if (!path) return {}; + std::string_view rest(path); + while (!rest.empty()) { + auto sep = rest.find(kPathSep); + auto dir = rest.substr(0, sep); + if (!dir.empty()) + if (auto p = program_in(std::filesystem::path(dir), name); !p.empty()) return p; + if (sep == std::string_view::npos) break; + rest.remove_prefix(sep + 1); + } + return {}; +} + +// The root a program implies: `/bin/`, or its directory. +inline std::string root_of(const std::filesystem::path& program) { + auto dir = program.parent_path(); + if (dir.filename() == "bin") return generic(dir.parent_path()); + return generic(dir); +} + +} // namespace mcpp::plugins::tool::detail + +export namespace mcpp::plugins::tool { + +// WHAT A BUILD PROGRAM STATES ABOUT ONE TOOL, as a member's option. +// +// A STRING IS A PROGRAM, so every option that was a `std::string` keeps its +// spelling: `o.cmake = "/usr/bin/cmake"` constructs a choice, and an empty +// string is the default. The constructor's default argument records the line +// of the assignment -- `std::source_location::current()` in a default +// argument is evaluated where the call is written, which is the build +// program -- and the line reaches the decision record. +struct choice { + enum class kind { payload, program, root, on_path }; + kind how = kind::payload; + std::string value; + std::source_location where{}; + + choice() = default; + choice(std::string program, + std::source_location w = std::source_location::current()) + : how(program.empty() ? kind::payload : kind::program), + value(std::move(program)), where(w) {} + choice(const char* program, + std::source_location w = std::source_location::current()) + : choice(std::string(program ? program : ""), w) {} + + // The default: the engine's answer (override or payload). + bool is_default() const { return how == kind::payload; } + // The text a member stored before this type, for code that read it. + const std::string& str() const { return value; } +}; + +inline choice payload() { return {}; } +// This program. A relative path is relative to the package root. +inline choice program(std::string path, + std::source_location w = std::source_location::current()) { + choice c; c.how = choice::kind::program; c.value = std::move(path); c.where = w; return c; +} +// A directory laid out like the payload (`/bin/`). +inline choice root(std::string dir, + std::source_location w = std::source_location::current()) { + choice c; c.how = choice::kind::root; c.value = std::move(dir); c.where = w; return c; +} +// The first program of the member's list found on PATH, or `name`. +inline choice on_path(std::string name = {}, + std::source_location w = std::source_location::current()) { + choice c; c.how = choice::kind::on_path; c.value = std::move(name); c.where = w; return c; +} + +// WHAT A MEMBER KNOWS ABOUT ONE OF ITS TOOLS, written once per tool. +struct spec { + std::string who; // the member's module: "mcpp.deps.cmake" + std::string ns = "xim"; // the payload's namespace + std::string package; // the payload: "cmake" + std::vector programs; // program names, in preference order + std::vector bin_dirs = {"bin", ""}; // where, under a root + std::string option; // "options::cmake", for messages + std::string legacy_env; // a variable the member read before, or empty + // Ask for a payload declared `provision = "on-request"` when nothing else + // answers. A member that only looks (to report what is there) passes false. + bool request = true; +}; + +enum class from { choice, legacy_env, override_, payload, path, none, pending }; + +inline std::string_view name(from f) { + switch (f) { + case from::choice: return "choice"; + case from::legacy_env: return "env"; + case from::override_: return "override"; + case from::payload: return "payload"; + case from::path: return "path"; + case from::pending: return "pending"; + case from::none: break; + } + return "none"; +} + +struct found { + std::string program; // the program, absolute, forward slashes + std::string root; // the root it implies, when one does + from source = from::none; + // What was consulted and what each answered, for `describe_missing`. + std::vector tried; + + explicit operator bool() const { return !program.empty(); } + // The payload was asked for: the engine installs it and runs the program + // again. The member returns without planning what needs the tool. + bool pending() const { return source == from::pending; } +}; + +// The subject of the decision record: `tool::`. +inline std::string subject_of(const spec& s) { + return "tool:" + s.who + ":" + (s.programs.empty() ? s.package : s.programs.front()); +} + +// The program in a root, by the member's names and directories. +inline std::string program_in_root(const spec& s, const std::filesystem::path& root) { + for (auto const& sub : s.bin_dirs) + for (auto const& p : s.programs) + if (auto hit = detail::program_in(sub.empty() ? root : root / sub, p); !hit.empty()) + return hit; + return {}; +} + +inline found resolve(const spec& s, const choice& c = {}) { + found out; + const auto payloadKey = s.ns + ":" + s.package; + auto record = [&](from f, std::string_view file = {}, unsigned line = 0) { + out.source = f; + mcpp::decision(subject_of(s).c_str(), std::string(name(f)).c_str(), + out.program.c_str(), std::string(file).c_str(), line, + payloadKey.c_str()); + }; + // 1. The build program's choice. + if (!c.is_default()) { + const std::string file = c.where.file_name() ? c.where.file_name() : ""; + const auto line = static_cast(c.where.line()); + std::filesystem::path base = mcpp::manifest_dir(); + switch (c.how) { + case choice::kind::program: { + std::filesystem::path p(c.value); + if (p.is_relative() && p.has_parent_path()) p = base / p; + if (!p.has_parent_path()) { // a bare name: on PATH + out.program = detail::find_on_path(c.value); + if (!out.program.empty()) { + out.root = detail::root_of(out.program); + record(from::choice, file, line); + return out; + } + } else if (detail::is_file(p)) { + out.program = detail::generic(p); + out.root = detail::root_of(p); + record(from::choice, file, line); + return out; + } + out.tried.push_back(std::format("{} = \"{}\" (not found)", s.option, c.value)); + return out; // a stated choice that fails is not replaced by another source + } + case choice::kind::root: { + std::filesystem::path r(c.value); + if (r.is_relative()) r = base / r; + out.program = program_in_root(s, r); + if (!out.program.empty()) { + out.root = detail::generic(r); + record(from::choice, file, line); + return out; + } + out.tried.push_back(std::format("{} = root(\"{}\") (no {} there)", s.option, + c.value, s.programs.empty() ? s.package : s.programs.front())); + return out; + } + case choice::kind::on_path: { + std::vector names = c.value.empty() ? s.programs + : std::vector{c.value}; + for (auto const& n : names) + if (auto p = detail::find_on_path(n); !p.empty()) { + out.program = p; + out.root = detail::root_of(p); + record(from::path, file, line); + return out; + } + out.tried.push_back(std::format("{} = on_path() (not on PATH)", s.option)); + return out; + } + case choice::kind::payload: break; + } + } + out.tried.push_back(std::format("{} (not set)", s.option)); + // 2. The member's legacy variable. + if (!s.legacy_env.empty()) { + mcpp::rerun_if_env_changed(s.legacy_env.c_str()); + if (const char* v = std::getenv(s.legacy_env.c_str()); v && *v) { + out.program = detail::generic(v); + out.root = detail::root_of(out.program); + out.source = from::legacy_env; + mcpp::decision(subject_of(s).c_str(), "env", out.program.c_str(), + s.legacy_env.c_str(), 0, payloadKey.c_str()); + return out; + } + out.tried.push_back(std::format("{} (not set)", s.legacy_env)); + } + // 3 and 4. The engine's answer: an override, or the payload. + const std::string source = mcpp::xpkg_source(s.ns.c_str(), s.package.c_str()); + if (source == "override") { + out.program = mcpp::xpkg_program(s.ns.c_str(), s.package.c_str()); + const std::string dir = mcpp::xpkg_dir(s.ns.c_str(), s.package.c_str()); + if (out.program.empty() && !dir.empty()) out.program = program_in_root(s, dir); + out.root = dir; + if (!out.program.empty()) { record(from::override_); return out; } + out.tried.push_back(std::format("the override of {} (names no {} under '{}')", + payloadKey, s.programs.empty() ? s.package + : s.programs.front(), dir)); + return out; + } + if (source == "pending" && s.request) { + (void)mcpp::xpkg_request(s.ns.c_str(), s.package.c_str()); + out.source = from::pending; + out.tried.push_back(std::format("payload {} (requested)", payloadKey)); + return out; + } + if (const std::string dir = mcpp::xpkg_dir(s.ns.c_str(), s.package.c_str()); !dir.empty()) { + out.program = program_in_root(s, dir); + out.root = dir; + if (!out.program.empty()) { record(from::payload); return out; } + out.tried.push_back(std::format("payload {} at '{}' (no {} in it)", payloadKey, dir, + s.programs.empty() ? s.package : s.programs.front())); + return out; + } + out.tried.push_back(source == "pending" + ? std::format("payload {} (declared on request, not requested)", payloadKey) + : std::format("payload {} (not declared for this build, or not installed)", payloadKey)); + return out; +} + +// THE ONE REFUSAL TEXT: what was consulted, and the four ways to name the +// tool. A member prints it with `mcpp::warning` (R1.2) or its own stream. +inline std::string describe_missing(const spec& s, const found& f) { + const auto tool = s.programs.empty() ? s.package : s.programs.front(); + const auto key = s.ns + ":" + s.package; + std::string env = "MCPP_XLINGS_OVERRIDE_"; + for (char ch : key) + env += (ch >= 'a' && ch <= 'z') ? char(ch - 'a' + 'A') + : ((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) ? ch : '_'; + std::string out = std::format("{}: no {}.\n", s.who, tool); + for (auto const& t : f.tried) out += " consulted: " + t + "\n"; + out += std::format( + " Any one of these names it:\n" + " o.{} = \"/path/to/{}\"; // build.mcpp\n" + " [xlings.overrides] \"{}\" = \"/path\" // mcpp.toml\n" + " {}=/path // environment\n" + " or install the payload: `mcpp build` provisions {} when it is declared.", + s.option.starts_with("options::") ? s.option.substr(9) : s.option, tool, key, env, key); + return out; +} + +} // namespace mcpp::plugins::tool diff --git a/src/toolchain.cppm b/src/toolchain.cppm new file mode 100644 index 0000000..928bd13 --- /dev/null +++ b/src/toolchain.cppm @@ -0,0 +1,200 @@ +// mcpp.plugins.toolchain -- a build toolchain stated by the root build +// program (L2 of the build-plugin architecture; mcpp#755, protocol 15; +// feature `plugins-toolchain`). +// +// A project whose manifest says +// +// [toolchain] +// default = { configure = "build.mcpp" } +// +// has its root build program run twice. Its toolchain phase runs first, with +// the bootstrap toolchain, before the dependency graph is resolved; there the +// program states the toolchain that builds the project, and returns. The +// build phase runs as every build program does, with that toolchain. +// +// import mcpp; +// import mcpp.plugins.toolchain; +// namespace tc = mcpp::plugins::toolchain; +// +// int main() { +// if (tc::configure([] { +// auto d = tc::layout(tc::env("ACME_LLVM", "/opt/acme-llvm")); +// tc::use(tc::with_launcher(d, "ccache")); +// })) +// return 0; +// // the build phase +// } +// +// A DESCRIPTION IS THE `[toolchain]` TABLE. Its fields are the table's keys, +// and the engine reads both in one place, so a toolchain stated here and one +// named in the manifest behave alike. What this module adds are the builders +// a manifest cannot express: a value read from the environment, a tree found +// by looking, a vendor SDK's environment script, a toolchain composed of parts. + +export module mcpp.plugins.toolchain; + +import std; +import mcpp; + +export namespace mcpp::plugins::toolchain { + +struct description { + // A managed toolchain (`llvm@23.1.3`), or empty for one named by path. + std::string spec; + // A toolchain named by path: its root and the rest of a `[toolchain]` table. + std::string path, prefix, sysroot, family, launcher; + std::vector> tools; // role -> program + // The statement that produced it, for the source the build reports. + std::source_location where{}; +}; + +// A managed toolchain the engine installs and drives, as `[toolchain]` +// would name it. Reported as a pinned source, stated by the build program. +inline description managed(std::string spec, + std::source_location w = std::source_location::current()) { + description d; d.spec = std::move(spec); d.where = w; return d; +} +// A tree in the normalized layout: `/bin/clang++` or `/bin/g++`. +inline description layout(std::string root, + std::source_location w = std::source_location::current()) { + description d; d.path = std::move(root); d.where = w; return d; +} +// A cross toolchain whose drivers and tools carry a prefix +// (`aarch64-none-linux-gnu-g++`). +inline description prefixed(std::string root, std::string prefix, + std::source_location w = std::source_location::current()) { + description d; d.path = std::move(root); d.prefix = std::move(prefix); d.where = w; return d; +} +inline description with_launcher(description d, std::string launcher) { + d.launcher = std::move(launcher); return d; +} +inline description with_sysroot(description d, std::string sysroot) { + d.sysroot = std::move(sysroot); return d; +} +inline description with_family(description d, std::string family) { + d.family = std::move(family); return d; +} +// A tool by role (`ld`, `ar`, `cxx`, ...), where the tree does not have it. +inline description with_tool(description d, std::string role, std::string program) { + for (auto& [r, p] : d.tools) if (r == role) { p = std::move(program); return d; } + d.tools.emplace_back(std::move(role), std::move(program)); + return d; +} + +// The value of an environment variable, or `fallback`; the build program +// runs again when the variable changes. +inline std::string env(const char* name, std::string fallback = {}) { + mcpp::rerun_if_env_changed(name); + const char* v = std::getenv(name); + return v && *v ? std::string(v) : std::move(fallback); +} + +// The newest directory under `parent` whose name starts with `stem`, by name +// (`/opt/acme-llvm-23`, `/opt/acme-llvm-24` -> the second); empty when none. +inline std::string newest_under(const std::filesystem::path& parent, std::string_view stem) { + std::error_code ec; + std::string best; + for (auto const& e : std::filesystem::directory_iterator(parent, ec)) { + if (!e.is_directory(ec)) continue; + const auto n = e.path().filename().string(); + if (!n.starts_with(stem)) continue; + if (best.empty() || n > std::filesystem::path(best).filename().string()) + best = e.path().lexically_normal().generic_string(); + } + mcpp::rerun_if_changed(parent.generic_string().c_str()); + return best; +} + +// A VENDOR SDK'S ENVIRONMENT SCRIPT (Yocto's `environment-setup-`, +// and SDKs shaped like it): `export NAME="value"` lines, read without running +// a shell. The driver is the first word of `CXX`, found on the script's own +// `PATH`; its directory's parent is the root, the part of its name before +// `g++`/`clang++` the prefix, and `--sysroot=` in `CXX` (or `SDKTARGETSYSROOT`) +// the sysroot. Empty `path` when the script does not describe a compiler. +inline description from_env_script(const std::filesystem::path& script, + std::source_location w = std::source_location::current()) { + description d; d.where = w; + mcpp::rerun_if_changed(script.generic_string().c_str()); + std::ifstream in(script); + std::map vars; + auto expand = [&](std::string v) { + std::string out; + for (std::size_t i = 0; i < v.size(); ++i) { + if (v[i] != '$') { out += v[i]; continue; } + std::size_t j = i + 1; + const bool brace = j < v.size() && v[j] == '{'; + if (brace) ++j; + std::size_t k = j; + while (k < v.size() && (std::isalnum(static_cast(v[k])) || v[k] == '_')) ++k; + const auto name = v.substr(j, k - j); + if (auto it = vars.find(name); it != vars.end()) out += it->second; + else if (const char* e = std::getenv(name.c_str())) out += e; + i = (brace && k < v.size() && v[k] == '}') ? k : k - 1; + } + return out; + }; + for (std::string line; std::getline(in, line);) { + std::string_view l(line); + while (!l.empty() && (l.front() == ' ' || l.front() == '\t')) l.remove_prefix(1); + if (!l.starts_with("export ")) continue; + l.remove_prefix(7); + const auto eq = l.find('='); + if (eq == std::string_view::npos) continue; + std::string name(l.substr(0, eq)); + std::string value(l.substr(eq + 1)); + if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') + && value.back() == value.front()) + value = value.substr(1, value.size() - 2); + vars[name] = expand(value); + } + auto cxx = vars["CXX"]; + if (cxx.empty()) return d; + const auto driver = cxx.substr(0, cxx.find(' ')); + if (auto at = cxx.find("--sysroot="); at != std::string::npos) { + auto end = cxx.find(' ', at); + d.sysroot = cxx.substr(at + 10, end == std::string::npos ? std::string::npos : end - at - 10); + } else if (auto it = vars.find("SDKTARGETSYSROOT"); it != vars.end()) { + d.sysroot = it->second; + } + std::string_view rest(vars["PATH"]); + while (!rest.empty()) { + auto sep = rest.find(':'); + std::filesystem::path dir(rest.substr(0, sep)); + std::error_code ec; + if (!dir.empty() && std::filesystem::exists(dir / driver, ec)) { + d.path = dir.parent_path().lexically_normal().generic_string(); + for (auto suffix : {std::string_view("clang++"), std::string_view("g++")}) + if (std::string_view(driver).ends_with(suffix)) + d.prefix = driver.substr(0, driver.size() - suffix.size()); + break; + } + if (sep == std::string_view::npos) break; + rest.remove_prefix(sep + 1); + } + return d; +} + +// States the build toolchain. Only meaningful in the toolchain phase. +inline void use(const description& d) { + if (!d.spec.empty()) mcpp::toolchain("spec", d.spec.c_str()); + if (!d.path.empty()) mcpp::toolchain("path", d.path.c_str()); + if (!d.prefix.empty()) mcpp::toolchain("prefix", d.prefix.c_str()); + if (!d.sysroot.empty()) mcpp::toolchain("sysroot", d.sysroot.c_str()); + if (!d.family.empty()) mcpp::toolchain("family", d.family.c_str()); + if (!d.launcher.empty()) mcpp::toolchain("launcher", d.launcher.c_str()); + for (auto const& [role, p] : d.tools) + mcpp::toolchain(("tool." + role).c_str(), p.c_str()); + if (d.where.file_name() && *d.where.file_name()) + mcpp::toolchain("origin", std::format("{}:{}", d.where.file_name(), d.where.line()).c_str()); +} + +// Runs `state` in the toolchain phase and answers true: `main` then returns. +// Answers false in the build phase, without running it. +template +bool configure(F&& state) { + if (std::string_view(mcpp::phase()) != "toolchain") return false; + std::forward(state)(); + return true; +} + +} // namespace mcpp::plugins::toolchain diff --git a/tests/plugin-logic/build.mcpp b/tests/plugin-logic/build.mcpp index 959d75c..4378805 100644 --- a/tests/plugin-logic/build.mcpp +++ b/tests/plugin-logic/build.mcpp @@ -10,6 +10,7 @@ import mcpp.deps; // the 0.16.0 names, for the compatibility case import mcpp.deps.vcpkg; import mcpp.deps.cmake; import mcpp.rules.qt; +import mcpp.plugins.tool; namespace t = mcpp::plugins::testing; namespace ts = mcpp::plugins::toolset; @@ -85,6 +86,11 @@ std::string stem(const std::string& path) { // name, and the text it is hashed from, hold no path (design §6.6). std::string first_linux_name; +// The resolver under the `deps-cmake` spec, which every case above shares. +mcpp::plugins::tool::found tool_resolve(mcpp::plugins::tool::choice c = {}) { + return mcpp::plugins::tool::resolve(mcpp::deps::cmake_spec("mcpp.deps.cmake"), c); +} + } // namespace int main(int argc, char** argv) { @@ -446,6 +452,97 @@ int main(int argc, char** argv) { "moc reads no header another build system wrote"); } }, + // ── the tool resolver (0.19.0, mcpp#755) ──────────────────────────── + // + // One order for every member: the build program's choice, the member's + // legacy variable, the engine's override, the declared payload. Each + // case states what the engine would answer and reads which source the + // resolver took -- and, for a choice, that no payload was requested. + { "tool: a choice names the program and asks for no payload", + // Relative to the package root, which is where a build program's own + // option would name it. + t::row::linux_libcxx().file("pkg/opt/bin/cmake") + .xpkg("xim", "cmake", "{root}/payload") + .xpkg_source("xim", "cmake", "pending"), + [] { + const auto f = tool_resolve(mcpp::plugins::tool::program( + std::string(mcpp::manifest_dir()) + "/opt/bin/cmake")); + std::printf("source=%s program=%s\n", + std::string(mcpp::plugins::tool::name(f.source)).c_str(), + f.program.c_str()); + return f ? 0 : 1; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("source=choice", "opt/bin/cmake"), "the choice is the source"); + c.expect(!r.has_line("mcpp:xpkg-request="), "a choice requests no payload"); + c.expect(r.has_line("mcpp:decision=tool:mcpp.deps.cmake:cmake", {"choice", "opt/bin/cmake"}), + "the decision names the choice and the program"); + } }, + + { "tool: an override answers, and the payload is not consulted", + t::row::linux_libcxx().file("opt/bin/cmake") + .xpkg("xim", "cmake", "{root}/opt") + .xpkg_source("xim", "cmake", "override") + .xpkg_program("xim", "cmake", "{root}/opt/bin/cmake"), + [] { + const auto f = tool_resolve(); + std::printf("source=%s program=%s\n", + std::string(mcpp::plugins::tool::name(f.source)).c_str(), + f.program.c_str()); + return f ? 0 : 1; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("source=override", "opt/bin/cmake"), "the override is the source"); + c.expect(!r.has_line("mcpp:xpkg-request="), "an override requests nothing"); + } }, + + { "tool: a payload declared on request is asked for, and nothing is planned", + t::row::linux_libcxx().xpkg_source("xim", "cmake", "pending"), + [] { + const auto f = tool_resolve(); + std::printf("pending=%d program=%s\n", f.pending() ? 1 : 0, f.program.c_str()); + return 0; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("pending=1 program="), "the resolver reports it is pending"); + c.expect(r.has_line("mcpp:xpkg-request=xim:cmake"), "the payload is requested"); + c.expect(!r.has_line("mcpp:decision="), "a pending payload decides nothing yet"); + } }, + + { "tool: the installed payload answers when nothing else does", + t::row::linux_libcxx().file("payload/bin/cmake") + .xpkg("xim", "cmake", "{root}/payload"), + [] { + const auto f = tool_resolve(); + std::printf("source=%s program=%s\n", + std::string(mcpp::plugins::tool::name(f.source)).c_str(), + f.program.c_str()); + return f ? 0 : 1; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("source=payload", "payload/bin/cmake"), "the payload is the source"); + c.expect(r.has_line("mcpp:decision=tool:mcpp.deps.cmake:cmake", {"payload"}), + "the decision names the payload"); + } }, + + { "tool: nothing names it, and the refusal lists every way to", + t::row::linux_libcxx(), + [] { + const auto f = tool_resolve(); + std::printf("%s\n", mcpp::plugins::tool::describe_missing( + mcpp::deps::cmake_spec("mcpp.deps.cmake"), f).c_str()); + return 0; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("mcpp.deps.cmake: no cmake."), "it names the member and the tool"); + c.expect(r.has_line(" consulted: options::cmake (not set)"), "it names the option"); + c.expect(r.has_line(" o.cmake = "), "it names the build program's way"); + c.expect(r.has_line(" [xlings.overrides] \"xim:cmake\" = \"/path\""), + "it names the manifest's way"); + c.expect(r.has_line(" MCPP_XLINGS_OVERRIDE_XIM_CMAKE=/path"), + "it names the environment's way"); + } }, + // ── compatibility ─────────────────────────────────────────────────── { "compat: program_compilers keeps its 0.16.0 answer and says until when", t::row::linux_libcxx().file("llvm/bin/clang").file("llvm/bin/clang++"), From 1fc2c37f9fc501f424dfffe8bf9182b7fab488e6 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 1 Oct 2026 22:36:01 +0800 Subject: [PATCH 02/24] docs: the plan records what was built, and the two portability findings CI gave --- ...1-ecosystem-build-plugin-framework-plan.md | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index b4214e9..4e882ff 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -76,4 +76,42 @@ T17 issue 评论与关闭;设计记录追加状态行 ## 4. 执行记录 -(随执行追加:PR、run、发布与验证结果。) +### 2026-10-01 + +**issue**:mcpp#755(引擎)、plugins#41(0.19.0)。 + +**mcpp 侧**(分支 `feat/build-sources`,PR mcpp#758,版本 2026.10.1.3): +- 清单层:`[xlings.overrides]`(根清单、`[target.'cfg(..)']`、环境变量、config.toml)、 + 条目的 `provision` 键、`[toolchain]` 的表形式与 `bootstrap`; +- 协议 15:`xpkg_source`、`xpkg_program`、`xpkg_request`、`xpkg_pending`、`phase`、 + `decision`、`toolchain`; +- prepare:`sources.cpp`(决策记录、覆盖、按需供给、`--managed-only`)与 + `local_toolchain.cpp`(按路径命名的工具链、工具链阶段、两遍 prepare); +- 输出:`ui::source` 的 `Using` 行、`Finished` 的汇总、`resolution.json` 的 `sources`、 + `mcpp why sources|tool|payload` 与 `mcpp.why.sources`; +- 测试:`tests/unit/test_sources.cpp`(15 例)与 e2e 873-877。 + +**plugins 侧**(分支 `feat/0.19.0-tool-sources`,0.19.0):`mcpp.plugins.tool`、 +`mcpp.plugins.toolchain`、13 个成员迁移、5 个按需条目、`plugin-logic` 新增 5 例、 +CI 新增 `tool-sources` 判据。 + +**两处发现,都由 CI 的其他平台给出**: +1. `std::to_string` 在 libc++ 上对文件系统时钟的 rep 与 `uintmax_t` 同时有重载,macOS + 编译失败;三处改为 `std::format` 加显式类型。 +2. 从 `mcpp.toolchain.model` 导出 + `std::vector>` 使 clang 20.1.7 在 + Windows 上为**另一个模块**的 `mcpp::pack::interface_set_digest` 生成代码时崩溃—— + 该函数实例化同一个特化并按指向 `first` 的成员指针排序。改为具名结构 + `ToolOverride`。这与 `modules/manifest/src/types.cppm` 记下的 GCC 16 截断 BMI 是 + 同一类危害:报错点名的文件与改动无关。 + +**本机验证**:单测 144 通过;e2e 873-877 通过;按关键词选出的既有 e2e 62 个通过 +(219 在已发布的 2026.10.1.2 上同样失败,658 需要连接 Android 设备);plugins 的 11 个 +consumer fixture 与 27 例 `plugin-logic` 通过;`tests/cmake-consumer` 在 +`MCPP_NO_AUTO_INSTALL=1` 下由 `build.mcpp` 点名 cmake 即可构建。 + +**llvm-macos27-lab**(speak-agent,公开):`release/23.x` 的 `21ef2ddb8060`(含 +`ee66426152f9`)构建的 lld 在 `xcode-27`(macOS 27.0、Xcode 27.0、SDK 27.0)上链接并 +运行 C、C++23(含 `std::format`)与 `import std;`;反例以 23.1.2 原装 lld 失败并点出 +`arm64e.x1`,`macos-15` 对照两者皆成功。官方 23.1.2 的 macOS 包确实带 libc++ 头文件、 +库与 `std` 模块源码(与 §12 R20a 的疑问相反)。 From e042567cad32546d4465fb35489d73972580b310 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 1 Oct 2026 22:44:44 +0800 Subject: [PATCH 03/24] ci: the release every job fetches is its own variable, so a change can 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. --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a75e4c8..b42e916 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: inputs: mcpp_source_ref: - description: "A branch or tag of mcpp-community/mcpp to build and run in place of the release MCPP_VERSION names" + description: "A branch or tag of mcpp-community/mcpp to build and run in place of the release MCPP_BOOTSTRAP names" required: false default: "" @@ -68,6 +68,17 @@ env: # collection now declares, so a consumer that names its own tool downloads # nothing. An older engine refuses the manifest at the package floor. MCPP_VERSION: 2026.10.1.3 + # THE RELEASE EVERY JOB FETCHES, WHICH IS NOT ALWAYS THE ONE UNDER TEST. + # + # `MCPP_VERSION` states the engine this collection needs -- the package floor + # (`[package] mcpp`) is the same statement -- and a release of it may not exist + # yet: 0.19.0 was written against mcpp#755 and pinned 2026.10.1.3 before that + # release was published. Every fetch therefore names this variable instead, and + # the dispatch input `mcpp_source_ref` builds the engine under review with it. + # + # It moves to `MCPP_VERSION` once that release exists, so the ordinary path + # runs the engine this collection states. + MCPP_BOOTSTRAP: 2026.10.1.2 # AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE. # # Empty on every push and pull request, so the steps run the release above. @@ -114,16 +125,16 @@ jobs: uses: actions/cache@v4 with: path: ~/.mcpp - key: mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}-${{ hashFiles('tests/*/mcpp.toml') }} + key: mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_BOOTSTRAP }}-${{ hashFiles('tests/*/mcpp.toml') }} restore-keys: | - mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}- + mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_BOOTSTRAP }}- - name: Fetch the released mcpp run: | curl -L -fsS --retry 3 --retry-all-errors -o mcpp.tar.gz \ - "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/mcpp-${MCPP_VERSION}-linux-x86_64.tar.gz" + "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_BOOTSTRAP}/mcpp-${MCPP_BOOTSTRAP}-linux-x86_64.tar.gz" tar -xzf mcpp.tar.gz - MCPP="$PWD/mcpp-${MCPP_VERSION}-linux-x86_64/bin/mcpp" + MCPP="$PWD/mcpp-${MCPP_BOOTSTRAP}-linux-x86_64/bin/mcpp" "$MCPP" --version # THE BUNDLED xlings, NAMED EXPLICITLY BECAUSE `MCPP_HOME` IS PINNED. # @@ -133,7 +144,7 @@ jobs: # with `xlings binary not found` -- measured. The override is needed # only for that first call: mcpp copies the binary into the pinned # home and answers from there afterwards. - export MCPP_VENDORED_XLINGS="$PWD/mcpp-${MCPP_VERSION}-linux-x86_64/registry/bin/xlings" + export MCPP_VENDORED_XLINGS="$PWD/mcpp-${MCPP_BOOTSTRAP}-linux-x86_64/registry/bin/xlings" "$MCPP" self config --mirror GLOBAL echo "MCPP=$MCPP" >> "$GITHUB_ENV" echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV" @@ -1796,20 +1807,20 @@ jobs: uses: actions/cache@v4 with: path: ~/.mcpp - key: mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}-${{ hashFiles('tests/spirv-consumer/mcpp.toml') }} + key: mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_BOOTSTRAP }}-${{ hashFiles('tests/spirv-consumer/mcpp.toml') }} restore-keys: | - mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}- + mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_BOOTSTRAP }}- - name: Fetch the released mcpp run: | set -e curl -L -fsS --retry 3 --retry-all-errors -o mcpp.pkg \ - "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/mcpp-${MCPP_VERSION}-${{ matrix.asset }}" + "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_BOOTSTRAP}/mcpp-${MCPP_BOOTSTRAP}-${{ matrix.asset }}" case "${{ matrix.asset }}" in *.zip) unzip -q mcpp.pkg ;; *) tar -xzf mcpp.pkg ;; esac - dir="mcpp-${MCPP_VERSION}-${{ matrix.dir-suffix }}" + dir="mcpp-${MCPP_BOOTSTRAP}-${{ matrix.dir-suffix }}" MCPP="$PWD/$dir/bin/mcpp" "$MCPP" --version # The bundled xlings, named explicitly because MCPP_HOME is pinned @@ -2473,20 +2484,20 @@ jobs: uses: actions/cache@v4 with: path: ~/.mcpp - key: mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}-${{ hashFiles('tests/vcpkg-consumer/mcpp.toml') }} + key: mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_BOOTSTRAP }}-${{ hashFiles('tests/vcpkg-consumer/mcpp.toml') }} restore-keys: | - mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}- + mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_BOOTSTRAP }}- - name: Fetch the released mcpp run: | set -e curl -L -fsS --retry 3 --retry-all-errors -o mcpp.pkg \ - "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/mcpp-${MCPP_VERSION}-windows-x86_64.zip" + "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_BOOTSTRAP}/mcpp-${MCPP_BOOTSTRAP}-windows-x86_64.zip" case "windows-x86_64.zip" in *.zip) unzip -q mcpp.pkg ;; *) tar -xzf mcpp.pkg ;; esac - dir="mcpp-${MCPP_VERSION}-windows-x86_64" + dir="mcpp-${MCPP_BOOTSTRAP}-windows-x86_64" MCPP="$PWD/$dir/bin/mcpp" "$MCPP" --version # The bundled xlings, named explicitly because MCPP_HOME is pinned From 5d92352b7880414782df68d47c6e69f705147e00 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 1 Oct 2026 23:12:12 +0800 Subject: [PATCH 04/24] testing: a case sees only the payloads it states, and one CI criterion 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. --- .github/workflows/ci.yml | 6 +++++- src/testing.cppm | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b42e916..b13fb73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -746,7 +746,11 @@ jobs: grep -v '^[[:space:]]*#' mcpp.toml | grep -n 'host-module' exit 1 fi - "$MCPP" build | tee build.log + # `2>&1`, BECAUSE THE LINE THIS ASSERTS ON IS NARRATION. mcpp writes + # every status line to standard error from 2026.10.1.1 (the output + # streams design), so a step that captures stdout alone reads an empty + # log and reports that the rule did not run while it did. + "$MCPP" build 2>&1 | tee build.log grep -q 'Rules mcpp.rules.spirv' build.log \ || { echo "FAIL: the build did not report which rule it ran"; exit 1; } "$MCPP" run | tee run.log diff --git a/src/testing.cppm b/src/testing.cppm index bd42334..50cb822 100644 --- a/src/testing.cppm +++ b/src/testing.cppm @@ -39,6 +39,9 @@ module; #include +#if !defined(_WIN32) +#include // environ -- the kit clears the real build's MCPP_XPKG_* +#endif export module mcpp.plugins.testing; @@ -71,10 +74,39 @@ inline constexpr std::string_view kKeys[] = { "MCPP_TOOL_ENV", "MCPP_TOOLSET_IDENTITY", "MCPP_MSVC_INSTANCE_DIR", "MCPP_NINJA", "MCPP_CXX_RUNTIME", "MCPP_MSVC_CRT_LINKAGE", // Sources (0.19.0, mcpp#755): which phase is running. The per-payload keys - // are stated by `context::xpkg*`, which names them itself. + // are stated by `context::xpkg*`, which names them itself, and the ones the + // real build set are cleared by `inherited_payload_keys`. "MCPP_PHASE", }; +// EVERY `MCPP_XPKG_*` THE REAL BUILD SET, so that a case sees only the payloads +// it states. These cases run inside a build program, and the engine gives that +// program one `_DIR`, `_PROGRAM` and `_SOURCE` for each payload its package +// declares -- `pending` among them, for a payload declared +// `provision = "on-request"` and not installed. Inherited, that made the eight +// vcpkg cases ask for `xim:vcpkg` and plan nothing on a host where it was not +// installed, while passing on one where it was; the symptom was a plan with the +// prefix mapping but no install action (measured on macOS arm64, 0.19.0). +// +// Enumerated rather than listed: the keys are derived from package names, so no +// fixed list can cover the next payload a member reads. +inline std::vector inherited_payload_keys() { +#if defined(_WIN32) + char** env = _environ; +#else + char** env = environ; +#endif + std::vector out; + for (char** e = env; e && *e; ++e) { + std::string_view entry(*e); + const auto eq = entry.find('='); + if (eq == std::string_view::npos) continue; + const auto name = entry.substr(0, eq); + if (name.starts_with("MCPP_XPKG_")) out.emplace_back(name); + } + return out; +} + inline std::string read_file(const std::filesystem::path& p) { std::ifstream in(p, std::ios::binary); return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; @@ -293,6 +325,8 @@ inline int run(int argc, char** argv, std::span cases) { // The union of the keys any case states, beside the kit's own list. std::vector keys; for (auto k : detail::kKeys) keys.emplace_back(k); + for (auto& k : detail::inherited_payload_keys()) + if (std::ranges::find(keys, k) == keys.end()) keys.push_back(std::move(k)); for (auto const& c : cases) for (auto const& kv : c.ctx.values) if (std::ranges::find(keys, kv.first) == keys.end()) keys.push_back(kv.first); From e67e6db8cd92970c81d6665a057fbdbe1444ecaa Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 1 Oct 2026 23:37:08 +0800 Subject: [PATCH 05/24] testing: the environment array is reached the way each platform offers 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 , whose declaration a module purview does not see. --- ...1-ecosystem-build-plugin-framework-plan.md | 26 +++++++++++++++++++ src/testing.cppm | 13 ++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index 4e882ff..ff4360d 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -110,6 +110,32 @@ CI 新增 `tool-sources` 判据。 consumer fixture 与 27 例 `plugin-logic` 通过;`tests/cmake-consumer` 在 `MCPP_NO_AUTO_INSTALL=1` 下由 `build.mcpp` 点名 cmake 即可构建。 +**另外四处发现,三处由 CI 的其他平台给出,一处由手工验证给出**: +3. 一个导入很多宿主模块的构建程序,其编译命令每个模块带一条 + `-fmodule-file==`(绝对路径)。`all-rules-compile` 导入十五个,在集合多出 + 一个模块的那一刻越过 Windows 上 `capture_exec` 所经 shell 的 8191 字节,只报 + `The command line is too long.`——既不点名长度也不点名原因,正是 + `mcpp.build.cmdlimits` 要让人认出来的那一类。改为超过该模块所述预算时走 `@file`, + 引号规则由导出的 `response_file_body` 与每种语法一个单测覆盖(命令本身无法在不越限的宿主上 + 复现)。第二遍才对:响应文件不是一种格式——clang 与 GCC 按 GNU 方式 tokenize,反斜杠是转义, + 于是照原样写入的 Windows 路径被吃掉分隔符 + (`no such file or directory: 'D:amcpp-plugins...'`);这两个驱动改为每个参数套单引号, + cl 与 clang-cl 保持 Windows 引号规则。 +4. `mcpp.plugins.testing` 把真实构建的 `MCPP_XPKG_*` 留在环境里。这些用例运行在一个构建 + 程序内部,而引擎为该程序声明的每个载荷设置 `_DIR`、`_PROGRAM`、`_SOURCE`——包括 + `provision = "on-request"` 且无人请求时的 `pending`。于是 8 个 vcpkg 用例在载荷未安装的 + 机器上请求 `xim:vcpkg` 并什么都不规划,在已安装的机器上照常通过:macOS arm64 与 Windows + 各 19/27,本机 27/27。键由包名派生,所以按前缀枚举而不是列表。同一个二进制、同一个继承值 + 两次测量:修前 19/27,修后 27/27。 +5. spirv fixture 的 CI 步骤断言一条状态行,却只捕获 stdout。mcpp 自 2026.10.1.1 起把叙述写到 + 标准错误,所以日志为空,步骤报告「规则没有运行」而它运行了。 +6. `platform::fs::which()` 对一个同时是 shell 内建命令的名字返回空:`command -v true` 打印 + `true` 而不是路径,因为 shell 回答的是它自己会运行的东西。于是一个按裸名写的载荷覆盖在 + 带 /usr/bin/true 的机器上被拒为「not found on PATH」。改为对 shell 返回的裸词走一遍 PATH。 + +**一处可测量的生态效果**:`provision = "on-request"` 之后,macOS 与 Windows 的 `rules` 作业 +不再安装 `xim:vcpkg`(main 上会装)。这既是本次要的节省,也正是它暴露了第 4 处发现。 + **llvm-macos27-lab**(speak-agent,公开):`release/23.x` 的 `21ef2ddb8060`(含 `ee66426152f9`)构建的 lld 在 `xcode-27`(macOS 27.0、Xcode 27.0、SDK 27.0)上链接并 运行 C、C++23(含 `std::format`)与 `import std;`;反例以 23.1.2 原装 lld 失败并点出 diff --git a/src/testing.cppm b/src/testing.cppm index 50cb822..a834fcc 100644 --- a/src/testing.cppm +++ b/src/testing.cppm @@ -39,8 +39,15 @@ module; #include -#if !defined(_WIN32) -#include // environ -- the kit clears the real build's MCPP_XPKG_* +// The environment as an array, so the kit can clear the real build's +// MCPP_XPKG_*. Darwin does not export `environ` to anything but a main program, +// and offers `_NSGetEnviron()` instead; elsewhere the symbol is declared here +// rather than taken from , whose declaration a module purview does not +// see. +#if defined(__APPLE__) +#include +#elif !defined(_WIN32) +extern "C" char** environ; #endif export module mcpp.plugins.testing; @@ -93,6 +100,8 @@ inline constexpr std::string_view kKeys[] = { inline std::vector inherited_payload_keys() { #if defined(_WIN32) char** env = _environ; +#elif defined(__APPLE__) + char** env = *_NSGetEnviron(); #else char** env = environ; #endif From a694d16fd7d19b1949be5988abeebe06308fdea7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 1 Oct 2026 23:40:10 +0800 Subject: [PATCH 06/24] docs: the plan record carries the lab result and the ecosystem-level review --- ...1-ecosystem-build-plugin-framework-plan.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index ff4360d..7d461f4 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -136,8 +136,52 @@ consumer fixture 与 27 例 `plugin-logic` 通过;`tests/cmake-consumer` 在 **一处可测量的生态效果**:`provision = "on-request"` 之后,macOS 与 Windows 的 `rules` 作业 不再安装 `xim:vcpkg`(main 上会装)。这既是本次要的节省,也正是它暴露了第 4 处发现。 +**llvm-macos27-lab 已完结**:PR #1 已 squash 合入(`3f2cceaf2cac`),主干运行 36878561820 六个 +作业全绿,工具链资产已发布并校验:tag `toolchain-21ef2ddb8060`、 +`llvm-23.1.2-x1-macos-arm64.tar.xz`、77,959,884 字节、 +sha256 `40b9fa16be5628a5d277824f961faa33dadbf84d9520cff9fe2aeb9b0b217ebf`(下载后 `sha256sum -c` +通过)。lld 报 `LLD 23.1.3 (…21ef2ddb…)`,包含 `ee66426152f9` 与 `532fa5afbe2b`。一处与预期不同: +原装 lld 的报错词是 `unknown target` 而不是 `unknown architecture`,两者都点出 `arm64e.x1`, +负例判据因此接受两种措辞,并仍要求 `could not load TAPI file` 与链接失败。另一处事实:同一台 +`xcode-27` runner 上,原装 lld 对随附的 `MacOSX26.5.sdk` 链接正常——只有 27.x SDK 列出 +`arm64e.x1`。冷构建 lld 2033 秒(3 核 7 GB),ccache 命中后 77 秒。 + **llvm-macos27-lab**(speak-agent,公开):`release/23.x` 的 `21ef2ddb8060`(含 `ee66426152f9`)构建的 lld 在 `xcode-27`(macOS 27.0、Xcode 27.0、SDK 27.0)上链接并 运行 C、C++23(含 `std::format`)与 `import std;`;反例以 23.1.2 原装 lld 失败并点出 `arm64e.x1`,`macos-15` 对照两者皆成功。官方 23.1.2 的 macOS 包确实带 libc++ 头文件、 库与 `std` 模块源码(与 §12 R20a 的疑问相反)。 + +## 5. 生态级 review + +按「这条机制在生态的每个接缝处是否闭合」来看,而不是按仓库看。 + +**1. 链条闭合。** 引擎给出语义与接口(协议 15 的六个访问器与两条指令、三个清单键、来源记录), +L2 把它们变成一个解析器与一个工具链描述,成员只写自己的知识,消费方什么都不用写。闭合的读数有 +三处:`tests/cmake-consumer` 在 `MCPP_NO_AUTO_INSTALL=1` 下由 `build.mcpp` 点名 cmake 即可构建; +framework-lab 的 8 个用例在 ubuntu-24.04、macos-15、windows-2022 上全绿;`plugin-logic` 27 例 +覆盖 13 个成员的决定。 + +**2. 版本地板与无感升级。** 0.19.0 的地板是 2026.10.1.3。两个方向都量过:新插件在旧引擎上, +2026.10.1.2 只说 `unknown key 'provision'`,不提版本——因为地板检查需要那次解析没能产出的文档; +这成了第 7 处修复(解析失败路径从文件文本读地板)。旧插件在新引擎上,默认路径的输出与行为逐字 +不变,由既有 e2e 与 framework-lab 的 `default` 用例断言。 + +**3. 默认代价的真实变化。** `provision = "on-request"` 之后,plugins CI 的 macOS 与 Windows +`rules` 作业日志里 `xim:vcpkg` 出现 0 次(main 上这两个作业会 provisioning +`xim:vcpkg@>=2026.7.27`)。节省是真的,而它也正是第 4 处发现的来源:少装一个载荷,会让依赖 +「装过」的测试环境露出假设。 + +**4. 可观察性是一条路径,不是一个开关。** 一个显式来源 = 一行 `Using`(类与出处)+ 一条 +`Finished` 汇总项 + `resolution.json` 的一条 `sources` 记录 + `mcpp why` 的一段。四处读的是同一条 +`SourceDecision`,所以不可能三处一致、一处落后。 + +**5. 新增的风险,按发生的那次记。** (a) 构建程序的环境是「在构建中跑的测试宿主」的契约的一部分: +引擎多设一个变量,就可能改变一个这样的宿主的答案(第 4 处发现);(b) 命令长度是个有名字的通道, +而宿主模块数量是它的无界载荷(第 3 处发现);(c) 一个按路径命名的工具链把产物与机器状态绑在一起, +所以身份按内容而不是按版本,并写进 stamp 供快速路径比较。 + +**6. 没做的事,以及为什么不做。** 工具链描述数据化(核心读描述文件)——`[toolchain] { path }` +的字段已经与那份描述同形,所以它是后续的第三种载体,而不是改写;`mcpp.lock` 不记工具链——lock 的 +内容是依赖解析的结果;`kind = "bin"` 的宿主工具仍走 `[tools.overrides]`——键空间与语义不同, +合并会让一张表有两种键。 From 11cf1cf02f9118d1279bc15113c36ab375393d52 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 1 Oct 2026 23:43:22 +0800 Subject: [PATCH 07/24] docs: two usage examples name the current version, and dist-apk says 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. --- docs/dist-apk.md | 14 +++++++++----- docs/engine-and-rules.md | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/dist-apk.md b/docs/dist-apk.md index d9ba284..ea2323c 100644 --- a/docs/dist-apk.md +++ b/docs/dist-apk.md @@ -27,13 +27,17 @@ Both features imply `dist-apk`, so a project names them in its place: ```toml [build-dependencies.mcpp] -plugins = { version = "0.11.0", features = ["dist-apk-kotlin", "dist-apk-maven"], host-module = true } +plugins = { version = "0.19.0", features = ["dist-apk-kotlin", "dist-apk-maven"], host-module = true } ``` -They are features and not payloads of `dist-apk` because provisioning runs -before the build program says what it compiles. A payload of `dist-apk` would be -installed for every Android consumer, and the Kotlin compiler is 90 MB. Kotlin -sources without the feature are refused, naming it. +They are features and not payloads of `dist-apk` because a feature is what a +project selects, and selecting is the statement: a payload of `dist-apk` would +belong to every Android consumer, and the Kotlin compiler is 90 MB. A project +that selects `dist-apk-kotlin` compiles Kotlin, so `xim:kotlin` is installed with +the feature; `xim:bundletool`, which only `--format aab` uses, is asked for while +that bundle is planned (`provision = "on-request"`, 0.19.0), so an ordinary +`--format apk` build installs nothing for it. Kotlin sources without the feature +are refused, naming it. **An ordinary build does not reach the network.** A Maven graph is resolved and downloaded only when the developer asks: diff --git a/docs/engine-and-rules.md b/docs/engine-and-rules.md index d442c43..28d25d5 100644 --- a/docs/engine-and-rules.md +++ b/docs/engine-and-rules.md @@ -41,7 +41,7 @@ A project names the rule and nothing else: ```toml [build-dependencies.mcpp] -plugins = { version = "0.8.0", features = ["rules-cuda"], host-module = true } +plugins = { version = "0.19.0", features = ["rules-cuda"], host-module = true } ``` The payloads each rule drives are declared **here**, under the feature that From e48c4814228878eb84cef83c0047983c4425d46a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 1 Oct 2026 23:57:39 +0800 Subject: [PATCH 08/24] rules-sycl: the compiler and the root its libraries sit under are two 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 `/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. --- rules/sycl.cppm | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rules/sycl.cppm b/rules/sycl.cppm index 61e4116..38880e3 100644 --- a/rules/sycl.cppm +++ b/rules/sycl.cppm @@ -352,7 +352,13 @@ inline std::vector plan(std::span sources, options opt const auto dpcppTool = mcpp::plugins::tool::resolve(dpcppSpec, opt.compiler); // Asked for: the engine installs `xim:dpcpp` and runs this program again. if (dpcppTool.pending()) return out; - const std::string dpcpp = dpcppTool.program; + // TWO VALUES, NOT ONE: the compiler to run, and the root its libraries sit + // under. They were one string while this rule took the payload directory and + // appended `/bin/clang++`; a resolver answers the program, and `-L` still + // needs the root (a build of the SYCL consumer failed with + // `unable to find library -lsycl` when the root was the program's path). + const std::string dpcpp = dpcppTool.program; + const std::string dpcppRoot = dpcppTool.root; const std::string gcc = payload("gcc"); const std::string cuda = tg.cuda_archs.empty() ? std::string{} : payload("cuda-nvcc"); // THE C LIBRARY IS THE SAME QUESTION AS THE C++ ONE, ONE LAYER DOWN. @@ -466,7 +472,7 @@ inline std::vector plan(std::span sources, options opt // The link line gets its directories from here, not from the manifest: the // rule resolved the payload, so the rule names where its libraries are. - mcpp::link_search((dpcpp + "/lib").c_str()); + mcpp::link_search((dpcppRoot + "/lib").c_str()); mcpp::link_lib("sycl"); // See the file header for why this is not `-lstdc++`. The reason is a // Linux one: two C++ runtimes cannot share a process, and on Windows there From 358841d3af479cb74c6bd4995b045cca3ac57877 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 1 Oct 2026 23:59:00 +0800 Subject: [PATCH 09/24] docs: the plan record carries the sycl finding and why the local round missed it --- ...26-10-01-ecosystem-build-plugin-framework-plan.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index 7d461f4..04b1f1e 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -133,6 +133,18 @@ consumer fixture 与 27 例 `plugin-logic` 通过;`tests/cmake-consumer` 在 `true` 而不是路径,因为 shell 回答的是它自己会运行的东西。于是一个按裸名写的载荷覆盖在 带 /usr/bin/true 的机器上被拒为「not found on PATH」。改为对 shell 返回的裸词走一遍 PATH。 +7. `rules-sycl` 的迁移把「载荷目录」与「要运行的程序」当成了一个字符串。此前 + `dpcpp = payload("dpcpp")` 是目录,代码在两处分别接上 `/bin/clang++` 与 `/lib`;解析器 + 回答的是程序,于是 `-L` 指向 `/bin/clang++/lib`,SYCL 消费方在 `compat.opencl` + 处以 `ld.lld: error: unable to find library -lsycl` 失败。改为两个值,并按同一类错误 + 审计了全部迁移成员(toolkit 类取 `.root`,工具类取 `.program`,没有第二处把目录接在 + 程序路径后)。**本机没装 dpcpp(208 MB),所以本机的 fixture 轮次跳过了这个用例—— + 这正是它只能由 CI 发现的原因。**修后本机复现通过:`tests/sycl-consumer` 构建成功, + `compat.opencl` 正常编译。 +8. GNU 响应文件要双写反斜杠,而不是套单引号。LLVM 的 GNU tokenizer 在引号**内**也把反斜杠 + 当转义(与 POSIX shell 不同),所以第一版修法无效。clang 22.1.8 实测:响应文件写 + `'-DX=a\b'` 得到 `X=ab`,写 `-DX=a\\b` 得到 `X=a\b`。 + **一处可测量的生态效果**:`provision = "on-request"` 之后,macOS 与 Windows 的 `rules` 作业 不再安装 `xim:vcpkg`(main 上会装)。这既是本次要的节省,也正是它暴露了第 4 处发现。 From 789a0bb36c10e3b9a41c15de899cb64ed5024e10 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 00:07:33 +0800 Subject: [PATCH 10/24] docs: the toolchain builders listed are the ones the module has 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. --- docs/plugin-development.md | 6 +++--- src/toolchain.cppm | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 294f0c4..8a9a0b4 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -101,9 +101,9 @@ build reports. **Stating the build toolchain.** `mcpp.plugins.toolchain` (feature `plugins-toolchain`) builds the statement a root build program makes in its toolchain phase, for a project whose `[toolchain]` says -`configure = "build.mcpp"`: `layout`, `prefixed`, `compose`, -`from_env_script`, `with_launcher`, `with_sysroot`, `with_tool`, `managed`, -`env`, and `configure(fn)` / `use(d)`. +`configure = "build.mcpp"`: `layout`, `prefixed`, `managed`, `from_env_script`, +`newest_under`, `env`, the `with_launcher` / `with_sysroot` / `with_family` / +`with_tool` refinements, and `configure(fn)` / `use(d)`. **Testing a plugin.** `mcpp.plugins.testing` runs a plugin function in a child process against a stated build context (`row::windows_visual_studio()`, diff --git a/src/toolchain.cppm b/src/toolchain.cppm index 928bd13..077c3e9 100644 --- a/src/toolchain.cppm +++ b/src/toolchain.cppm @@ -29,7 +29,8 @@ // and the engine reads both in one place, so a toolchain stated here and one // named in the manifest behave alike. What this module adds are the builders // a manifest cannot express: a value read from the environment, a tree found -// by looking, a vendor SDK's environment script, a toolchain composed of parts. +// by looking, a vendor SDK's environment script, and a tree refined part by part +// (a launcher, a sysroot, a tool named by role). export module mcpp.plugins.toolchain; From 8a7d6049b240478d438146e406770a291da3940e Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 00:14:42 +0800 Subject: [PATCH 11/24] tool: a stated program path resolves whichever executable suffix the 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. --- src/tool.cppm | 26 +++++++++++++++++++++++--- tests/plugin-logic/build.mcpp | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/tool.cppm b/src/tool.cppm index 7490fa4..8cfa831 100644 --- a/src/tool.cppm +++ b/src/tool.cppm @@ -73,6 +73,26 @@ inline std::string program_in(const std::filesystem::path& dir, std::string_view return {}; } +// A STATED PATH, WITH THE SAME SUFFIX RULE AS A DISCOVERED ONE. 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`; process creation there +// appends the suffix itself, so a resolver that insisted on the exact spelling +// refused a program the host would have run (measured in CI: the cmake consumer +// reported `options::cmake = "C:/Program Files/CMake/bin/cmake" (not found)` on a +// runner carrying cmake). Empty when neither spelling is a file. +inline std::string program_at(const std::filesystem::path& p) { + if (is_file(p)) return generic(p); + auto with = p; + with += ".exe"; + if (is_file(with)) return generic(with); + if (p.extension() == ".exe") { + auto without = p; + without.replace_extension(); + if (is_file(without)) return generic(without); + } + return {}; +} + inline std::string find_on_path(std::string_view name) { const char* path = std::getenv("PATH"); if (!path) return {}; @@ -226,9 +246,9 @@ inline found resolve(const spec& s, const choice& c = {}) { record(from::choice, file, line); return out; } - } else if (detail::is_file(p)) { - out.program = detail::generic(p); - out.root = detail::root_of(p); + } else if (auto hit = detail::program_at(p); !hit.empty()) { + out.program = std::move(hit); + out.root = detail::root_of(out.program); record(from::choice, file, line); return out; } diff --git a/tests/plugin-logic/build.mcpp b/tests/plugin-logic/build.mcpp index 4378805..9d029a6 100644 --- a/tests/plugin-logic/build.mcpp +++ b/tests/plugin-logic/build.mcpp @@ -479,6 +479,27 @@ int main(int argc, char** argv) { "the decision names the choice and the program"); } }, + { "tool: a stated path resolves whichever executable suffix the file carries", + // What a shell hands a build program on Windows: `command -v cmake` + // answers a path with no `.exe`, for a `cmake.exe`. Process creation + // there appends the suffix, so the resolver does too -- stated on a + // Linux row, because the rule is the same on every host. + t::row::linux_libcxx().file("pkg/opt/bin/cmake.exe") + .xpkg_source("xim", "cmake", "pending"), + [] { + const auto f = tool_resolve(mcpp::plugins::tool::program( + std::string(mcpp::manifest_dir()) + "/opt/bin/cmake")); + std::printf("source=%s program=%s\n", + std::string(mcpp::plugins::tool::name(f.source)).c_str(), + f.program.c_str()); + return f ? 0 : 1; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("source=choice", "opt/bin/cmake.exe"), + "the file beside the stated path answers"); + c.expect(!r.has_line("mcpp:xpkg-request="), "a choice requests no payload"); + } }, + { "tool: an override answers, and the payload is not consulted", t::row::linux_libcxx().file("opt/bin/cmake") .xpkg("xim", "cmake", "{root}/opt") From be3dfbdb9c93f5fe7c536b3b6af224b59c9a7855 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 00:20:11 +0800 Subject: [PATCH 12/24] manifest: the test kit's comment sits above the test kit again Inserting the toolchain feature put it between that comment and the feature it describes. --- mcpp.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mcpp.toml b/mcpp.toml index 4fb130c..013c2d4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -92,9 +92,6 @@ default = [] sources = ["src/declare.cppm", "src/toolset.cppm", "src/fs.cppm", "src/tool.cppm", "src/compat/detected_toolset.cppm"] -# The test kit: a plugin function runs against a stated build context, and its -# directives are compared. Separate from `plugins-core`, so that no build -# program compiles it unless it asks. # `mcpp.plugins.toolchain` -- the builders a root build program states its # build toolchain with, in the toolchain phase of a project whose `[toolchain]` # says `configure = "build.mcpp"` (mcpp#755). Its own feature rather than part @@ -103,6 +100,9 @@ sources = ["src/declare.cppm", "src/toolset.cppm", "src/fs.cppm", "src/tool.cppm sources = ["src/toolchain.cppm"] implies = ["plugins-core"] +# The test kit: a plugin function runs against a stated build context, and its +# directives are compared. Separate from `plugins-core`, so that no build +# program compiles it unless it asks. [features.plugins-testing] sources = ["src/testing.cppm"] implies = ["plugins-core"] From 10a866295a2300fc934ce31b0a6a861bce0f6eb3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 00:23:26 +0800 Subject: [PATCH 13/24] docs: the plan record carries the linker and executable-suffix findings --- ...26-10-01-ecosystem-build-plugin-framework-plan.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index 04b1f1e..5817ad4 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -145,6 +145,18 @@ consumer fixture 与 27 例 `plugin-logic` 通过;`tests/cmake-consumer` 在 当转义(与 POSIX shell 不同),所以第一版修法无效。clang 22.1.8 实测:响应文件写 `'-DX=a\b'` 得到 `X=ab`,写 `-DX=a\\b` 得到 `X=a\b`。 +9. 被陈述的链接器只在 Linux 的 clang 分支进入链接(`--ld-path` 写在那一支里)。macOS 的 + Apple 链接形状拿不到它:工具进了指纹(改 wrapper 会让快速路径失效),却不参与链接, + 而且什么都不说。由 toolchain-lab 在 macos-15 上量到(`build.ninja` 里没有 `--ld-path`)。 + 现在在所有形状之后追加一次;gcc 的树陈述 `ld` 改为在声明处拒绝,并补了反例用例。 + **引擎自己的 e2e 875 断言了这条,但在没装 llvm 载荷的宿主上 SKIP,而 macOS CI 正是这样的 + 宿主——这就是 lab 存在的理由。** +10. 一个被陈述的程序路径必须按宿主自己的规则补可执行后缀。Windows 上 `command -v cmake` + 回答 `C:/Program Files/CMake/bin/cmake`(对应 `cmake.exe`),而进程创建会自己补 `.exe`; + 解析器坚持原样拼写,于是拒绝了宿主本可以运行的程序(CI 实测:cmake 消费方报 + `options::cmake = "C:/Program Files/CMake/bin/cmake" (not found)`,随后因为子工程没有配置 + 而编译失败)。L2 与引擎的覆盖路径现在用同一条规则,各补一个用例。 + **一处可测量的生态效果**:`provision = "on-request"` 之后,macOS 与 Windows 的 `rules` 作业 不再安装 `xim:vcpkg`(main 上会装)。这既是本次要的节省,也正是它暴露了第 4 处发现。 From 87c7f9deeea03ffd4e5f2c7a7e2dd98b43f885fe Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 00:31:54 +0800 Subject: [PATCH 14/24] docs: the plan record carries the framework lab's results and the measured saving --- ...1-ecosystem-build-plugin-framework-plan.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index 5817ad4..499d67d 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -160,6 +160,26 @@ consumer fixture 与 27 例 `plugin-logic` 通过;`tests/cmake-consumer` 在 **一处可测量的生态效果**:`provision = "on-request"` 之后,macOS 与 Windows 的 `rules` 作业 不再安装 `xim:vcpkg`(main 上会装)。这既是本次要的节省,也正是它暴露了第 4 处发现。 +**mcpp-framework-lab 已完结**:PR #1 squash 合入 `90c630f1`,10 个用例 × 3 平台 = 29 通过、 +1 跳过(`override-bare-name` 在 Windows 跳过:shell 内建是 POSIX 的概念,而引擎在 Windows 用 +`where`,它只报告程序——引擎自己的用例同理跳过)。用例:control、choice-build-mcpp、 +override-env、override-manifest、override-from-dependency、override-bare-name、managed-only、 +why、timing、default。除判据外还加了一条:从 `CMakeCache.txt` 读 `CMAKE_COMMAND`,确认真正 +运行的 cmake 就是被陈述的那一个。 + +**「下载耗时减少多少」的读数**(run 36888453587,全部冷测:`xim:cmake` 事先未安装,control +自己的日志里有 `Downloading xim:cmake`): + +| 平台 | control(秒) | 点名宿主 cmake(秒) | 差 | xim:cmake 载荷 | +|---|---|---|---|---| +| ubuntu-24.04 | 29.5 | 6.4 / 6.3 | 23.2 | 61.9 MB 下载 3.5 s,安装后 207 MB | +| macos-15 | 19.6 | 5.4 / 4.3 | 14.8 | 85.9 MB 下载 9.4 s,安装后 265 MB | +| windows-2022 | 20.0 | 5.8 / 5.7 | 14.2 | 51.9 MB 下载 10.0 s,安装后 152 MB | + +**省下的是什么**:第四次构建(control,但载荷已安装)为 6.4 / 5.0 / 5.7 秒,与点名宿主 cmake +基本相同。所以省下的是把载荷供给进一个 `MCPP_HOME` 的一次性成本,不是每次构建的成本——这正是 +`provision = "on-request"` 要省的那一项,也说明它对已有缓存的 CI 不会再省第二次。 + **llvm-macos27-lab 已完结**:PR #1 已 squash 合入(`3f2cceaf2cac`),主干运行 36878561820 六个 作业全绿,工具链资产已发布并校验:tag `toolchain-21ef2ddb8060`、 `llvm-23.1.2-x1-macos-arm64.tar.xz`、77,959,884 字节、 From 141f416ea777b2a74a88c216c7682d3f5f7175da Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 00:45:00 +0800 Subject: [PATCH 15/24] docs: the plan record carries the toolchain lab's before-and-after reading --- ...2026-10-01-ecosystem-build-plugin-framework-plan.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index 499d67d..61ec155 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -160,6 +160,16 @@ consumer fixture 与 27 例 `plugin-logic` 通过;`tests/cmake-consumer` 在 **一处可测量的生态效果**:`provision = "on-request"` 之后,macOS 与 Windows 的 `rules` 作业 不再安装 `xim:vcpkg`(main 上会装)。这既是本次要的节省,也正是它暴露了第 4 处发现。 +**mcpp-toolchain-lab 已完结**:PR #1 已合入,`main` 在 `3d33c26a`。八个用例(path-llvm、 +env-path、launcher-and-ld、fast-path、toolchain-phase、phase-refuses-a-flag、managed-only、 +lock-local)在 linux 与 macos-15 全部通过;xcode-27 除已知红的 `toolchain-phase`(#669)外通过。 +它给出了第 9 处发现的前后读数:同一个 `launcher-and-ld` 用例,在 `77b632fd` 上 +「the linker wrapper ran during the link: no」,在 `cb918615` 与 `136eb2a7` 上为 yes, +`build.ninja` 的 ldflags 末尾出现 +`--ld-path=/Users/runner/work/_temp/lab-work/launcher-and-ld/ld-wrapper`,链接出的程序运行并打印 +`toolchain-lab: sum=6 count=3 greeting=hello 42`。`lock-local` 按引擎的实际行为改写:lock 里有 +解析出的依赖、没有工具链。 + **mcpp-framework-lab 已完结**:PR #1 squash 合入 `90c630f1`,10 个用例 × 3 平台 = 29 通过、 1 跳过(`override-bare-name` 在 Windows 跳过:shell 内建是 POSIX 的概念,而引擎在 Windows 用 `where`,它只报告程序——引擎自己的用例同理跳过)。用例:control、choice-build-mcpp、 From 5eb083b5fb39355c00bd5232f9e37811b7209b6a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 01:01:47 +0800 Subject: [PATCH 16/24] rules-qt: warn() walks the message by index, because gcc 16 refuses to 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. --- rules/qt.cppm | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/rules/qt.cppm b/rules/qt.cppm index 351dcba..e6e3e72 100644 --- a/rules/qt.cppm +++ b/rules/qt.cppm @@ -129,11 +129,26 @@ inline std::filesystem::path absolute_from_root(const std::string& p) { return path.lexically_normal(); } +// INDEXED, NOT A RANGE-FOR, FOR A COMPILER REASON (gcc 16.1.0, 0.19.0). 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 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. An index touches no iterator, so it does not depend on what a BMI +// carries across a module boundary. The same hazard class is on record in the +// engine (a clang 20.1.7 crash from an exported `std::pair` specialization): the +// error names a file the change never touched. inline void warn(const std::string& message) { std::cerr << message << '\n'; std::string folded; bool space = false; - for (char c : message) { + for (std::size_t i = 0; i < message.size(); ++i) { + const char c = message[i]; if (c == '\n') { space = true; continue; } if (space) { if (c == ' ') continue; folded += ' '; space = false; } folded += c; From 40a5d21ec2fb097e50c1d7e2a56be3f25fec1a60 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 01:02:24 +0800 Subject: [PATCH 17/24] docs: the plan record carries the gcc 16 inlining finding --- .../2026-10-01-ecosystem-build-plugin-framework-plan.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index 61ec155..62b3eef 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -157,6 +157,14 @@ consumer fixture 与 27 例 `plugin-logic` 通过;`tests/cmake-consumer` 在 `options::cmake = "C:/Program Files/CMake/bin/cmake" (not found)`,随后因为子工程没有配置 而编译失败)。L2 与引擎的覆盖路径现在用同一条规则,各补一个用例。 +11. gcc 16.1.0 拒绝在 `warn@mcpp.rules.qt` 里内联 `std` 模块带来的 + `__gnu_cxx::__normal_iterator::operator*` 与 `operator++`——两者是 `always_inline`, + 而这个文件多 import 了一个模块之后就报 `inlining failed in call to 'always_inline'`, + 点名的却是 `bits/stl_iterator.h`。集合里其他 range-for 都正常。改为按下标遍历(不碰迭代器, + 因此不依赖 BMI 跨模块边界带了什么)。本机以 `MCPP_TOOLCHAIN=gcc@16.1.0` 两次测量:修前 2 个 + 错误,修后 0 个,qt 消费方构建通过。与引擎记下的 clang 20.1.7 崩溃同属一类:报错点名的文件 + 与改动无关。 + **一处可测量的生态效果**:`provision = "on-request"` 之后,macOS 与 Windows 的 `rules` 作业 不再安装 `xim:vcpkg`(main 上会装)。这既是本次要的节省,也正是它暴露了第 4 处发现。 From e00da7325b089942447b46d7548779691e339bfa Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 03:09:28 +0800 Subject: [PATCH 18/24] ci: the bootstrap pin is the released 2026.10.1.3, which is this collection'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. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b13fb73..624208e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ env: # # It moves to `MCPP_VERSION` once that release exists, so the ordinary path # runs the engine this collection states. - MCPP_BOOTSTRAP: 2026.10.1.2 + MCPP_BOOTSTRAP: 2026.10.1.3 # AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE. # # Empty on every push and pull request, so the steps run the release above. From bbf0f50390efdb9a864750f155147a04121ef9c1 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 03:51:30 +0800 Subject: [PATCH 19/24] docs: a project that names its own Qt SDK states where the payload comes 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. --- docs/rules-qt.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/rules-qt.md b/docs/rules-qt.md index e95ab0e..dca5f1e 100644 --- a/docs/rules-qt.md +++ b/docs/rules-qt.md @@ -47,7 +47,14 @@ The first of three levels that names an SDK decides, and the rule records which Each payload carries its runtime closure: the loader and the libraries Qt loads on Linux, and the VC++ runtime on Windows x64. 0.15.0 removed the features `rules-qt-xim`, `rules-qt-xim-base` and `rules-qt-xim-addons`, which declared a payload at a fixed version. A feature states a mechanism; the SDK a program links is the project's choice. A project that used one of those features declares the payload instead, as above. -A project that does not use a payload comments out its declaration, because `mcpp build` provisions every declared payload whether or not a higher level names another SDK. It then names its SDK with `QT_ROOT_DIR` or with `options::root` in `build.mcpp`: +A project that names its SDK at a higher level states where the payload comes from, and it is then not provisioned (mcpp 2026.10.1.3): + +```toml +[xlings.overrides] +"xim:qt-base" = { root = "/opt/Qt/6.11.1/gcc_64" } +``` + +The declaration stays, so a machine without that directory still gets the ecosystem's SDK, and the build reports which one it used. Before that release a declared payload was provisioned whether or not a higher level named another SDK, and the only way out was to comment the declaration out: ```toml [target.'cfg(any(windows, linux, macos))'.xlings.workspace] From 14ff15c52cf1045dfa9028460c331eb6ae0417e1 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 04:07:14 +0800 Subject: [PATCH 20/24] docs: where a tool comes from, as scenarios for authors, designers and 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. --- README.md | 5 +- docs/plugin-development.md | 5 + docs/tool-sources.md | 374 +++++++++++++++++++++++++++++++++++++ 3 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 docs/tool-sources.md diff --git a/README.md b/README.md index 6e146f4..92a4ff5 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,10 @@ The second segments `core`, `plugins`, `deps`, `rules`, `dist` and `tools` of `mcpp.` belong to this package and the engine; a third-party plugin names its modules `mcpp..*` ([docs/plugin-development.md](docs/plugin-development.md#3-layers-names-and-the-engine-floor)). The full account of the families, and of how the engine routes a file to a -rule, is in [docs/engine-and-rules.md](docs/engine-and-rules.md). +rule, is in [docs/engine-and-rules.md](docs/engine-and-rules.md). Where the tool a +member runs comes from -- what a project can name, what a plugin must declare for +naming it to avoid a download, and the scenarios on both sides -- is in +[docs/tool-sources.md](docs/tool-sources.md). ## Members diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 8a9a0b4..285b98e 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -98,6 +98,11 @@ An option field is a `tool::choice`, which a string constructs, so `o.cmake = "/usr/bin/cmake"` keeps working and the line that wrote it is what a build reports. +A choice alone does not prevent a download: an eagerly declared payload is +provisioned before any build program runs. The pair that makes "named here, not +downloaded" true, the scenarios on both sides of it, and how each official member +declares its tools are in [tool-sources.md](tool-sources.md). + **Stating the build toolchain.** `mcpp.plugins.toolchain` (feature `plugins-toolchain`) builds the statement a root build program makes in its toolchain phase, for a project whose `[toolchain]` says diff --git a/docs/tool-sources.md b/docs/tool-sources.md new file mode 100644 index 0000000..a6d912e --- /dev/null +++ b/docs/tool-sources.md @@ -0,0 +1,374 @@ +# Where a tool comes from + +A member that runs a program — cmake, vcpkg, glslangValidator, nvcc, appimagetool +— answers one question before it can plan anything: which program. This page is +the account of that question for the three people it concerns: whoever writes a +plugin, whoever designs one, and whoever uses one. It is written as scenarios, +because each audience meets the mechanism from a different side. + +The mechanism is mcpp 2026.10.1.3 and `mcpp.plugins.tool` (0.19.0). The engine's +own account is mcpp `docs/23-the-project-environment.md`; the design record is +`.agents/docs/2026-10-01-ecosystem-build-plugin-framework-design.md`. + +## 1. The four places an answer can come from + +``` +1. the build program's choice o.cmake = "/usr/bin/cmake" + o.cmake = tool::root("/opt/cmake") + o.cmake = tool::on_path() +2. the member's legacy variable MCPP_SLANGC, MCPP_GLSLC (kept, never extended) +3. the engine's override [xlings.overrides] in the root or workspace manifest + MCPP_XLINGS_OVERRIDE__ + ~/.mcpp/config.toml +4. the declared payload installed with the feature, or declared + `provision = "on-request"` and asked for here +``` + +Two rules hold the whole mechanism together. + +**A choice never asks for the payload.** The first level that answers returns, +and only the fourth calls `mcpp::xpkg_request`. That is what makes "the build +program names its own tool" mean "the payload is not downloaded"; it is a +property of the code path, not a convention. + +**The environment outranks the manifest, and the manifest outranks the machine.** +CI and distribution packaging have to replace a tool without editing the +manifest, and a project's statement is more specific than a fact about one +machine. Every level names itself in the output, so an environment variable +quietly displacing a project's decision is not possible. + +An override is read from the root manifest only — or the workspace manifest, when +a member is built. A dependency that writes the table is refused and named: +**which payloads a package needs is its own statement; where they come from is the +project's.** + +## 2. Five classes, and why the output distinguishes them + +| class | meaning | how a build reports it | +|---|---|---| +| `managed` | the ecosystem resolved a range and installed it | exactly as before this mechanism existed | +| `pinned` | the ecosystem installed it, at a stated version | the same | +| `custom` | a person or a machine stated where it comes from, with a version | `Using … [custom · mcpp.toml:22]`, bright cyan | +| `program` | the build program named the program | `Using cmake (mcpp.deps.cmake) ← /usr/bin/cmake [program · build.mcpp:9]` | +| `host` | stated without a version (a bare name on PATH) | `[host · PATH, version not stated]`, yellow verb | + +The line the classes draw is single: **did the ecosystem choose, or did a person +or a machine choose.** `managed` and `pinned` are together the default, and the +default's output is unchanged — that is the criterion for a seamless upgrade, and +the engine's e2e cases and the framework lab assert it. + +`host` is its own class because it binds the artifact to the machine's state. The +test is whether a version was stated, not whether the path looks like a system +directory, which would be a guess. + +## 3. For whoever uses a plugin + +### 3.1 Write nothing + +```toml +[build-dependencies.mcpp] +plugins = { version = "0.19.0", features = ["deps-cmake"], host-module = true } +``` + +The member resolves nothing stated, reaches the fourth level, finds `xim:cmake` +declared on request and not installed, asks for it, and the engine installs it and +runs the program again. The output is byte-for-byte what it was before this +mechanism existed. + +One difference from 0.18.1: **a payload declared and not used is not installed.** +A build that never reaches the member — because it compiles only its own modules, +or because `--format` took another branch — downloads nothing for it. + +### 3.2 Name the tool the machine already has + +```cpp +// build.mcpp +mcpp::deps::cmake::options o; +o.cmake = "/usr/bin/cmake"; // or tool::on_path(), or tool::root("/opt/cmake") +``` + +``` + Using cmake (mcpp.deps.cmake) ← /usr/bin/cmake [program · build.mcpp:9] + Finished dev [unoptimized + debuginfo] in 6.4s · program: cmake (mcpp.deps.cmake) +``` + +Measured in `speak-agent/mcpp-framework-lab`, cold on three runners, against a +control that provisions the payload: + +| platform | control | the host's cmake named | difference | `xim:cmake` | +|---|---|---|---|---| +| ubuntu-24.04 | 29.5 s | 6.4 s | 23.2 s | 61.9 MB downloaded, 207 MB installed | +| macos-15 | 19.6 s | 5.4 s | 14.8 s | 85.9 MB, 265 MB | +| windows-2022 | 20.0 s | 5.8 s | 14.2 s | 51.9 MB, 152 MB | + +**What is saved is the one-time cost of provisioning into an `MCPP_HOME`, not a +per-build cost.** A fourth build — the control with the payload already installed +— took 6.4, 5.0 and 5.7 s, which is what naming the host's cmake costs. A CI with +a warm payload cache does not save this twice. + +### 3.3 State it in the manifest instead of the build program + +```toml +[xlings.overrides] +"xim:cmake" = "/usr/bin/cmake" # a path is a program +"xim:vcpkg" = { root = "/opt/vcpkg" } # a tree +"xim:slang" = { program = "slangc", version = "2026.14.1" } # checked against every requirement +``` + +This path needs no support from the member: the engine removes the payload from +the provisioning set, `mcpp::xpkg_dir` answers the root the override implies, and +`mcpp::xpkg_source` answers `override`. **A plugin that has not migrated to +`mcpp.plugins.tool` benefits from it unchanged**, as long as it reads its +directory from `mcpp::xpkg_dir`. + +A stated `version` is compared with every requirement a package of the graph +made, and a version below one is refused naming both sides. Without a `version` +nothing can be compared, and the build records a note naming the requirement that +went unchecked. The engine never runs a program to ask its version: every tool +spells `--version` differently, and that is the plugin's knowledge. + +### 3.4 Replace a tool for one CI job + +```bash +MCPP_XLINGS_OVERRIDE_XIM_VCPKG=/opt/vcpkg/vcpkg mcpp build +MCPP_XLINGS_OVERRIDE_XIM_CMAKE=path:cmake mcpp build # looked up on PATH: the host class +``` + +### 3.5 Share one tool between every project on a machine + +```toml +# ~/.mcpp/config.toml — a fact about this machine, not in the repository +[xlings.overrides] +"xim:vcpkg" = { root = "/opt/vcpkg" } +``` + +### 3.6 Build without the network, and audit what a build used + +```bash +MCPP_NO_AUTO_INSTALL=1 mcpp build # refused, naming what it would have installed +mcpp build --managed-only # refuses any source that is not the ecosystem's +mcpp why payload xim:cmake # what was consulted, and what each answered +``` + +`mcpp why` reads the record the build wrote, so it answers on a machine that +already holds the payload: + +``` +sources: + payload:xim:cmake /usr/bin/cmake + program · build.mcpp:9 for cmake-consumer + considered: options::cmake = "/usr/bin/cmake"; payload xim:cmake (not requested by this build) +``` + +### 3.7 Bring a whole toolchain + +A toolchain is the same question one layer down, and it is answered in the +manifest or by the root build program, never by a plugin: + +```toml +[toolchain] +default = { path = "/opt/llvm-trunk" } # a tree this machine already has +# or +default = { configure = "build.mcpp" } # the build program states it +bootstrap = "llvm@22.1.8" # the one that compiles build programs +``` + +`mcpp.plugins.toolchain` (feature `plugins-toolchain`) builds that statement: +`layout`, `prefixed`, `managed`, `from_env_script` (a vendor SDK's +`environment-setup-*`), `newest_under`, `env`, the `with_launcher` / +`with_sysroot` / `with_family` / `with_tool` refinements, and `configure(fn)` / +`use(d)`. + +## 4. For whoever writes a plugin + +### 4.1 A member that runs a program + +Three pieces, written once per tool: + +```cpp +// 1. the option, as a choice -- a string still assigns to it +struct options { + mcpp::plugins::tool::choice cmake; +}; + +// 2. the spec: this member's own knowledge, and nothing else +inline mcpp::plugins::tool::spec cmake_spec(std::string who) { + return { .who = std::move(who), .package = "cmake", .programs = {"cmake"}, + .bin_dirs = {"bin", "CMake.app/Contents/bin"}, .option = "options::cmake" }; +} + +// 3. one resolve, one refusal +auto t = mcpp::plugins::tool::resolve(cmake_spec("mcpp.deps.cmake"), opt.cmake); +if (t.pending()) return true; // asked for; the engine runs this program again +if (!t) { warn(mcpp::plugins::tool::describe_missing(cmake_spec("mcpp.deps.cmake"), t)); + return false; } +run(t.program); +``` + +`choice` is constructible from a string, so a member migrating from +`std::string` costs its users nothing: `o.cmake = "/usr/bin/cmake"` still +compiles, and the line that wrote it reaches the build's output and +`mcpp why tool cmake`. + +`describe_missing` is the one refusal text, and it lists every way to name the +tool. A member does not write its own. + +### 4.2 The pair that makes "named here, not downloaded" true + +A `tool::choice` alone is not enough. Provisioning of an eagerly declared payload +happens in prepare, **before any build program runs**, so a payload declared +plainly is installed even when the build program goes on to name another program. +Nothing written in `build.mcpp` can undo it. That was the original symptom behind +mcpp#755. + +The manifest half: + +```toml +[feature-xlings.deps-cmake] +"xim:cmake" = { version = ">=3.31", provision = "on-request" } +``` + +So the author's decision is one question: + +| when the member needs the tool | how to declare it | after a user names their own | +|---|---|---| +| while **planning** (it runs cmake, vcpkg, appimagetool, bundletool) | `provision = "on-request"` | nothing is downloaded | +| **to plan at all** (it reads a version out of the toolkit's headers, an SDK file, an API level from a platform directory) | plainly, which is eager | still downloaded; the user skips it with `[xlings.overrides]` | + +`rules-cuda`, `rules-sycl`, `rules-qt` and `dist-apk` carry their main payloads +the second way, and that is timing, not an oversight: moving provisioning after +the build program would make them fail to plan. + +### 4.3 Ask for a payload only on the branch that uses it + +`dist-apk` declares five payloads for every Android build and one more only for +`--format aab`: + +```toml +"xim:bundletool" = { version = "1.18.3", provision = "on-request" } +``` + +The member requests it while planning a bundle, so an ordinary `--format apk` +build installs nothing for it. The engine installs every request of one +invocation together and runs only the programs that asked; the asking run is +discarded before its directives are applied, so there is no half-applied state. +Three rounds at most, and a program that asks for something new in the third is +refused by name. + +### 4.4 A tool that is a tree, not a program + +```cpp +auto f = mcpp::plugins::tool::resolve(nvcc_spec(), opt.toolkit); +t.nvcc_root = t.cudart_root = t.crt_root = t.curand_root = t.cccl_root = f.root; +``` + +Take `.root` for a toolkit and `.program` for a program. The two were one string +in `rules-sycl` while it took the payload directory and appended +`/bin/clang++`; after the migration `-L` pointed at `/bin/clang++/lib` and +the SYCL consumer failed with `ld.lld: error: unable to find library -lsycl`. The +whole collection was audited for that swap. + +### 4.5 Keep the variable a member already read + +```cpp +.legacy_env = "MCPP_SLANGC" +``` + +It answers at the second level, before the engine's override. `rules-slang` also +keeps its historical silent PATH fallback, with a warning that names the correct +spelling (`options::compiler = tool::on_path()`) and the date it is removed. + +### 4.6 A member that only looks + +A member that reports what a machine has, rather than running it, passes +`.request = false`, so looking never pulls a payload down. `rules-qt` does this +when it resolves an SDK root: it reports which of the three levels named one, +and a report is not a reason to download anything. + +### 4.7 Prove it, without installing anything + +`mcpp.plugins.testing` runs a member against a stated context and compares the +directives it emitted: + +```cpp +{ "tool: a choice names the program and asks for no payload", + t::row::linux_libcxx().file("pkg/opt/bin/cmake") + .xpkg("xim", "cmake", "{root}/payload") + .xpkg_source("xim", "cmake", "pending"), // declared on request, not installed + [] { /* resolve with a choice */ }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("source=choice", "opt/bin/cmake"), "the choice is the source"); + c.expect(!r.has_line("mcpp:xpkg-request="), "a choice requests no payload"); + } }, +``` + +`!has_line("mcpp:xpkg-request=")` is the written proof that nothing was asked +for. The kit clears the real build's `MCPP_XPKG_*` variables, which a case must +not inherit: a case that inherited `pending` from the outer build asked for the +payload and planned nothing wherever it was absent, and passed wherever it was +installed. + +In CI the same criterion reads `resolution.json`, so it holds on a runner that +already has the payload: + +``` +payload:xim:cmake not requested by this build +``` + +## 5. For whoever designs a plugin + +**The project names a mechanism; the plugin declares that mechanism's payloads.** +A project writes `features = ["deps-cmake"]`, not `xim:cmake`. A feature states a +mechanism, so a payload at a fixed version does not belong in a feature's name — +0.15.0 removed the features that did that. Where a payload comes from is the +project's statement, which is why `[xlings.overrides]` is read from the root and +refused in a dependency. + +**One answer, recorded once.** An explicit source produces exactly one `Using` +line, one entry in the `Finished` summary, one record in `resolution.json`, and +one section of `mcpp why`. All four read the same `SourceDecision`, so three +cannot agree while the fourth lags. A member contributes to it by calling +`resolve`, which states `mcpp:decision=`; it does not print its own. + +**The default stays the default.** The ecosystem's path is the one a project +takes by writing nothing, and its output is unchanged by this mechanism. Anything +else gets a line of its own, in a colour and with a tag, precisely so that reading +a log cannot confuse the two. + +**One version of a package exists.** Several declarations of the same payload +unify to one winner, and `xpkg_dir` answers the version this build installed +rather than the spelling some manifest wrote. An override takes part in version +checking but not in deciding: it states where a package comes from, not which +version is wanted, so its key carries no version. + +**What stays outside.** A host tool a dependency publishes as `kind = "bin"` is +still named through `[tools.overrides]`: the two key spaces differ, and merging +them would put two kinds of key in one table. `mcpp.lock` records the result of +dependency resolution and deliberately records no toolchain. `tools = { ld = … }` +is read by a clang tree, where a linker reaches the link as `--ld-path`; a gcc +tree that states it is refused where the declaration is read, because gcc selects +a linker by the name `ld` inside a `-B` directory and a stated tool that takes no +part in the build is what this mechanism exists to prevent. + +## 6. Every official member that drives a tool, and how it declares it + +| member | tool | declaration | reason | +|---|---|---|---| +| `deps-vcpkg` | `xim:vcpkg` | on request | it runs vcpkg while planning | +| `deps-cmake`, `deps-archive` | `xim:cmake` | on request | it runs cmake while planning | +| `dist-appimage` | `xim:appimagetool` | on request | it runs the tool while planning | +| `dist-apk` | `xim:bundletool` | on request | only `--format aab` uses it | +| `dist-apk` | build tools, platform, JDK, keystore | eager | the plan reads an API level from the platform directory | +| `dist-apk-kotlin`, `dist-apk-maven` | `xim:kotlin`, `xim:coursier` | eager | a project that selects the feature compiles Kotlin or resolves a Maven graph | +| `dist-wix` | `xim:wix` | eager | the plan checks the payload's files | +| `rules-cuda`, `rules-hip` | toolkit components | eager | the plan reads versions out of the toolkit | +| `rules-sycl` | `xim:dpcpp`, `xim:gcc`, `xim:glibc`, `xim:linux-headers` | eager | the compile line is built from the C and C++ libraries | +| `rules-spirv`, `rules-slang`, `rules-ascendc` | `xim:shaderc` / `glslang`, `xim:slang`, `xim:cann-toolkit` | eager | the plan classifies the compiler it found | +| `dist-apple` | `xim:macapp-run`, `xim:apple-device-tools` | eager, under `when = "run"` | only `mcpp run` needs them, and a tier states that without a request | +| `rules-qt` | none | the project declares `xim:qt-base` in its own `[xlings]` table | the SDK a program links is the project's choice | +| `rules-metal`, `rules-swift` | none | the host's Xcode | the platform publishes no payload for them | + +Every one of them is named by `options::` in `build.mcpp`, by +`[xlings.overrides]`, by `MCPP_XLINGS_OVERRIDE__` or in +`~/.mcpp/config.toml`. The four on-request entries are the ones for which naming +it means no download at all. From 96b856b4294fbf26ee2431a7cf437e77da66f13b Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 04:08:08 +0800 Subject: [PATCH 21/24] docs: the plan record carries the reference page and why it exists --- ...-01-ecosystem-build-plugin-framework-plan.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index 62b3eef..b7df702 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -214,6 +214,23 @@ sha256 `40b9fa16be5628a5d277824f961faa33dadbf84d9520cff9fe2aeb9b0b217ebf`(下载 `arm64e.x1`,`macos-15` 对照两者皆成功。官方 23.1.2 的 macOS 包确实带 libc++ 头文件、 库与 `std` 模块源码(与 §12 R20a 的疑问相反)。 +## 4.1 面向两类读者的参考文档(2026-10-02 追加) + +`docs/tool-sources.md`(372 行,README 与 plugin-development.md 各有入口)。写它的理由是: +机制此前在三处分别被记下——引擎的键在 mcpp docs/23、模块的接口在 plugin-development.md、 +每个成员的选项在各自页面——而**没有一处说出必须同时成立的那两半**: +`build.mcpp` 里的 `tool::choice` 只有在插件把该载荷声明为 `provision = "on-request"` 时才等于 +「不下载」,因为 eager 的载荷在任何构建程序运行之前就已供给。这正是 mcpp#755 的原始症状, +也是两类读者真正会问的那个问题。 + +按场景组织:使用者七个场景(什么都不写、点名机器上的工具、写在清单里、为一次 CI 任务替换、 +多工程共享、离线构建与审计、自带整条工具链),作者七个场景(三块代码、那一对、只在某条分支上 +用到的载荷、一棵树而不是一个程序、历史环境变量、只看不要、如何在不安装任何东西的前提下证明), +以及设计侧的取舍。末尾一张表列出每个驱动工具的官方成员、它的声明方式与理由——这样 eager 的 +那些不会被读成遗漏。 + +全部论断都对清单与源码核对过;删掉了一处无来源的「80% 用户」数字,改为陈述事实。 + ## 5. 生态级 review 按「这条机制在生态的每个接缝处是否闭合」来看,而不是按仓库看。 From 191160053ce088cae9782baca6ccaa7e09aad324 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 04:11:29 +0800 Subject: [PATCH 22/24] docs: the final self-review, with the readings this delivery actually produced --- ...1-ecosystem-build-plugin-framework-plan.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index b7df702..c2566d1 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -264,3 +264,38 @@ framework-lab 的 8 个用例在 ubuntu-24.04、macos-15、windows-2022 上全 的字段已经与那份描述同形,所以它是后续的第三种载体,而不是改写;`mcpp.lock` 不记工具链——lock 的 内容是依赖解析的结果;`kind = "bin"` 的宿主工具仍走 `[tools.overrides]`——键空间与语义不同, 合并会让一张表有两种键。 + +## 6. 最终自我 review(交付后,2026-10-02) + +§5 的生态级 review 写在 CI settle 之前。这一节是交付后的复核:按 goal 列出的每个视角,给出**这次实际 +拿到的读数**,以及明确留下的边界。 + +| 视角 | 读数 | +|---|---| +| 架构 | 核心只新增 6 个访问器、2 条指令、3 个清单键;顺序与输出各只实现一份(核心一份、L2 一份)。13 个成员迁移完成,**没有一个成员重新实现顺序、拒绝文案或输出**——`describe_missing` 是唯一一份拒绝文案 | +| 稳定性 | 默认路径输出逐字不变;没有本地工具链时快速路径的全部代价是一次失败的 `open`;请求循环上限三轮,且请求的那次运行在 `apply` 与写缓存之前被丢弃,所以没有半应用状态 | +| 优雅简洁 | 一条 `SourceDecision` 被四处读(status 行、`Finished` 汇总、`resolution.json`、`mcpp why`);`tool::choice` 可由字符串构造,所以成员迁移对其使用者零改动 | +| 用户体验 | 每个显式来源一行,带类与 `file:line`;每条拒绝都列出命名它的四种方式;`considered` 记下每一层问过什么、答了什么 | +| 兼容性 | **未迁移的第三方插件无需改动即受益**(覆盖发生在引擎层,`xpkg_dir` 直接回答);成员的历史环境变量保留;地板不满足时的拒绝现在会点名地板与升级方式(第 7 处发现) | +| 跨平台 | Linux / macOS / Windows 全绿。跨平台本身贡献了 5 处发现:响应文件的两种 tokenize 语法、Darwin 的 `environ`、宿主自己补的 `.exe` 后缀、Apple 链接形状拿不到 `--ld-path`、gcc 16 的 `always_inline` 拒绝 | +| 一致性 | 优先级只写两份(核心、L2);中英文档结构一一对应;`docs/tool-sources.md` 补上了此前三处分散记述都没说出的那一对条件 | +| 无感升级 | 不写任何新键的工程行为与输出不变,由既有 e2e 与 framework-lab 的 `default` 用例断言;插件 0.19.0 的成员在默认路径上下载与输出不变 | +| 测试覆盖 | 单测 144(新增 21);e2e 873-877 加 875 里 gcc 拒绝的反例;`plugin-logic` 29 例;framework-lab 10 例 × 3 平台;toolchain-lab 8 例 × 3 平台;CI 判据读 `resolution.json`,所以在已装载荷的 runner 上也能断言「这次没要求它」 | + +**11 处发现里,有 2 处是我自己的缺陷**(rules-sycl 把载荷根与程序当成一个值;`--ld-path` 只写在 +Linux clang 分支里),**3 处是编译器/工具链危害**(clang 20.1.7 的两次崩溃、gcc 16 的内联拒绝), +**1 处是文档过度断言**(lock 记 `local` 从未实现)。每一处都带前后读数,没有一处靠推断结案。 + +**明确留下的边界:** + +1. 省下的是一次性的供给成本,不是每次构建的成本——对已有暖缓存的 CI 不会再省第二次(framework-lab + 的第四次构建读数)。 +2. `xcode-27` 的两条 lane 仍然红,原因在上游(SDK 的 `arm64e.x1` stub,lld 22.1.8 无法解析)。 + llvm-macos27-lab 证明了 `release/23.x` 的修复可用,但没有任何 LLVM 发布带着它,所以 mcpp#669 + 保持打开。 +3. Windows e2e 的 25 分钟超时是那条 lane 既有的 flake(main 在 2026-10-01T08:09 的运行同样超时), + 重跑即过;本次的五个新用例在 Windows 上因能力缺失被跳过,不可能是原因。 +4. e2e 875 在没有装 llvm 载荷的宿主上 SKIP,而 macOS CI 正是这样的宿主——这就是第 9 处发现只能 + 由 lab 找到的原因,也是 lab 这种形态存在的理由。 +5. 工具链描述数据化、`mcpp.lock` 记工具链、`kind = "bin"` 宿主工具并入同一张覆盖表:都明确不做, + 理由记在设计记录的「已知边界」。 From 7d22dc02a17beae9f072f44a06e049c888afab78 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 04:38:06 +0800 Subject: [PATCH 23/24] docs: the four ways a build program names its own tool, compiled before 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. --- ...1-ecosystem-build-plugin-framework-plan.md | 7 ++ docs/tool-sources.md | 83 ++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index c2566d1..c4d9f02 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -231,6 +231,13 @@ sha256 `40b9fa16be5628a5d277824f961faa33dadbf84d9520cff9fe2aeb9b0b217ebf`(下载 全部论断都对清单与源码核对过;删掉了一处无来源的「80% 用户」数字,改为陈述事实。 +**§3.2 的四种写法是编译过的。** 第一版只用一行注释提了 `tool::on_path()` 与 `tool::root()`, +不成介绍;补成完整示例后把它当成真的构建程序编译,立刻暴露一处文档缺陷:**特性让模块可用, +不等于可见**——写 `tool::root(...)` 必须 `import mcpp.plugins.tool;`,否则编译器报 +`declaration of 'root' must be imported from module 'mcpp.plugins.tool' before it is required`; +而赋一个字符串或写 `vcpkg_root` 不需要。页面现在把这条规则写出来了。页面里引的那段拒绝文案 +也是实测抄下来的:`consulted: options::vcpkg = root("/opt/vcpkg") (no vcpkg there)`。 + ## 5. 生态级 review 按「这条机制在生态的每个接缝处是否闭合」来看,而不是按仓库看。 diff --git a/docs/tool-sources.md b/docs/tool-sources.md index a6d912e..421eef7 100644 --- a/docs/tool-sources.md +++ b/docs/tool-sources.md @@ -81,12 +81,91 @@ or because `--format` took another branch — downloads nothing for it. ### 3.2 Name the tool the machine already has +Four spellings, all in `build.mcpp`, none of which downloads the payload. The +example is `deps-vcpkg`, whose option is `options::vcpkg`; every member that +drives a tool has the same shape, with its own option name (section 6). + ```cpp // build.mcpp -mcpp::deps::cmake::options o; -o.cmake = "/usr/bin/cmake"; // or tool::on_path(), or tool::root("/opt/cmake") +import std; +import mcpp; +import mcpp.deps.vcpkg; +import mcpp.plugins.tool; // for `tool::root` and `tool::on_path` below + +int main() { + mcpp::deps::vcpkg::options o; + o.libraries = { "fmt", "spdlog" }; + + // 1. the program itself -- a string assigns, so code written before 0.19.0 + // keeps compiling + o.vcpkg = "/opt/vcpkg/vcpkg"; + + // 2. a tree. Each member states where it looks under a root: `deps-vcpkg` + // expects `vcpkg` directly there, `deps-cmake` looks in `bin` and in + // `CMake.app/Contents/bin` + o.vcpkg = mcpp::plugins::tool::root("/opt/vcpkg"); + + // 3. the first one on PATH. A fallback is a choice, stated here, rather + // than something a member does quietly + o.vcpkg = mcpp::plugins::tool::on_path(); + + // 4. the spelling `deps-vcpkg` has had since 0.18.1, still read: the same + // statement as `tool::root(...)` + o.vcpkg_root = "/opt/vcpkg"; + + return mcpp::deps::vcpkg::use(o) ? 0 : 1; +} +``` + +A relative path is relative to the package root, which is where a project keeps +a vendored copy: + +```cpp +o.vcpkg = mcpp::plugins::tool::root("third_party/vcpkg"); ``` +`mcpp.plugins.tool` needs no extra feature: `deps-vcpkg` implies `deps`, which +implies `plugins-core`. It does need the `import` above, though -- a feature makes +a module available, not visible. Assigning a plain string (form 1) and +`vcpkg_root` (form 4) need no import; naming `tool::root` or `tool::on_path` does, +and without it the compiler says `declaration of 'root' must be imported from +module 'mcpp.plugins.tool' before it is required`. + +**Deciding in the build program, from whatever the machine says.** The choice is +a value, so the decision is ordinary code. Register the variable, and the build +re-plans when it changes: + +```cpp +if (const char* r = std::getenv("VCPKG_ROOT"); r && *r) { + mcpp::rerun_if_env_changed("VCPKG_ROOT"); + o.vcpkg = mcpp::plugins::tool::root(r); // use it where CI provides one +} +// left default: the ecosystem's xim:vcpkg +``` + +`mcpp::plugins::toolchain::env("VCPKG_ROOT")` is the same two lines, for a +project that already enables `plugins-toolchain`. + +**A stated choice that fails is not replaced by another source.** The member +refuses and names what it consulted, rather than falling back to the payload: + +``` +warning: my-app: mcpp.deps.vcpkg: no vcpkg. + consulted: options::vcpkg = root("/opt/vcpkg") (no vcpkg there) + … + So this plan installs nothing. +``` + +That is deliberate. Replacing a decision that was made explicitly, and failed, +with a different source would make "was the one I named actually used" impossible +to answer from the output. + +**What makes this avoid the download** is that `deps-vcpkg` declares its payload +`provision = "on-request"`, so the choice is read before anything is provisioned. +For a member whose payload is eager -- `rules-cuda`'s toolkit, which the plan +reads a version out of -- naming the tool in `build.mcpp` does not prevent the +download, and `[xlings.overrides]` is the way out. Section 4.2 is the table. + ``` Using cmake (mcpp.deps.cmake) ← /usr/bin/cmake [program · build.mcpp:9] Finished dev [unoptimized + debuginfo] in 6.4s · program: cmake (mcpp.deps.cmake) From d1a3cf57e3da25988e43cc9ec38a39be83c0a2b1 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 04:41:00 +0800 Subject: [PATCH 24/24] docs: replacing the default tool from build.mcpp is its own chapter 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. --- ...1-ecosystem-build-plugin-framework-plan.md | 8 +- docs/tool-sources.md | 146 ++++++++++++------ 2 files changed, 108 insertions(+), 46 deletions(-) diff --git a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md index c4d9f02..21ae403 100644 --- a/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md +++ b/.agents/docs/2026-10-01-ecosystem-build-plugin-framework-plan.md @@ -231,7 +231,13 @@ sha256 `40b9fa16be5628a5d277824f961faa33dadbf84d9520cff9fe2aeb9b0b217ebf`(下载 全部论断都对清单与源码核对过;删掉了一处无来源的「80% 用户」数字,改为陈述事实。 -**§3.2 的四种写法是编译过的。** 第一版只用一行注释提了 `tool::on_path()` 与 `tool::root()`, +**「用自己的工具替换默认的」独立成章(§3)。** 第一版把它塞在「使用者」一章的一个小节里, +与「写在清单里」「为一次 CI 替换」并列,读者要先认出哪一节才是自己要的。现在它是单独一章, +七个小节:四种写法、相对路径指向哪里、在构建程序里决定、点名失败不会静默回退、这条路为什么 +能免掉下载、哪个成员用哪个选项及它在根下面找什么(一张表)、如何确认这次真的没下载。 +原来的「使用者」一章改为第 4 章「其他途径」。 + +**§3.1 的四种写法是编译过的。** 第一版只用一行注释提了 `tool::on_path()` 与 `tool::root()`, 不成介绍;补成完整示例后把它当成真的构建程序编译,立刻暴露一处文档缺陷:**特性让模块可用, 不等于可见**——写 `tool::root(...)` 必须 `import mcpp.plugins.tool;`,否则编译器报 `declaration of 'root' must be imported from module 'mcpp.plugins.tool' before it is required`; diff --git a/docs/tool-sources.md b/docs/tool-sources.md index 421eef7..ae74a64 100644 --- a/docs/tool-sources.md +++ b/docs/tool-sources.md @@ -61,29 +61,17 @@ the engine's e2e cases and the framework lab assert it. test is whether a version was stated, not whether the path looks like a system directory, which would be a guess. -## 3. For whoever uses a plugin +## 3. Replacing the default tool from `build.mcpp` -### 3.1 Write nothing +This is the chapter for a project that wants its own tool rather than the one the +plugin declares, stated in the build program and nowhere else -- no +`[xlings.overrides]`, no environment variable. Chapter 4 covers those. -```toml -[build-dependencies.mcpp] -plugins = { version = "0.19.0", features = ["deps-cmake"], host-module = true } -``` - -The member resolves nothing stated, reaches the fourth level, finds `xim:cmake` -declared on request and not installed, asks for it, and the engine installs it and -runs the program again. The output is byte-for-byte what it was before this -mechanism existed. - -One difference from 0.18.1: **a payload declared and not used is not installed.** -A build that never reaches the member — because it compiles only its own modules, -or because `--format` took another branch — downloads nothing for it. - -### 3.2 Name the tool the machine already has +### 3.1 The four spellings Four spellings, all in `build.mcpp`, none of which downloads the payload. The example is `deps-vcpkg`, whose option is `options::vcpkg`; every member that -drives a tool has the same shape, with its own option name (section 6). +drives a tool has the same shape, with its own option name (section 3.6). ```cpp // build.mcpp @@ -117,6 +105,15 @@ int main() { } ``` +`mcpp.plugins.tool` needs no extra feature: `deps-vcpkg` implies `deps`, which +implies `plugins-core`. It does need the `import` above, though -- a feature makes +a module available, not visible. Assigning a plain string (form 1) and +`vcpkg_root` (form 4) need no import; naming `tool::root` or `tool::on_path` does, +and without it the compiler says `declaration of 'root' must be imported from +module 'mcpp.plugins.tool' before it is required`. + +### 3.2 Where a relative path points + A relative path is relative to the package root, which is where a project keeps a vendored copy: @@ -124,16 +121,10 @@ a vendored copy: o.vcpkg = mcpp::plugins::tool::root("third_party/vcpkg"); ``` -`mcpp.plugins.tool` needs no extra feature: `deps-vcpkg` implies `deps`, which -implies `plugins-core`. It does need the `import` above, though -- a feature makes -a module available, not visible. Assigning a plain string (form 1) and -`vcpkg_root` (form 4) need no import; naming `tool::root` or `tool::on_path` does, -and without it the compiler says `declaration of 'root' must be imported from -module 'mcpp.plugins.tool' before it is required`. +### 3.3 Deciding inside the build program -**Deciding in the build program, from whatever the machine says.** The choice is -a value, so the decision is ordinary code. Register the variable, and the build -re-plans when it changes: +**The choice is a value, so the decision is ordinary code.** Register the variable +that informs it, and the build re-plans when it changes: ```cpp if (const char* r = std::getenv("VCPKG_ROOT"); r && *r) { @@ -146,7 +137,9 @@ if (const char* r = std::getenv("VCPKG_ROOT"); r && *r) { `mcpp::plugins::toolchain::env("VCPKG_ROOT")` is the same two lines, for a project that already enables `plugins-toolchain`. -**A stated choice that fails is not replaced by another source.** The member +### 3.4 A stated choice that fails is not replaced + +The member refuses and names what it consulted, rather than falling back to the payload: ``` @@ -160,11 +153,13 @@ That is deliberate. Replacing a decision that was made explicitly, and failed, with a different source would make "was the one I named actually used" impossible to answer from the output. -**What makes this avoid the download** is that `deps-vcpkg` declares its payload +### 3.5 What makes this avoid the download + +`deps-vcpkg` declares its payload `provision = "on-request"`, so the choice is read before anything is provisioned. For a member whose payload is eager -- `rules-cuda`'s toolkit, which the plan reads a version out of -- naming the tool in `build.mcpp` does not prevent the -download, and `[xlings.overrides]` is the way out. Section 4.2 is the table. +download, and `[xlings.overrides]` is the way out. Section 5.2 is the table. ``` Using cmake (mcpp.deps.cmake) ← /usr/bin/cmake [program · build.mcpp:9] @@ -185,7 +180,68 @@ per-build cost.** A fourth build — the control with the payload already instal — took 6.4, 5.0 and 5.7 s, which is what naming the host's cmake costs. A CI with a warm payload cache does not save this twice. -### 3.3 State it in the manifest instead of the build program + +### 3.6 Which option, and what it looks for + +Each member names its tool through one option, and states where it looks under a +root. A path assigned as a string is taken as the program itself in every case. + +| member | option | program names | under a root it looks in | variable it still reads | +|---|---|---|---|---| +| `deps-vcpkg` | `options::vcpkg` (and `options::vcpkg_root`) | `vcpkg` | the root itself | -- | +| `deps-cmake`, `deps-archive` | `options::cmake` | `cmake` | `bin`, `CMake.app/Contents/bin` | -- | +| `rules-spirv` | `options::compiler` | `glslangValidator`, `glslang`; or `glslc` | `bin`, the root | `MCPP_GLSLANG`, `MCPP_GLSLC` | +| `rules-slang` | `options::compiler` | `slangc` | `bin`, the root | `MCPP_SLANGC` | +| `rules-sycl` | `options::compiler` | `clang++` | `bin`, the root | -- | +| `rules-cuda` | `options::toolkit` | `nvcc` | `bin`, the root | -- | +| `rules-hip` | `options::toolkit` | `hipcc`, `clang++` | `bin`, the root | -- | +| `rules-ascendc` | `options::toolkit` | a root, no program | -- | -- | +| `rules-qt` | `options::root`, `options::extra_roots` | a root, no program | -- | `QT_ROOT_DIR` | +| `dist-appimage` | `options::tool` | `appimagetool` | the root, `bin` | -- | +| `dist-wix` | `options::tool` | `wix` | `tool/tools/net6.0/any` | -- | +| `dist-apk` | `options::build_tools`, `options::platform`, `options::jdk`, `options::bundletool_dir`, `options::kotlin`, `options::coursier` | roots, no program | -- | -- | + +A member whose option names a **root rather than a program** -- `rules-qt`, +`rules-ascendc`, `dist-apk` -- takes `tool::root(...)` or a plain directory path, +because what it needs is the tree, not one executable. + +### 3.7 Confirming nothing was downloaded + +```bash +MCPP_NO_AUTO_INSTALL=1 mcpp build # success means no payload was asked for +mcpp why payload xim:vcpkg +``` + +``` +sources: + payload:xim:vcpkg /opt/vcpkg/vcpkg + program · build.mcpp:9 for my-app + considered: options::vcpkg = root("/opt/vcpkg"); payload xim:vcpkg (not requested by this build) +``` + +`not requested by this build` is the written proof. On a machine that already +holds the payload this is the only reliable check, because the build would have +succeeded either way. + +## 4. For whoever uses a plugin: the other ways + +### 4.1 Write nothing + +```toml +[build-dependencies.mcpp] +plugins = { version = "0.19.0", features = ["deps-cmake"], host-module = true } +``` + +The member resolves nothing stated, reaches the fourth level, finds `xim:cmake` +declared on request and not installed, asks for it, and the engine installs it and +runs the program again. The output is byte-for-byte what it was before this +mechanism existed. + +One difference from 0.18.1: **a payload declared and not used is not installed.** +A build that never reaches the member — because it compiles only its own modules, +or because `--format` took another branch — downloads nothing for it. + +### 4.2 State it in the manifest instead of the build program ```toml [xlings.overrides] @@ -206,14 +262,14 @@ nothing can be compared, and the build records a note naming the requirement tha went unchecked. The engine never runs a program to ask its version: every tool spells `--version` differently, and that is the plugin's knowledge. -### 3.4 Replace a tool for one CI job +### 4.3 Replace a tool for one CI job ```bash MCPP_XLINGS_OVERRIDE_XIM_VCPKG=/opt/vcpkg/vcpkg mcpp build MCPP_XLINGS_OVERRIDE_XIM_CMAKE=path:cmake mcpp build # looked up on PATH: the host class ``` -### 3.5 Share one tool between every project on a machine +### 4.4 Share one tool between every project on a machine ```toml # ~/.mcpp/config.toml — a fact about this machine, not in the repository @@ -221,7 +277,7 @@ MCPP_XLINGS_OVERRIDE_XIM_CMAKE=path:cmake mcpp build # looked up on PATH: th "xim:vcpkg" = { root = "/opt/vcpkg" } ``` -### 3.6 Build without the network, and audit what a build used +### 4.5 Build without the network, and audit what a build used ```bash MCPP_NO_AUTO_INSTALL=1 mcpp build # refused, naming what it would have installed @@ -239,7 +295,7 @@ sources: considered: options::cmake = "/usr/bin/cmake"; payload xim:cmake (not requested by this build) ``` -### 3.7 Bring a whole toolchain +### 4.6 Bring a whole toolchain A toolchain is the same question one layer down, and it is answered in the manifest or by the root build program, never by a plugin: @@ -258,9 +314,9 @@ bootstrap = "llvm@22.1.8" # the one that compiles build pr `with_sysroot` / `with_family` / `with_tool` refinements, and `configure(fn)` / `use(d)`. -## 4. For whoever writes a plugin +## 5. For whoever writes a plugin -### 4.1 A member that runs a program +### 5.1 A member that runs a program Three pieces, written once per tool: @@ -292,7 +348,7 @@ compiles, and the line that wrote it reaches the build's output and `describe_missing` is the one refusal text, and it lists every way to name the tool. A member does not write its own. -### 4.2 The pair that makes "named here, not downloaded" true +### 5.2 The pair that makes "named here, not downloaded" true A `tool::choice` alone is not enough. Provisioning of an eagerly declared payload happens in prepare, **before any build program runs**, so a payload declared @@ -318,7 +374,7 @@ So the author's decision is one question: the second way, and that is timing, not an oversight: moving provisioning after the build program would make them fail to plan. -### 4.3 Ask for a payload only on the branch that uses it +### 5.3 Ask for a payload only on the branch that uses it `dist-apk` declares five payloads for every Android build and one more only for `--format aab`: @@ -334,7 +390,7 @@ discarded before its directives are applied, so there is no half-applied state. Three rounds at most, and a program that asks for something new in the third is refused by name. -### 4.4 A tool that is a tree, not a program +### 5.4 A tool that is a tree, not a program ```cpp auto f = mcpp::plugins::tool::resolve(nvcc_spec(), opt.toolkit); @@ -347,7 +403,7 @@ in `rules-sycl` while it took the payload directory and appended the SYCL consumer failed with `ld.lld: error: unable to find library -lsycl`. The whole collection was audited for that swap. -### 4.5 Keep the variable a member already read +### 5.5 Keep the variable a member already read ```cpp .legacy_env = "MCPP_SLANGC" @@ -357,14 +413,14 @@ It answers at the second level, before the engine's override. `rules-slang` also keeps its historical silent PATH fallback, with a warning that names the correct spelling (`options::compiler = tool::on_path()`) and the date it is removed. -### 4.6 A member that only looks +### 5.6 A member that only looks A member that reports what a machine has, rather than running it, passes `.request = false`, so looking never pulls a payload down. `rules-qt` does this when it resolves an SDK root: it reports which of the three levels named one, and a report is not a reason to download anything. -### 4.7 Prove it, without installing anything +### 5.7 Prove it, without installing anything `mcpp.plugins.testing` runs a member against a stated context and compares the directives it emitted: @@ -394,7 +450,7 @@ already has the payload: payload:xim:cmake not requested by this build ``` -## 5. For whoever designs a plugin +## 6. For whoever designs a plugin **The project names a mechanism; the plugin declares that mechanism's payloads.** A project writes `features = ["deps-cmake"]`, not `xim:cmake`. A feature states a @@ -429,7 +485,7 @@ tree that states it is refused where the declaration is read, because gcc select a linker by the name `ld` inside a `-B` directory and a stated tool that takes no part in the build is what this mechanism exists to prevent. -## 6. Every official member that drives a tool, and how it declares it +## 7. Every official member that drives a tool, and how it declares it | member | tool | declaration | reason | |---|---|---|---|