diff --git a/.agents/docs/2026-08-29-build-rule-package-spec.md b/.agents/docs/2026-08-29-build-rule-package-spec.md index 087bbe0e..a0a4e2a3 100644 --- a/.agents/docs/2026-08-29-build-rule-package-spec.md +++ b/.agents/docs/2026-08-29-build-rule-package-spec.md @@ -26,7 +26,7 @@ > reading anything: `role = "check"` could not be written portably, and that is > fixed here (§9, open question 4). > -> ⚠️ A sixth is about this document's own method. The descriptor count was +> A sixth is about this document's own method. The descriptor count was > first taken from `mcpp-community/mcpp-index`, while `mcpp index list` reports > `mcpplibs/mcpp-index` as the default — the right question asked of the wrong > object. The conclusion survived (one rule package either way) and the number @@ -223,7 +223,12 @@ behaviour without using a new API, is left uncovered deliberately. ### I8. `mcpp.*` is reserved for official plugins, and using it produces a warning -Official rule packages are named `mcpp.build.`. In C++ this creates no +Official rule packages are named `mcpp.rules.` and official build-time +utilities `mcpp.tools.`; both live in the collection `mcpp:plugins` +(repository `mcpp-community/mcpp-plugins`), each member selected by a feature +(2026-09-05 revision; the first revision of this section said `mcpp.build.`, +which is the engine's own module family -- `mcpp.build.plan`, +`mcpp.build.prepare` -- and was withdrawn for that reason). In C++ this creates no relationship with the bundled `mcpp` module — a dot in a module name carries no hierarchy — but to a reader the prefix reads as an endorsement, and that is a supply-chain statement. @@ -537,7 +542,12 @@ hazard. 10.1 row 4: the `mcpp.rules.*` decision is withdrawn. It was unimplementable under the rule that the module name is the bare package name. It is replaced by `mcpp.build.*` as a **module name** prefix — the distinction the original - decision lacked. + decision lacked. *Revised 2026-09-05:* with I1 implemented the objection no + longer holds, and `mcpp.build.*` names the engine's own modules; the prefix + is `mcpp.rules.*` for rules and `mcpp.tools.*` for utilities, as I8 now + states. A package may contribute every module interface unit among its + feature-resolved sources (mcpp 2026.9.5.3+), which is what lets one + collection carry them all. 2. `docs/05-mcpp-toml.md:1605` and its Chinese counterpart: the paragraph stating that the module name is the package's `name`, and that `grpc-rules` is therefore illegal, is rewritten under I1 and I2. @@ -611,7 +621,7 @@ own. needs no shell, no `touch`, and no per-platform spelling. A command that already writes its own stamp is unaffected — existing files are left alone. - ⚠️ **The obvious assertion does not discriminate.** Measured: ninja does NOT + **The obvious assertion does not discriminate.** Measured: ninja does NOT fail when a declared output goes unproduced. It leaves the file absent and re-runs that edge on every subsequent build, so the build stays green and the only symptom is work silently redone. `tests/e2e/312` therefore asserts @@ -1176,7 +1186,7 @@ subsystem that has quietly come to rely on something it does not declare compiles in the root build and fails there, which is the reason to run both rather than pick one. -⚠️ `mcpp test -p ` exits 0 for a member with no tests, so the CI loop is +`mcpp test -p ` exits 0 for a member with no tests, so the CI loop is green either way and "has no tests" reads exactly like "tests pass". `check_modules_wiring.sh` therefore prints which members have none. It does not fail on them: a package of vendored parsers legitimately has nothing of its own diff --git a/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md b/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md new file mode 100644 index 00000000..470032da --- /dev/null +++ b/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md @@ -0,0 +1,351 @@ +# Heterogeneous C++ builds and their ecosystem, v2: closing the host surface + +> Supersedes `.agents/docs/2026-09-05-multi-device-ecosystem-design.md` (v1) and +> keeps its task table. v1 asked how a device build should be shaped; this +> document asks what remains outside the ecosystem when it is, answers it with a +> measurement rather than a list, and states the one invariant the rest follows +> from. +> +> Status of the sections below: §2 is delivered and verified, §3 is a +> measurement taken on 2026-09-05, §4 is the plan that closes what §3 found. + +--- + +## 1. The invariant + +**Nothing reaches the host except PROPRIETARY vendor userspace that is in ABI +lockstep with a kernel module, and even that is linked rather than +redistributed.** + +An open-source driver is not an exception: Mesa builds in a subos and ships as a +payload, so a machine with AMD, Intel or no GPU at all runs entirely on +packages. A closed-source component is either linked where it already is (the +sentinel packages) or fetched from the vendor's own published URL — never +copied into an xlings-res release. + +Everything else — the X protocol stack, compression, ICU, an assembler, a C++ +runtime, LLVM, a shader compiler, a software rasteriser — is a package. Where +one does not exist yet, the correct action is to publish it, not to widen the +exception. + +Two consequences that are easy to state and were not being observed: + +1. A *rule package* driving a second compiler must drive a compiler the + ecosystem resolved. `mcpp.build.cuda` never invokes `/usr/bin/g++`: it takes + the toolchain's own `g++`, and when nvcc's `crt/host_config.h` states a bound + that excludes it, a `gcc` payload the project declared in + `[xlings.workspace]`, and otherwise it refuses and names the declaration to + add. There is no host branch in that decision. +2. A compiler the ecosystem resolved still has to be *told* where the + ecosystem is. `MCPP_TOOLCHAIN_SYSROOT` and `MCPP_TOOLCHAIN_BINUTILS_DIR` + carry the `--sysroot` and `-B` mcpp passes to its own compiler, so a rule + package can forward them. Without them the payload compiler looks in + `/usr/include` and the payload assembler is not found at all — the two + variables are the mechanism that keeps the host out, not evidence of it. + +## 2. What v1 delivered, with its evidence + +| | criterion, as measured | +|---|---| +| Accelerator axis, constrained globs, action edges, probe channel, version floors | mcpp 2026.9.5.2, released; e2e 601/605/606/607/608 | +| Device objects reach a static library | e2e 608 asserts the `ar t` member list and an `nm` symbol, not the exit status — an empty archive also succeeds | +| CUDA lane, two routes | `12 24 36 48` on an RTX 4080, clang `-x cuda` and nvcc; no `/usr` path on any command line | +| A rule package owns the vendor spelling | `test_core_vendor_probes`: no vendor tool name in `src/` after comments are stripped, with its own denominator | +| Vulkan lane | `examples/10-vulkan-compute`: one artifact, three devices — discrete GPU, CPU rasteriser, and `--no-accel` — all `12 24 36 48` | +| A Vulkan device that needs no GPU | `xim:mesa-lavapipe@26.2.1`, published; `CPU llvmpipe (LLVM 22.1.8)` from a fresh extraction at an unrelated path | +| The ICD closure is computed, not listed | `compat.vulkan-runtime` seeds `ldd` from each ICD manifest's `library_path`; before, a machine with lavapipe installed enumerated no CPU device because nothing in a hand-written list could know that LLVM links ICU | + +Four corrections v2 makes to v1, each found by building the second instance of +something v1 had built once: + +* **The device-source table was one row per vendor.** `SourceKind::Device` + documents itself as a graph role explicitly so the table does not grow per + vendor, and held `.cu` and `.hip` alone. A shader in a constrained glob was + refused with "no role for the extension '.comp'" while the same run told the + rule package there were no device sources. Now 18 extensions: CUDA, HIP, the + GLSL stages, HLSL, OpenCL C, Metal. +* **Rule-package naming did not follow its own specification.** + `2026-08-29-build-rule-package-spec.md` I1/I8 say the module name is declared + by the rule's source and that `mcpp.*` is reserved for rules the project + maintains, enforced as a warning keyed on the package namespace. + `mcpplibs.rules.cuda` was neither. Both rules are now `mcpp.build.cuda` and + `mcpp.build.spirv` under the `mcpp` namespace, and the warning firing under + the old namespace is the control that proves the check is live. +* **`ldd` on `PATH` is not the host's.** Under xlings it is the payload's own, + whose default search path is its build prefix, so it answers `not found` for + every host library — and that failure is indistinguishable from "this machine + needs nothing", because both produce an empty match. Search paths are now + supplied explicitly. +* **`DT_RUNPATH` is not a modern `DT_RPATH`.** A non-empty `RUNPATH` on a + `dlopen`'d object switches off the executable's inherited `RPATH` for that + object's dependencies, and an mcpp binary reaches its C library only through + that inherited path. Measured both ways on one machine, minutes apart. + +## 3. The host surface, measured + +`compat.vulkan-runtime` now writes `HOST-SURFACE.txt` into the package at +install time: every farm entry with the file it points at, what was filled from +an installed payload, and what nothing could resolve. The classification below +is that file, intersected with what `xim-pkgindex` publishes, on a machine with +an NVIDIA driver and the distribution's Mesa. + +| class | count | verdict | +|---|---|---| +| Vendor driver userspace (`libnvidia*`, `libGLX_nvidia`, `libvulkan_*`, `libcuda`, `libnvcuvid`) | 47 | **irreducible.** Already modelled as sentinel packages (`xim:libcuda-host-link`, `xim:nvidia-gl-host-link`, `xim:wsl-gl-host-link`) that link rather than redistribute | +| Version-locked to the host's Mesa (`libLLVM.so.20.1`) | 1 | **not irreducible — an artefact of using the host's Mesa at all.** The soname names the LLVM that build was linked against, so it cannot be substituted; the answer is not to substitute it but to stop loading the host's Mesa. See §3.1 | +| Published by `xim-pkgindex` already (`libX11`, `libxcb` and its 25 extension libraries, `libdrm` and its four `libdrm_*`, `libXau`, `libXdmcp`, `libXext`, `libxshmfence`, `libexpat`, `libz`, `libffi`, `libelf`, `libxml2`, `libtinfo`, `libwayland-client`, `libstdc++`) | 21 sonames, 25+4 more inside two of them | **reducible today by declaration** | +| Published by nobody | 6 upstream projects | **reducible by packaging**, listed below | + +The six, and what needs them: + +| project | sonames | reached through | +|---|---|---| +| `zstd` | `libzstd.so.1` | Mesa, LLVM | +| `xz` | `liblzma.so.5` | `libxml2` ← LLVM | +| `icu` | `libicuuc`, `libicudata` | LLVM 20+ | +| `libedit` | `libedit.so.2` | LLVM | +| `libbsd` + `libmd` | `libbsd.so.0`, `libmd.so.0` | `libX11` on distributions that link it | +| `xcb-util` family | `libxcb-util`, `-image`, `-keysyms`, `-icccm`, `-render-util`, `-cursor` | toolkits above X | + +They are ordinary autotools/meson projects and the harness that builds the rest +of the stack in a subos (`.agents/tools/graphics/build-in-subos.sh`) applies +unchanged. + +### 3.1 The driver taxonomy, which decides everything above + +The table's second row is not a law of nature. It exists because the machine +loaded the *host's* Mesa, and the host's Mesa was linked against the host's +LLVM. A driver that is open source does not have to come from the host at all: + +| driver | source | who provides it | +|---|---|---| +| llvmpipe / lavapipe (CPU) | Mesa | **payload** — `xim:mesa-lavapipe`, shipped | +| radeonsi, RADV (AMD) | Mesa | **payload** — `xim:mesa` already builds them | +| iris, anv (Intel), nouveau, zink, d3d12 | Mesa | **payload**, once the build gains `-Dvulkan-drivers=intel` and the `clc` chain its Intel Vulkan driver needs | +| NVIDIA proprietary userspace (`libcuda`, `libnvidia*`, `libGLX_nvidia`) | NVIDIA | **sentinel** — linked, never copied: `xim:libcuda-host-link`, `xim:nvidia-gl-host-link` | +| WSL2's D3D12 userspace | Microsoft, mounted by WSL | **sentinel** — `xim:wsl-gl-host-link` | + +So the irreducible set is exactly *proprietary userspace in ABI lockstep with a +kernel module*, and even that is linked rather than redistributed. Everything +else on a machine — including a graphics driver — is a payload, and when the +payload driver is used, the second row of the table above disappears with it, +because the payload's LLVM is `xim:libllvm`. + +The consequence for §4 is a task rather than an exception: extend the Mesa +payload's driver set, and prefer the payload ICD over the host's whenever one +covers the hardware. The host ICD path then exists for proprietary drivers +only. + +### 3.2 The C++ runtime is a package, not an exception + +`libstdc++.so.6` and `libgcc_s.so.1` are redistributable — GPL-3.0 with the +runtime library exception exists for exactly this — and `xim:gcc-runtime` +publishes them. The earlier reasoning for leaving them on the host was +directional and stated without its direction: substituting an **older** +libstdc++ under a host driver fails as a missing symbol version, but +substituting a **newer** one is what every distribution upgrade does, and +libstdc++ is backward compatible by design. + +So the rule is a comparison, not an avoidance: the payload copy is used when its +`GLIBCXX`/`CXXABI` version set covers what the host's provides, and the host's +otherwise. The same holds for the third class generally — the substitution is +safe in the direction where the package is at least as new, and that is a +question a package can answer at install time rather than a hazard it has to +route around. + +### 3.3 What remains deliberately unsubstituted + +Only this: a soname the host provides at a version *newer* than any package in +this ecosystem. The farm then keeps the host's copy, records it in +`HOST-SURFACE.txt`, and the entry is a packaging backlog item rather than a +permanent exception. + +## 4. The plan that closes it + +Dependencies run downward; each task states the criterion that decides it. + +### Tier A — packages that remove host libraries (xim-pkgindex) + +| # | task | criterion | depends on | +|---|---|---|---| +| A1 | `zstd`, `xz`, `icu`, `libedit`, `libbsd`+`libmd` | each installs and `selfcontained-check.sh` reports no host reference | — | +| A2 | `xcb-util`, `-image`, `-keysyms`, `-wm`, `-renderutil`, `-cursor` | same | A1 (`libbsd`) | +| A3 | aarch64 payloads for the 21 sonames of the third class | the same probe passes on aarch64 | A1, A2 | +| A4 | `pocl` (CPU OpenCL), repacked from conda-forge as `mesa-lavapipe` was | `clinfo`-equivalent probe reports a CPU device with no GPU present | A1 | +| A5 | extend `xim:mesa`'s driver set (`-Dvulkan-drivers=amd,swrast,intel`, `-Dgallium-drivers=+iris,nouveau`) so hardware Vulkan and GL on open drivers are payloads | on an AMD or Intel machine, `HOST-SURFACE.txt` contains no driver entry at all | A1, `glslang` (published), the `clc` chain for anv | +| A6 | `gcc-runtime` version comparison in the farm: payload copy when its `GLIBCXX`/`CXXABI` set covers the host's | the probe still loads on a host newer than the payload, and the report says which copy was chosen | A1 | + +### Tier B — the OpenCL adapter (mcpp-index) + +| # | task | criterion | depends on | +|---|---|---|---| +| B1 | `compat.opencl-headers`, `compat.opencl` (Khronos ICD loader, shared, `libOpenCL.so.1`) | a probe links and reports the host platform | — | +| B2 | `compat.opencl-runtime` (library farm + `HOST-SURFACE.txt`) | a manifest naming `libnvidia-opencl.so.1` resolves under mcpp's loader | B1 | +| B3 | pocl's manifest into the subos vendors directory, plus a sentinel linking the host's | a machine with a GPU sees both platforms; `OCL_ICD_VENDORS` **replaces** rather than adds, which is why one merged directory is the only correct shape | A4, B2 | + +### Tier C — rules and frameworks (mcpp) + +| # | task | criterion | depends on | +|---|---|---|---| +| C1 | `mcpp.build.spirv` + `examples/10-vulkan-compute` | three devices, one artifact — **done** | — | +| C2 | `mcpp.build.sycl` driving the `dpcpp` payload | a SYCL kernel runs on the CUDA backend | — | +| C3 | `mcpp.build.hip` | `HIP_PLATFORM=nvidia` kernel runs | `hip-runtime` payload | +| C4 | llama.cpp Vulkan lane | tokens on lavapipe with no GPU | C1, and a `glslc` payload or a flag translator | + +### Tier D — the ecosystem criterion + +`HOST-SURFACE.txt` contains **only** proprietary vendor userspace, and only on +machines that have such a driver, plus -- on a host that uses a proprietary +driver -- the libraries that driver links which no installed payload covers, +each recorded with the reason (no payload publishes the soname, or the host +copy is newer than the payload). A dependency on those payloads is not +declared by the adapter: the 21 self-built graphics packages are x86_64-only +and a hard dependency would refuse aarch64 outright, so the adapter fills +from what the sandbox holds and states the remainder. The cases: + +* no GPU, or an open driver — the file is empty, because the payload driver + path touches no host file; +* AMD or Intel — empty after A5; +* NVIDIA — the `libnvidia*` family and `libcuda.so.1`, reached through a + sentinel that links and does not copy. + +The file is produced by the package at install time, so the claim is a +measurement on the user's own machine rather than a statement in this +document. + +## 5. The dimensions the plan is judged on + +**Architecture.** The engine owns the graph and knows no vendor name; a rule +package owns the spelling; a payload owns the binaries; a sentinel owns the +irreducible host link. Four layers, and each of the four corrections in §2 was a +value that had leaked across one of those boundaries. + +**Stability.** Every widening in this round is provably inert on existing +builds: device extensions are absent from the default globs and were previously +a hard error; the ICD closure only adds symlinks for versioned sonames, which +the linker never resolves; the farm's gap-filling is monotone. + +**Elegance.** No new primitive was introduced. Shaders reach a rule package +through the same constrained glob that carries `.cu`; the SPIR-V header is an +ordinary `role = "source"` action; the target environment is read from the same +`accel` axis that carries `sm_89`. + +**User experience.** A build states what it is for once, in the manifest. +Failures name the declaration to add: the nvcc host-compiler bound, the missing +glslang, the accelerator that names no architecture. + +**Compatibility.** `mcpplibs:rules-cuda@0.1.0` stays in the index, frozen, with +a pointer to `mcpp:rules-cuda@0.2.0`; the pattern the `mcpplibs:llamacpp` → +`ggml-org:llamacpp` move established. + +**Cross-platform.** The gap is honest: the third-class packages are x86_64-only +today, which is why the farm falls back to the host rather than declaring them +as dependencies — a hard dependency on an x86_64-only package would break +aarch64 Linux outright. A3 closes it; until it does, the fallback is the +behaviour aarch64 already has. + +**Consistency.** Both rule packages now carry the module name their own +specification requires, and the check that enforces it was verified in both +directions. + +**Seamless upgrade.** No manifest key changed. A project that never mentions a +shader or an accelerator builds exactly as before, which the default-glob +assertion states over the whole list rather than over the two names that +happened to be there. + +**Test coverage.** e2e 609 asserts all 18 extensions with the table as its +denominator — and caught a `;`-vs-newline splitter in the rule package that was +correct for exactly one shader. + +## 6. Round 3 (2026-09-05): directives, corrections and the implementation schedule + +### 6.1 Directives added in this round + +1. Every library or tool a build reaches is published in `xim-pkgindex` or + `mcpp-index`. Nothing is taken from the host except proprietary driver + userspace, which is linked in place or fetched from the vendor's own URL. + Redistributable payloads carry a CN mirror (`gitcode.com/xlings-res/...`, + `gitcode.com/mcpp-res/...`). +2. Official build plugins live in one repository, `mcpp-community/mcpp-plugins`, + published as the package `mcpp:plugins`. Module names follow + `mcpp.rules.` for rule packages and `mcpp.tools.` for build-time + utilities; the members of the collection are selected through features. + The rule packages under `examples/` in the mcpp repository are withdrawn; + an example consumes the index package like any other project. +3. Chapter 20 of the manual is renamed from "Accelerators" to "Heterogeneous + Builds", with a subtitle naming GPU and AI accelerator targets and mixed + host/device compilation. The `accel` manifest key is unchanged. +4. Documentation and code comments carry no emoji or decorative symbols. +5. The plan below is the record of what is done; a task is closed only by its + criterion. + +### 6.2 Corrections to sections 2 to 5 + +* `mcpp.build.` was the wrong prefix for a plugin module. mcpp's own engine + modules are named `mcpp.build.plan`, `mcpp.build.prepare` and so on, so a + plugin under that prefix shares a family name with the engine it drives. The + rule-package specification withdrew `mcpp.rules.*` only because, at the time, + a host module's name was its bare package name and could not contain a dot + (I1 in `2026-08-29-build-rule-package-spec.md`). I1 is implemented, the + objection no longer holds, and `mcpp.rules.*` is reinstated. +* A feature-controlled collection needs one engine extension: a host-module + package contributes every module interface unit among its feature-resolved + sources, the lib root first, rather than the lib root alone. Nothing else in + the host-module path assumes one unit per package; `build_host_module` is + already per unit and the compile loop already accumulates BMIs in order. +* pocl's ICD does not need a merged vendors directory. The Khronos loader + enumerates `OCL_ICD_FILENAMES` and then the vendors directory + (`khrIcdOsVendorsEnumerate` in `loader/linux/icd_linux.c`), so a payload ICD + is added through the environment while the host's `/etc/OpenCL/vendors` + stays in effect. B3 of section 4 is replaced accordingly. + +### 6.3 Schedule + +Dependencies run downward within a repository and across the arrows noted. +Status is one of `done`, `open`, `deferred (reason)`. + +#### mcpp (single PR from `feat/vulkan-spirv`, version 2026.9.5.3) + +| # | task | criterion | depends on | status | +|---|---|---|---|---| +| M1 | Host-module packages contribute every interface unit among their feature-resolved sources | e2e 610: a consumer enabling `rules-a` imports `mcpp.rules.a`; without the feature the import fails as an unknown module; both features enable both; a package with neither a lib root nor a unit keeps today's diagnostic. Unit tests for the interface-unit detector | — | done | +| M2 | Module family `mcpp.rules.*` / `mcpp.tools.*`; specification I8 and section 7 corrected; docs 05 and 07 (both languages); examples 09 and 10 consume `mcpp:plugins` from the index | `grep -r 'mcpp\.build\.\(cuda\|spirv\)'` over docs, examples and tests is empty; the reserved-prefix warning still fires for `mcpplibs` and not for `mcpp` (e2e 309) | M1 | done | +| M3 | Chapter 20 renamed to `20-heterogeneous-builds.md` (both languages); every reference updated | `check_docs_style.sh` passes; no reference to `20-accelerators.md` remains | — | done | +| M4 | No emoji in docs, README, CHANGELOG, code comments, or the two design documents | a grep over the emoji ranges returns nothing outside program output strings | — | done | +| M5 | Version 2026.9.5.3 in `mcpp.toml` and `modules/versioning`; CHANGELOG; unit and e2e suites; PR; CI green; self-review | e2e summary has no failure other than 168 (pre-existing on `origin/main`); all CI jobs green on the PR head | M1–M4 | open | +| M6 | Release, mirror, index bump, bootstrap pin | both mirrors return 200 and identical bytes for every asset; `origin/main:pkgs/m/mcpp.lua` in xim-pkgindex names 2026.9.5.3 as `latest`; `.xlings.json` pin bumped | M5 | open | + +#### mcpp-plugins (new repository, single PR, version 0.1.0) + +| # | task | criterion | depends on | status | +|---|---|---|---|---| +| P1 | Repository with `mcpp.toml` (`mcpp:plugins`), `src/plugins.cppm` (`mcpp.plugins`), `rules/cuda.cppm` (`mcpp.rules.cuda`), `rules/spirv.cppm` (`mcpp.rules.spirv`), features `rules-cuda`, `rules-spirv`, README stating the naming rule and the mcpp floors | a consumer with `features = ["rules-spirv"]` builds a shader through it | M1 | done (PR #1 open) | +| P2 | CI: one consumer fixture per feature, built with the pinned mcpp | green on the PR head | P1, M6 for the spirv fixture | open | +| P3 | Release `v0.1.0`; GitHub archive and a GitCode release asset with identical bytes | both URLs return 200 and one sha256 | P2 | open | + +#### mcpp-index (PR #349) + +| # | task | criterion | depends on | status | +|---|---|---|---|---| +| I1 | `pkgs/m/mcpp.plugins.lua` (GLOBAL and CN URLs, floor 2026.9.5.3); `mcpplibs:rules-cuda` kept, marked superseded | `mcpp add mcpp:plugins` resolves in a sandbox | P3, M6 | open | +| I2 | `compat.vulkan-runtime` 2026.09.05: the pattern list is reduced to proprietary vendor userspace; an ICD's needs are computed by closing over the manifests' libraries; a farmed soname an installed payload also provides is re-pointed at the payload when the payload's versioned symbol set covers the host copy's (the `GLIBCXX`/`CXXABI` nodes of libstdc++ included); `HOST-SURFACE.txt` states the class of every entry | measured on this machine: 37 vendor entries, 20 payload substitutions, 8 host sonames no installed payload provides (`libbsd`, `libedit`, `libicudata`, `libicuuc`, `liblzma`, `libmd`, `libzstd`, `libtinfo`), 8 host Mesa ICDs, 3 host copies newer than the payload (`libdrm_amdgpu`, `libLLVM.so.20.1`, `libxml2`); example 10 still answers `12 24 36 48` | — | done | +| I3 | `compat.opencl-headers`, `compat.opencl` verified with a probe (`tests/examples/opencl`, a workspace member); `compat.opencl-runtime` 2026.09.05 farms the libraries the host manifests name, their closure and the vendor family, prefers payloads, records the surface; payload entries of `OCL_ICD_FILENAMES` are left to the payload | the probe enumerates the NVIDIA platform on this machine and zero platforms on a runner; the pocl platform once X2 is installed | X2 for the pocl half | open | +| I4 | CI green; merge; index artifact published | `Publish Index Artifact` green on the merge commit | I1–I3 | open | + +#### xim-pkgindex (PR #762) + +| # | task | criterion | depends on | status | +|---|---|---|---|---| +| X1 | `zstd`, `xz`, `icu`, `libedit`, `libmd`, `xcb-util`, `xcb-util-image`, `xcb-util-keysyms`, `xcb-util-renderutil`, `xcb-util-wm`, `xcb-util-cursor`: conda-forge repacks for x86_64 and aarch64, published to `xlings-res/` on both mirrors, recipes with `deps`, `exports`, `declare_libs`, headers and `.pc` | each installs; every `DT_NEEDED` of each payload library resolves inside payloads; CI green | — | open | +| X1b | `libbsd` built in the subos harness (not on conda-forge), x86_64 | same | X1 (`libmd`) | open | +| X2 | `pocl` 7.1 (CPU OpenCL) repacked for both architectures; its ICD reached through `OCL_ICD_FILENAMES` in the subos environment | `clinfo`-equivalent probe reports the pocl platform with no GPU | — | open | +| X3 | `mesa-lavapipe` aarch64 payload | `archs` lists both; the aarch64 tarball is on both mirrors | — | open | +| X4 | `xim:mesa` gains the Intel Vulkan driver | on an Intel machine `HOST-SURFACE.txt` has no driver entry | libclc and SPIRV-LLVM-Translator payloads | deferred: anv requires `intel_clc`, which needs a libclc and clang chain this index does not publish yet; the chain is a separate packaging round and is recorded here rather than approximated | +| X5 | CI green; merge; index artifact published | `Publish Index Artifact` green on the merge commit | X1–X3 | open | + +#### Verification + +| # | task | criterion | depends on | status | +|---|---|---|---|---| +| V1 | Fresh sandbox (`xlings subos --sandbox --cmd`), CN mirror configured for both mcpp and xlings: install mcpp 2026.9.5.3, build examples 09 and 10 against `mcpp:plugins`, run 10 on the lavapipe payload, run the OpenCL probe on pocl | `12 24 36 48` from example 10 with no GPU; the pocl platform enumerated; `HOST-SURFACE.txt` empty in the sandbox | M6, P3, I4, X5 | open | +| V2 | The same on this host with the GPU: example 10 on the host ICD, example 09 on both routes | `12 24 36 48` in every case; `HOST-SURFACE.txt` lists proprietary userspace only | V1 | open | diff --git a/.agents/docs/2026-09-05-multi-device-ecosystem-design.md b/.agents/docs/2026-09-05-multi-device-ecosystem-design.md index 19dbdd1c..402cf062 100644 --- a/.agents/docs/2026-09-05-multi-device-ecosystem-design.md +++ b/.agents/docs/2026-09-05-multi-device-ecosystem-design.md @@ -18,12 +18,12 @@ | 5 | 「CUDA kernel 的执行只能上真卡」(§8.5 第三档) | 错。**chipStar v1.3.0**(CUDA/HIP → SPIR-V → OpenCL)配 **PoCL v7.2** 的 CPU 设备,这条链是活的 | 两者均在 2026-09 有发布/提交 | | 6 | 「Metal 本机无 macOS」 | 理由不成立。仓库有四条 macOS CI | `ci-macos`、`ci-macos-e2e`、`bootstrap-macos`、`macOS ARM64 — xlings LLVM end-to-end` | -⭐ 六条里有五条的形状相同:**我把「我手边没有」写成了「做不到」**,而 xlings 生态 +六条里有五条的形状相同:**我把「我手边没有」写成了「做不到」**,而 xlings 生态 的存在意义正是消除「手边有没有」这个变量。 --- -## 0.1 ⚠️⚠️ 已发布的示例违反了本文的规则,两处,必须先修 +## 0.1 已发布的示例违反了本文的规则,两处,必须先修 `examples/09-cuda-kernel`(2026.9.5.1 已发布)是**教用户怎么做**的东西, 而它教的两件事都是错的。 @@ -32,13 +32,13 @@ ```toml [xlings] -deps = [{ linux = "libcuda-host-link" }] # ⚠️ 2026.9.3.1 起已被取代 +deps = [{ linux = "libcuda-host-link" }] # 2026.9.3.1 起已被取代 ``` `[xlings.workspace]` 才是**那一张表**(#548)。`deps` 仍被接受,所以不报错 —— **这正是它危险的地方**:示例不会红,而抄它的人写下的是过时形式。 -⚠️ 且这个键在文档里**到处都是**:英文侧 `docs/05` ×5、`07` ×2、`13` ×2、 +且这个键在文档里**到处都是**:英文侧 `docs/05` ×5、`07` ×2、`13` ×2、 `15` ×1、`17` ×3、`18` ×1;中文侧 `05` ×3、`07` ×2、`13` ×2、`17` ×2、`18` ×1。 ⇒ **从全部用户文档中删除,不留「也可以这么写」** —— 留着就是留一个误用入口。 (`[xlings].subos` 与 `[xlings.workspace]` 不受影响,它们是当前形式。) @@ -59,7 +59,7 @@ c.push_back("/usr/bin/nvcc"); c.push_back("/usr/include/crt/host_config.h"); ``` -⚠️⚠️ **示例通篇依赖宿主的 CUDA 工具包**,而本文 §1 的规则说 host 只留 +**示例通篇依赖宿主的 CUDA 工具包**,而本文 §1 的规则说 host 只留 `libcuda.so.1`。**我写了一个示范违反自己规则的示例。** ⇒ 正确形态,在 §3.1 的载荷落地后: @@ -80,7 +80,7 @@ accel = "cuda12.9+{sm_89} ptx>=89" 规则包侧:nvcc 与 `crt/host_config.h` 都从 `mcpp::xpkg_dir("xim","cuda-nvcc")` 得到,**一个 `/usr` 都不出现**。 -⭐ 这两条排在 §8 的最前面 —— 它们不是新功能,是**已发布的错误示范**, +这两条排在 §8 的最前面 —— 它们不是新功能,是**已发布的错误示范**, 每多一天就多一批照着抄的人。 ## 1. 一条规则,以及它的判据 @@ -95,7 +95,7 @@ accel = "cuda12.9+{sm_89} ptx>=89" 2. **它的版本边界是被问出来的还是被假设的?** 每一条 host 依赖必须有一个 **运行期可查的问法**,mcpp 用那个问法回答,而不是内置一张表。 -⭐ 这是同一条原则的第三次应用: +这是同一条原则的第三次应用: - `crt/host_config.h` → 宿主编译器上界(已落地) - `nvcc --dryrun` → 设备编译器能否够到自己的后端(已落地) - `cuDriverGetVersion` → **驱动能跑多新的工具包**(本文新增,见 §2.1) @@ -118,7 +118,7 @@ accel = "cuda12.9+{sm_89} ptx>=89" 其余一切 —— nvcc、cudart、nvrtc、cuBLAS、hipcc、DPC++、AdaptiveCpp、 PoCL、lavapipe、SPIR-V 工具、chipStar —— **全部进 xim**。 -### 2.1 ⭐⭐ 驱动版本决定可用的工具包版本 —— 已实测,失败很晚 +### 2.1 驱动版本决定可用的工具包版本 —— 已实测,失败很晚 **实测(2026-09-05,本机 RTX 4080 / 驱动 550.144.03):** @@ -126,16 +126,16 @@ PoCL、lavapipe、SPIR-V 工具、chipStar —— **全部进 xim**。 | 用什么编 | 编译 | 链接 | 运行 | |---|---|---|---| -| CUDA 13.3 载荷 | ✅ 干净 | ✅ 干净 | ❌ `runtime=13030 driver=12040`
`cudaMalloc: CUDA driver version is insufficient for CUDA runtime version` | -| 宿主 CUDA 12.0 | ✅ | ✅ | ✅ `result: 12 24 36 48` | +| CUDA 13.3 载荷 | 干净 | 干净 | `runtime=13030 driver=12040`
`cudaMalloc: CUDA driver version is insufficient for CUDA runtime version` | +| 宿主 CUDA 12.0 | yes | | `result: 12 24 36 48` | -⚠️⚠️ **编译与链接全干净,失败在第一次 `cudaMalloc`。** 这与 §1 那个「为 sm_90 编的 +**编译与链接全干净,失败在第一次 `cudaMalloc`。** 这与 §1 那个「为 sm_90 编的 库被 sm_86 消费」是同一类失败:晚、且消息不指向任何一次人为选择。 ⇒ **处置:两条版本线并存,由 xim 的多版本管理承担。** 13.3 线与 12.9 线各自完整, mcpp 在构建前问驱动要一个数,选能跑的那条。**这不是权变,是 xim 本来就是干这个的。** -### 2.2 ⭐⭐ 载荷是一个闭包,而这个闭包要被**发现**,不能被猜 +### 2.2 载荷是一个闭包,而这个闭包要被**发现**,不能被猜 同一次实测暴露的第二件事:CUDA 13.x 把组件拆得比 12.x 细得多。 只装 `cuda_nvcc`(30.1 MB)会得到: @@ -148,17 +148,17 @@ sh: 1: .../bin/../nvvm/bin/cicc: not found 另有 `libnvptxcompiler`、`libnvfatbin`、`cuda_culibos`、`cccl`。 一个**能编东西的** nvcc 是七个组件的闭包,不是一个包。 -⭐⭐ **补齐它的办法不是抄一张目录表,而是用已经落地的那条 doctor 检查驱动它** —— +**补齐它的办法不是抄一张目录表,而是用已经落地的那条 doctor 检查驱动它** —— `nvcc --dryrun` 报出第一个解析不到的裸名,装上带它的组件,再问一次。 这与 §15.7 是同一个机制:**那条检查原本用来抓坏掉的宿主,它同样抓不完整的载荷。** -### 2.3 ⭐ 载荷带着自己的 `nvcc.profile`,因此没有沙箱那个病 +### 2.3 载荷带着自己的 `nvcc.profile`,因此没有沙箱那个病 Debian 把 `nvcc.profile` 做成指向 `/etc` 的符号链接,沙箱替换 `/etc` 就断 (前文 §15.7)。**redist 载荷把 `nvcc.profile` 放在自己的 `bin/` 里** —— 解到任意前缀都自定位。实测 `nvcc --dryrun` 声明的 PATH 指向载荷自己的目录。 -⭐ 顺带量到:**载荷自带的 `host_config.h` 上界是 gcc ≤ 15 / clang ≤ 21**, +顺带量到:**载荷自带的 `host_config.h` 上界是 gcc ≤ 15 / clang ≤ 21**, 而宿主那个 CUDA 12.0 是 gcc ≤ 12。⇒ **载荷不只是「不依赖 host」,它的宿主编译器 上界还宽得多** —— 这是「进生态」的直接收益,不是理念收益。 @@ -167,7 +167,7 @@ Debian 把 `nvcc.profile` 做成指向 `/etc` 的符号链接,沙箱替换 `/etc 版本号一律用上游的。日期版本只留给「内容是本生态自己写的、没有上游产物」的适配器 (与 `compat.glx-runtime` / `compat.vulkan-runtime` 同规)。 -### 3.0 ⭐⭐ 载荷边界由 manifest 的 `license` 字段决定,不由我判断 +### 3.0 载荷边界由 manifest 的 `license` 字段决定,不由我判断 CUDA 13.3.0 的 linux-x86_64 组件共 **43 个、4.19 GB**,manifest 把它们分成三类许可, 而这三类的处置各不相同: @@ -178,7 +178,7 @@ CUDA 13.3.0 的 linux-x86_64 组件共 **43 个、4.19 GB**,manifest 把它们 | `NVIDIA SLA` | **nsight_systems 1074.8 MB**、**nsight_compute 336.5 MB** | 1.41 GB | **进 xim**,GLOBAL url 直指 NVIDIA 自己的 CDN | | `NVIDIA Driver` | nvidia_driver 528.9 MB、cuda_compat | 0.53 GB | **唯一留在 host 的那层** | -⭐ 第二行不是让步,但我原先的说法过绝对,已按 xim 的规范改正: +第二行不是让步,但我原先的说法过绝对,已按 xim 的规范改正: - xim **推荐**官方二进制走 `xpm.source = "xlings-res"`,那要求 `github.com/xlings-res/` 与 `gitcode.com/xlings-res/` 两腿都有同名 @@ -189,10 +189,10 @@ CUDA 13.3.0 的 linux-x86_64 组件共 **43 个、4.19 GB**,manifest 把它们 ⇒ **CUDA 组件走 URL template 直连 NVIDIA 的 CDN**:manifest 已给出稳定 URL 与 sha256,不产生再分发行为,SLA 与 Toolkit 两类许可都成立。 -⚠️ 代价是 CN 用户直连 NVIDIA CDN;若实测过慢,再单独决定是否把第①档 +代价是 CN 用户直连 NVIDIA CDN;若实测过慢,再单独决定是否把第①档 (150 MB)镜像进 xlings-res —— **那是一个可以后加的优化,不是前置条件**。 -⚠️ **「体积」不是任何一条的理由。** 去掉 driver 后 **3.66 GB 全部进 xim**, +**「体积」不是任何一条的理由。** 去掉 driver 后 **3.66 GB 全部进 xim**, 拆成约 40 个可独立安装的组件包 —— 一次普通 CUDA 构建只装第①档的 **150 MB**, 要 profile 的人才装 nsight。**这正是组件级拆包存在的意义。** @@ -202,10 +202,10 @@ sha256,不产生再分发行为,SLA 与 Toolkit 两类许可都成立。 | xim 包 | 版本 | 说明 | |---|---|---| -| `xim:llvm` | **22.1.8**(已装) | ⭐ **加速器路线的默认编译器**(§6.3.5)。它同时是宿主编译器、CUDA 设备编译器(`-x cuda`)、SPIR-V 前端 | +| `xim:llvm` | **22.1.8**(已装) | **加速器路线的默认编译器**(§6.3.5)。它同时是宿主编译器、CUDA 设备编译器(`-x cuda`)、SPIR-V 前端 | | `xim:gcc` | 13.3.0 / 15.1.0 / 16.1.0(已装) | 岛形态里设备 TU 的 `-ccbin` 备选;普通 C++ 工程的默认不变 | -⭐ 主路线的编译器**一个新包都不用加** —— 它已经是 mcpp 生态的一等公民。 +主路线的编译器**一个新包都不用加** —— 它已经是 mcpp 生态的一等公民。 这正是「以 LLVM 为主」在工程上便宜的原因。 **① CUDA 编译与运行必需 —— 150 MB** @@ -219,7 +219,7 @@ sha256,不产生再分发行为,SLA 与 Toolkit 两类许可都成立。 | `xim:cuda-cccl` | — | 12.9.27 | 1.0 MB | | `xim:cuda-nvtx` / `xim:cuda-profiler-api` | 13.3.29 / 13.3.27 | | 0.1 MB | -两条线并存,由 §2.1 的驱动判据选。⚠️ 本机驱动只到 CUDA 12.4 ⇒ 走 12.9 线。 +两条线并存,由 §2.1 的驱动判据选。本机驱动只到 CUDA 12.4 ⇒ 走 12.9 线。 **② 算子库 —— 按 shared / static 拆开** @@ -247,7 +247,7 @@ sha256,不产生再分发行为,SLA 与 Toolkit 两类许可都成立。 | `xim:nsight-systems` | 2026.1.3.243 | 1074.8 MB | NVIDIA SLA | | `xim:nsight-compute` | 2026.2.0.7 | 336.5 MB | NVIDIA SLA | -⭐ 少了这一档,mcpp 上的 CUDA 开发只能编不能查 —— **那才是偷工减料**。 +少了这一档,mcpp 上的 CUDA 开发只能编不能查 —— **那才是偷工减料**。 `mcpp` 侧接线:`mcpp run --profile nsys` / `--check sanitizer` 走既有的 runner 与 action 机制,不新增专用命令。 @@ -258,11 +258,11 @@ runner 与 action 机制,不新增专用命令。 | `xim:dpcpp` | **7.1.0**(2026-09-02) | 207 MB(linux) | `intel/llvm` release `sycl_linux.tar.gz` | | `xim:adaptivecpp` | **25.10.0** | 源码构建 | 无 release 资产 ⇒ 依赖 `xim:llvm` 构建 | -⭐ **已核实:官方 Linux 资产已含 CUDA 与 HIP 后端。** v7.1.0 的构建配置是 +**已核实:官方 Linux 资产已含 CUDA 与 HIP 后端。** v7.1.0 的构建配置是 `--cuda --hip`,release notes 的测试矩阵里有 "NVIDIA CUDA BACKEND on NVIDIA GeForce RTX 3090"。⇒ Linux 不需要自建。 -⚠️ **Windows 不含**:同一份 release notes 写着 "HIP & CUDA plugins on Windows are +**Windows 不含**:同一份 release notes 写着 "HIP & CUDA plugins on Windows are not being built"。Windows 上自建,而**版本号仍用上游 tag `7.1.0`** —— 见 §3.5。 ### 3.3 CPU 设备与模拟器(这些也进载荷,不依赖 host) @@ -274,16 +274,16 @@ not being built"。Windows 上自建,而**版本号仍用上游 tag `7.1.0`** | `xim:chipstar` | **1.3.0** | CUDA/HIP → SPIR-V → OpenCL/Level Zero | | `xim:spirv-tools` / `xim:glslang` | 最新 | SPIR-V 汇编与 GLSL→SPIR-V | -⭐⭐ **模拟器进载荷,是「无卡 CI 也能跑 kernel」这件事从偶然变成契约的唯一办法。** +**模拟器进载荷,是「无卡 CI 也能跑 kernel」这件事从偶然变成契约的唯一办法。** 本机现在能跑 lavapipe 是因为发行版恰好装了它;进 xim 之后,任何机器都能跑。 ### 3.4 ROCm / HIP | xim 包 | 版本 | 说明 | |---|---|---| -| `xim:hip-runtime` / `xim:hipcc` | ROCm **7.14.0**(2026-07-16) | ⭐ HIP 可以 `HIP_PLATFORM=nvidia` 走 CUDA 后端 ⇒ **本机 4080 就能验 HIP 前端** | +| `xim:hip-runtime` / `xim:hipcc` | ROCm **7.14.0**(2026-07-16) | HIP 可以 `HIP_PLATFORM=nvidia` 走 CUDA 后端 ⇒ **本机 4080 就能验 HIP 前端** | -### 3.5 ⚠️⚠️ 版本对齐是硬规则,自建也不例外 +### 3.5 版本对齐是硬规则,自建也不例外 > **凡是有上游版本的,一律用上游版本;自己编的不改号。** @@ -296,7 +296,7 @@ not being built"。Windows 上自建,而**版本号仍用上游 tag `7.1.0`** 日期版本(`2026.09.05` 这种)**只留给内容是本生态自己写的、没有上游产物的适配器**, 与 `compat.glx-runtime` / `compat.vulkan-runtime` 同规。 -⚠️ 这条已经被违反过一次:`compat.cuda-runtime` 的名字与内容不符(§4.1), +这条已经被违反过一次:`compat.cuda-runtime` 的名字与内容不符(§4.1), 虽然版本形态是对的。 --- @@ -310,8 +310,8 @@ xim 装载荷,mcpp-index 描述「怎么对它构建」。实测:xim-pkgindex 17 | 包 | 状态 | 动作 | |---|---|---| -| `compat.cuda-driver` | ⚠️ 现名 `compat.cuda-runtime`,**名字与内容不符** | **改名**。NVIDIA 词汇里 "CUDA Runtime" 专指 `libcudart`;本包 farm 的是驱动,其 `capabilities`/`provides` 已写作 `cuda.driver` | -| — | ⚠️ `repo` 字段指向 `openxlings/xim-pkgindex` | **改正**。同族两包指向被适配物的上游 | +| `compat.cuda-driver` | 现名 `compat.cuda-runtime`,**名字与内容不符** | **改名**。NVIDIA 词汇里 "CUDA Runtime" 专指 `libcudart`;本包 farm 的是驱动,其 `capabilities`/`provides` 已写作 `cuda.driver` | +| — | `repo` 字段指向 `openxlings/xim-pkgindex` | **改正**。同族两包指向被适配物的上游 | | `compat.vulkan-icd` | 新增 | farm host ICD;缺失时回落 `xim:mesa-lavapipe` | | `compat.opencl-icd` | 新增 | 同上,回落 `xim:pocl` | @@ -319,7 +319,7 @@ xim 装载荷,mcpp-index 描述「怎么对它构建」。实测:xim-pkgindex 17 | 包 | 来源 | 说明 | |---|---|---| -| `mcpplibs.rules-cuda` | mcpp release tarball 的 `examples/09-cuda-kernel/rules-cuda` | ⭐ **一个仓库都不用新建**,与 `grpcgen` 同形。搬进索引后示例改成消费索引里那一份,避免两份 | +| `mcpplibs.rules-cuda` | mcpp release tarball 的 `examples/09-cuda-kernel/rules-cuda` | **一个仓库都不用新建**,与 `grpcgen` 同形。搬进索引后示例改成消费索引里那一份,避免两份 | | `mcpplibs.rules-hip` | 同一 tarball | hipcc 的 `--offload-arch` | | `mcpplibs.rules-spirv` | 同一 tarball | `glslc` / `slangc` → SPIR-V,产物是数据不是目标文件 | @@ -336,17 +336,17 @@ xim 装载荷,mcpp-index 描述「怎么对它构建」。实测:xim-pkgindex 17 | 后端 | 编译期判据 | kernel 真跑的 lane | 本机可用? | |---|---|---|---| -| **CUDA / nvcc** | 无需设备 | **本机 RTX 4080**(compute 8.9) | ✅ 已用过 | -| CUDA(无卡环境) | — | `xim:chipstar` + `xim:pocl` CPU 设备 | ✅ 待接 | -| **SYCL / DPC++ (icpx)** | 无需设备 | ② CUDA 后端 → **本机 4080**,⭐ **已实测跑通**(§6.4.5,`seam: 11 22 33 44`) | ✅ 已验证 | -| **SYCL / AdaptiveCpp** | 无需设备 | `--acpp-targets=omp`,**CPU 上真跑** | ✅ | -| **Vulkan compute** | 无需设备 | ① **lavapipe(已实测跑通)** ② **本机 4080** | ✅ **两条 lane 同机** | -| **OpenCL** | 无需设备 | ① `xim:pocl` CPU ② 本机 `nvidia.icd` | ✅ 两条 | -| **HIP / ROCm** | 无需设备 | ① `HIP_PLATFORM=nvidia` → **本机 4080** ② chipStar+PoCL | ✅ | -| **Metal** | 无需设备 | macOS CI runner | ✅ 四条 macOS CI | -| Level Zero | 无需设备 | 无 Intel GPU ⇒ CPU device | ⚠️ 仅 CPU | - -### 5.1 ⭐⭐ Vulkan 那一行已经跑通了,记在这里作为 lane 的样板 +| **CUDA / nvcc** | 无需设备 | **本机 RTX 4080**(compute 8.9) | 已用过 | +| CUDA(无卡环境) | — | `xim:chipstar` + `xim:pocl` CPU 设备 | 待接 | +| **SYCL / DPC++ (icpx)** | 无需设备 | ② CUDA 后端 → **本机 4080**,**已实测跑通**(§6.4.5,`seam: 11 22 33 44`) | 已验证 | +| **SYCL / AdaptiveCpp** | 无需设备 | `--acpp-targets=omp`,**CPU 上真跑** | yes | +| **Vulkan compute** | 无需设备 | ① **lavapipe(已实测跑通)** ② **本机 4080** | **两条 lane 同机** | +| **OpenCL** | 无需设备 | ① `xim:pocl` CPU ② 本机 `nvidia.icd` | 两条 | +| **HIP / ROCm** | 无需设备 | ① `HIP_PLATFORM=nvidia` → **本机 4080** ② chipStar+PoCL | yes | +| **Metal** | 无需设备 | macOS CI runner | 四条 macOS CI | +| Level Zero | 无需设备 | 无 Intel GPU ⇒ CPU device | 仅 CPU | + +### 5.1 Vulkan 那一行已经跑通了,记在这里作为 lane 的样板 本机同时有两个 Vulkan 设备: @@ -364,10 +364,10 @@ checked 256 elements, wrong=0 ; sample: 1 4 7 766 KERNEL RAN ON CPU AND THE RESULT IS CORRECT ``` -⭐ **对照也跑了**:只给 `nvidia_icd.json` 时同一程序报 `NO CPU COMPUTE DEVICE` 退 1, +**对照也跑了**:只给 `nvidia_icd.json` 时同一程序报 `NO CPU COMPUTE DEVICE` 退 1, 而这台机器确有可用的 4080 ⇒ 对照不是空的,成功确实来自软件设备。 -⭐⭐ 这就是每条 lane 要达到的样子:**一个真的 dispatch、一个真的结果、一个能让它 +这就是每条 lane 要达到的样子:**一个真的 dispatch、一个真的结果、一个能让它 变红的对照。** 不是「设备列出来了」。 --- @@ -390,7 +390,7 @@ KERNEL RAN ON CPU AND THE RESULT IS CORRECT | 一个载荷能不能在这台机器上用 | **核心**(机制) | | 用什么函数去问驱动要那个数 | **插件** | -⭐ 判据:**核心里不出现任何厂商名字。** 现有实现已经违反过一次 —— `mcpp.toolchain.devicehost` +判据:**核心里不出现任何厂商名字。** 现有实现已经违反过一次 —— `mcpp.toolchain.devicehost` 读的是 `crt/host_config.h`、`unreachable_device_stage` 调的是 `nvcc`,两者都写死了 CUDA。⇒ 本轮要把它们改成**由规则包提供探针、核心只负责调用与报告**。 @@ -416,32 +416,32 @@ CUDA。⇒ 本轮要把它们改成**由规则包提供探针、核心只负责 | `rules-spirv` | `glslc` / `slangc` → SPIR-V;产物是数据不是对象 | | `rules-metal` | `metal` / `metallib` | -⚠️ **既有实现的两处要迁走**:`devicehost.cppm` 的 `parse_host_config` 与 doctor 的 +**既有实现的两处要迁走**:`devicehost.cppm` 的 `parse_host_config` 与 doctor 的 `unreachable_device_stage` 现在都写死 CUDA。改法是核心提供 「向规则包要一个探针、执行它、报告结果」的通道,**CUDA 的那两个探针搬进 `rules-cuda`**。 -⭐ 收益不是洁癖:AMD/Intel/Apple 各有自己的上界与后端可达性问题,**核心不该长四次**。 +收益不是洁癖:AMD/Intel/Apple 各有自己的上界与后端可达性问题,**核心不该长四次**。 -### 6.3 ⭐ 这条分界对本方案其余部分的影响 +### 6.3 这条分界对本方案其余部分的影响 - §2.1 的驱动配对 ⇒ C-5(机制)+ `rules-cuda`(探针),而不是核心里加一个 CUDA 检查; - §2.2 的载荷闭包发现 ⇒ 规则包报出缺什么,核心只负责把消息变成一条可执行的建议; - §7 的九个框架 ⇒ 每一个都只依赖「核心 + 对应规则包」,**不要求核心认识它们**。 -## 6.3.5 ⭐⭐ 主路线是 LLVM/clang,不是 gcc —— 这是生态事实决定的 +## 6.3.5 主路线是 LLVM/clang,不是 gcc —— 这是生态事实决定的 主次不该按偏好定。逐个核实每个后端的编译器**实体**是什么: | 后端 | 编译器 | 是不是 LLVM | |---|---|---| -| HIP / ROCm | `hipcc` | ✅ **AMD 的 LLVM fork**(`ROCm/llvm-project`) | -| SYCL / DPC++ | `icpx` | ✅ **Intel 的 LLVM fork**(`intel/llvm`,仓库自述 "Home for Intel LLVM-based projects") | -| SYCL / AdaptiveCpp | `acpp` | ✅ 自述 "a powerful, generic **LLVM JIT compiler**" | -| Vulkan / SPIR-V | clspv / slang / glslang 链路 | ✅ | -| Metal | `metal` | ✅ clang 系 | -| OpenMP offload | clang 的更成熟 | ✅ | -| **CUDA** | `nvcc`(EDG 前端) | ❌ **唯一的例外** | +| HIP / ROCm | `hipcc` | **AMD 的 LLVM fork**(`ROCm/llvm-project`) | +| SYCL / DPC++ | `icpx` | **Intel 的 LLVM fork**(`intel/llvm`,仓库自述 "Home for Intel LLVM-based projects") | +| SYCL / AdaptiveCpp | `acpp` | 自述 "a powerful, generic **LLVM JIT compiler**" | +| Vulkan / SPIR-V | clspv / slang / glslang 链路 | yes | +| Metal | `metal` | clang 系 | +| OpenMP offload | clang 的更成熟 | yes | +| **CUDA** | `nvcc`(EDG 前端) | **唯一的例外** | -⭐⭐ 而**唯一的例外也有两条通向 LLVM 的路,两条都已实测**: +而**唯一的例外也有两条通向 LLVM 的路,两条都已实测**: 1. nvcc **接受 clang 作 `-ccbin`** —— 本文的编译实验全程用的就是 clang; 2. clang **原生编 CUDA**(`-x cuda`),此时根本没有 nvcc,也就没有宿主编译器上界。 @@ -462,17 +462,17 @@ CUDA。⇒ 本轮要把它们改成**由规则包提供探针、核心只负责 | `rules-*` 的形态 | 每个后端一套厂商驱动 | **五个后端共用 clang 驱动的形状**,差别只在 flag 与 target 拼法 | | gcc 的位置 | 默认 | **岛形态里设备 TU 的 `-ccbin` 备选**,以及不含设备代码的普通工程照旧 | -⚠️ **不是弃用 gcc。** mcpp 主体仍以 gcc 自举,普通 C++ 工程不受影响; +**不是弃用 gcc。** mcpp 主体仍以 gcc 自举,普通 C++ 工程不受影响; 改的只是**加速器这条线的默认**。§7.9 场景 1(纯 C++)一个字都不用改。 -⚠️ 代价要写明:clang 对**新 CUDA 版本**的支持滞后(实测 clang 22 只部分支持到 +代价要写明:clang 对**新 CUDA 版本**的支持滞后(实测 clang 22 只部分支持到 CUDA 12.9,clang 18 只到 12.3)。⇒ **需要最新 CUDA 特性的工程走 nvcc 路线**, 由 `rules-cuda` 按「clang 版本 × CUDA 版本」的可用配对来选, 两条路线共存由 xim 的多版本管理承担。 --- -## 6.4 ⭐⭐ 宿主编译器上界不是限制,是四条路里的一次选择(已实测) +## 6.4 宿主编译器上界不是限制,是四条路里的一次选择(已实测) 问题:CUDA 13.3 声明 gcc ≤ 15 / clang ≤ 21,CUDA 12.9 声明 gcc ≤ 14, 而 mcpp 的工具链载荷是 **gcc 16.1.0**。这条上界会不会把混合编程钉死? @@ -484,7 +484,7 @@ CUDA 12.9,clang 18 只到 12.3)。⇒ **需要最新 CUDA 特性的工程走 nvc 存在,但它只是关掉检查;真正的风险是新版 libstdc++ 头文件里的构造 EDG 前端解析不了。 ⇒ **可用作最后手段,不作为设计的答案。** -### 路线 B —— 岛形态把上界关进设备 TU 里(⭐ 已经在用了) +### 路线 B —— 岛形态把上界关进设备 TU 里(已经在用了) 设备 TU 的 `-ccbin` 与项目的宿主编译器**本来就不必是同一个**: @@ -495,14 +495,14 @@ CUDA 12.9,clang 18 只到 12.3)。⇒ **需要最新 CUDA 特性的工程走 nvc 两者在 extern "C" 的缝上相遇 —— C ABI 跨编译器版本稳定 ``` -⭐ mcpp 的 registry 里现成就有 **13.3.0 / 15.1.0 / 16.1.0** 三个 gcc 载荷。 +mcpp 的 registry 里现成就有 **13.3.0 / 15.1.0 / 16.1.0** 三个 gcc 载荷。 `rules-cuda` 已经在做「挑一个上界内的 ccbin」这件事,只是现在挑的是 host 上的; 改成**从 xim 挑**,这条路就完全在生态内闭合。 -⚠️ 代价是缝必须干净:`extern "C"`、不跨缝传 std 类型。 +代价是缝必须干净:`extern "C"`、不跨缝传 std 类型。 **这正是形态 A 本来就要求的纪律**,不是额外负担。 -### 路线 C —— 用 clang 直接编 CUDA,上界消失(⭐⭐ 已实测通过) +### 路线 C —— 用 clang 直接编 CUDA,上界消失(已实测通过) clang 原生支持 `-x cuda`。此时**宿主编译器就是 clang 自己**, 「nvcc 接不接受这个宿主编译器」这个问题不存在。 @@ -513,7 +513,7 @@ clang 原生支持 `-x cuda`。此时**宿主编译器就是 clang 自己**, $ ~/.xlings/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++ -x cuda t.cu \ --cuda-path= --cuda-gpu-arch=sm_89 -std=c++17 -c clang++: warning: CUDA version 12.9 is only partially supported - ✓ 编译成功 + 编译成功 ``` **全程没有任何宿主编译器上界。** 产物的动态依赖闭包也是干净的: @@ -524,7 +524,7 @@ libc++abi.so.1 → xim-x-llvm/22.1.8/lib/... libc.so.6 → xim-x-glibc/2.39/lib64/... ``` -⚠️ **约束是对称的,不是消失的**:nvcc 限制*宿主编译器*版本,clang 限制 *CUDA* 版本。 +**约束是对称的,不是消失的**:nvcc 限制*宿主编译器*版本,clang 限制 *CUDA* 版本。 实测 clang 18 只到 CUDA 12.3(遇 13.3 报 `fatbinary fatal: Unknown option '-image'`), clang 22 部分支持到 12.9。⇒ **两条约束都由 xim 的多版本管理承担**, `rules-cuda` 按「clang 版本 × CUDA 版本」选一对可用的。 @@ -534,7 +534,7 @@ clang 22 部分支持到 12.9。⇒ **两条约束都由 xim 的多版本管理 上面那个二进制里,glibc、libc++、cudart、nvcc/clang 全部来自载荷; host 参与的只有 `libcuda.so.1`。 -⚠️⚠️ **但不能用 `LD_LIBRARY_PATH` 去够它** —— 实测把宿主 `/lib/x86_64-linux-gnu` +**但不能用 `LD_LIBRARY_PATH` 去够它** —— 实测把宿主 `/lib/x86_64-linux-gnu` 放进 `LD_LIBRARY_PATH`,进程 **segfault(exit 139)**,因为宿主 glibc 与载荷 glibc 在同一个地址空间里撞了。**正解就是 `compat.cuda-driver` 现在做的事**: farm 出**单独一条** `libcuda.so.1` 符号链接,经 `runtime.library_dirs` 进搜索路径。 @@ -549,7 +549,7 @@ $ ldd tC6 | grep -c '/usr/lib\|/lib/x86_64-linux-gnu' libc++.so.1 → xim-x-llvm/22.1.8/... libc.so.6 → xim-x-glibc/2.39/... ``` -⚠️ **但驱动仍然 dlopen 不到。** 宿主的 `libcuda.so.1` 自己声明五个 `DT_NEEDED`: +**但驱动仍然 dlopen 不到。** 宿主的 `libcuda.so.1` 自己声明五个 `DT_NEEDED`: ``` libm.so.6 libc.so.6 libdl.so.2 libpthread.so.0 librt.so.1 @@ -558,11 +558,11 @@ libm.so.6 libc.so.6 libdl.so.2 libpthread.so.0 librt.so.1 私有 loader 必须能在**同一个闭包里**解析这五个 —— farm 一条链接只解决了 「找得到 libcuda」,没解决「libcuda 找得到它自己要的东西」。 -⭐ `compat.cuda-driver` 之所以在真实工程里能用(`examples/09-cuda-kernel` 实测 +`compat.cuda-driver` 之所以在真实工程里能用(`examples/09-cuda-kernel` 实测 输出 `12 24 36 48`),是因为 **mcpp 的链接模型把 glibc 载荷目录也放进了那条搜索 路径**。手搓 `clang -Wl,-rpath,...` 绕过了这个模型,于是缺了那一半。 -⚠️⚠️ **由此:判据 C0 必须经 mcpp 跑,不能手搓 clang。** 被测的对象包含 mcpp 的 +**由此:判据 C0 必须经 mcpp 跑,不能手搓 clang。** 被测的对象包含 mcpp 的 链接模型本身;绕过它去测,测的是另一个东西。 (旁证:手搓产物的 `INTERP` 请求 glibc **2.39** 却解析到 **2.44** —— 一个 mcpp 不会产生的不一致。) @@ -573,7 +573,7 @@ libm.so.6 libc.so.6 libdl.so.2 libpthread.so.0 librt.so.1 |---|---|---|---| | A `-allow-unsupported-compiler` | 关掉检查 | 可能真的编不过 | 最后手段 | | B 岛形态 + xim 里挑 ccbin | **只约束设备 TU** | 缝要干净(本来就要) | gcc 工程 / 需要 `-gencode` 精细控制时 | -| C clang 编 CUDA | **消失**,换成 CUDA 版本上界 | clang 对新 CUDA 支持滞后 | ⭐ **默认路线**(见 §6.3.5) | +| C clang 编 CUDA | **消失**,换成 CUDA 版本上界 | clang 对新 CUDA 支持滞后 | **默认路线**(见 §6.3.5) | | D 全载荷 | — | 无 | 上面三条的共同底座 | ⇒ **不是「混合编程受限」,是引擎不该替用户选。** 核心提供「设备 TU 可以有自己的 @@ -581,7 +581,7 @@ libm.so.6 libc.so.6 libdl.so.2 libpthread.so.0 librt.so.1 --- -## 6.4.5 ⭐⭐⭐ 形态 B × modules 已实测:缝形态是**唯一**可行,而它统一了全部后端 +## 6.4.5 形态 B × modules 已实测:缝形态是**唯一**可行,而它统一了全部后端 这是方案里最后一格「不知道会怎样」。已测完(DPC++ 7.1.0 载荷 + 本机 RTX 4080)。 @@ -589,10 +589,10 @@ libm.so.6 libc.so.6 libdl.so.2 libpthread.so.0 librt.so.1 | 形态 | 结果 | |---|---| -| kernel 写在**模块接口单元**里,SYCL 头进 GMF | ❌ **DPC++ 的 driver 产不出 BMI** | -| **缝形态**:模块接口 SYCL-free,SYCL 在普通 TU | ✅ **跑通**,`seam: 11 22 33 44`,kernel 在 4080 上执行 | +| kernel 写在**模块接口单元**里,SYCL 头进 GMF | **DPC++ 的 driver 产不出 BMI** | +| **缝形态**:模块接口 SYCL-free,SYCL 在普通 TU | **跑通**,`seam: 11 22 33 44`,kernel 在 4080 上执行 | -⭐ 第一行的判据取自**最简可能的模块**(`module;` + `#include ` + +第一行的判据取自**最简可能的模块**(`module;` + `#include ` + `export module tiny;`),它同样失败: ``` @@ -603,7 +603,7 @@ clang++: error: clang-offload-bundler command failed with exit code 1 ⇒ **不是 C++ 语义冲突,是 DPC++ 的 driver 管线不认识 `.pcm`。** 这是上游的一个 洞,不是设计的选择;它会被上游修好,而在那之前形态 B 只能走缝。 -### ⭐⭐⭐ 由此得到的统一结论 +### 由此得到的统一结论 > **缝形态不是 CUDA 的权宜,它是全部后端唯一都成立的那一个。** @@ -618,23 +618,23 @@ mcpp 侧也只需要支持一种形状,而不是每个后端一种。 1. **缝必须 `extern "C"`。** 先用 C++ linkage 声明,链接期报 `undefined reference to 'add_inplace@seam(...)'` —— 模块 linkage 把名字改了。 - ⭐ 与 `examples/09-cuda-kernel` 的纪律**逐字相同**,这不是巧合。 + 与 `examples/09-cuda-kernel` 的纪律**逐字相同**,这不是巧合。 2. **必须显式指定设备目标。** 不指定 `-fsycl-targets` 时默认编 spir64 镜像, 而本机只有 CUDA 后端,运行期报 `No kernel named _ZTSZZ11add_inplace... was found`。 ⇒ **`accel` 维要记录的正是这个**:产物为哪个 SYCL target 编的。 -3. ⚠️⚠️ **PTX 版本受驱动限制** —— 与 §2.1 同一个失败模式的**第三次独立出现**: +3. **PTX 版本受驱动限制** —— 与 §2.1 同一个失败模式的**第三次独立出现**: | SYCL TU 用哪个 CUDA 载荷编 | 运行 | |---|---| - | CUDA 12.9 | ❌ `CUDA_ERROR_UNSUPPORTED_PTX_VERSION` | - | CUDA 12.4(驱动上限) | ✅ `seam: 11 22 33 44` | + | CUDA 12.9 | `CUDA_ERROR_UNSUPPORTED_PTX_VERSION` | + | CUDA 12.4(驱动上限) | `seam: 11 22 33 44` | - ⭐ 三次(nvcc 产物、SYCL 产物、载荷选择)都是**编译链接全干净、运行期才说话**。 + 三次(nvcc 产物、SYCL 产物、载荷选择)都是**编译链接全干净、运行期才说话**。 这把 §2.1 从「一条 CUDA 的注意事项」升格为**跨后端的通用约束**: `accel` 维必须能表达「这个产物要求驱动至少多新」,而不只是「为哪个架构编的」。 -### ⚠️ 一条前置条件:mcpp 的 LLVM 载荷缺 offload 工具 +### 一条前置条件:mcpp 的 LLVM 载荷缺 offload 工具 实测 `clang -x cuda -fgpu-rdc`(跨 TU `__device__` 调用)在 `xim:llvm@22.1.8` 上失败: @@ -645,13 +645,13 @@ clang++: error: llvm-offload-binary command failed with exit code 1 该载荷是 **slim 构建(36 个二进制)**,不含 `llvm-offload-binary` / `clang-linker-wrapper` / `clang-offload-bundler`。 -⭐ 对照:**DPC++ 载荷全带**(它就是 LLVM 的一个发行)。 +对照:**DPC++ 载荷全带**(它就是 LLVM 的一个发行)。 ⇒ **「以 LLVM 为主」的前置条件是给 `xim:llvm` 补上 offload 工具**, 或发一个 `xim:llvm-offload` 伴生包。非 RDC 路径不受影响(已实测通过)。 --- -## 6.4.6 ⭐⭐ CUDA 还是不是「唯一的例外」 +## 6.4.6 CUDA 还是不是「唯一的例外」 §6.3.5 的表里 CUDA 是唯一非 LLVM 的一格。综合本节与 §6.4 的实测,答案是: **在构建层面它已经不是例外了**,理由有三条,每条都已实测: @@ -670,17 +670,17 @@ clang++: error: llvm-offload-binary command failed with exit code 1 | `-gencode` 的精细控制 | `sm_XXf` 家族目标等 clang 未覆盖的拼法 | | RDC / device link | 在 `xim:llvm` 补齐 offload 工具之前 | -⭐ **对用户的意义:项目形状、依赖声明、feature、诊断在所有后端上是同一套; +**对用户的意义:项目形状、依赖声明、feature、诊断在所有后端上是同一套; 换后端只换 `[features]` 与载荷,不换开发方式。** 这就是「统一的构建与开发体验」的 具体含义 —— 不是把 nvcc 藏起来,是让它不再决定工程长什么样。 -⚠️ **兜底也写明**:若某个后端将来出现连缝形态都容纳不了的要求, +**兜底也写明**:若某个后端将来出现连缝形态都容纳不了的要求, 它由**自己的规则包**单独支持(§6.2),核心不为它变形。**单独支持是有的, 但它是插件的单独,不是架构的分叉。** --- -## 6.5 ⭐ 多设备后端复用既有通用机制,不新增概念 +## 6.5 多设备后端复用既有通用机制,不新增概念 用户体验的关键不是新语法,是**新东西能不能落在已经存在的格子里**。逐项对照: @@ -693,12 +693,12 @@ clang++: error: llvm-offload-binary command failed with exit code 1 | 装工具链 | **xim 载荷 + `xpm..deps`** | 否 | | 跑/调试/分析 | **`mcpp run` 的 runner 机制** —— `--profile nsys`、`--check sanitizer` | 否 | | 产物选变体 | **`pack::AbiTag` 的 `accel` 维 + `tag_check`** | 否(已落地) | -| 声明能力独占 | `provides` + 新增 `exclusive` | ⚠️ **一个布尔键** | +| 声明能力独占 | `provides` + 新增 `exclusive` | **一个布尔键** | -⭐⭐ **整套多设备支持只新增了两样东西**:`accel` 这一个身份维(已落地), +**整套多设备支持只新增了两样东西**:`accel` 这一个身份维(已落地), 和 `exclusive` 这一个布尔键。其余全部是既有机制的取值扩展。 -⚠️ 这是本方案对「简洁」的操作性定义:**不是少写字,是不让用户学第二套概念。** +这是本方案对「简洁」的操作性定义:**不是少写字,是不让用户学第二套概念。** 一个已经会用 `--features` 的人,切后端时不需要学任何新东西。 ### 6.5.1 feature 与 accel 的关系要说清 @@ -713,7 +713,7 @@ clang++: error: llvm-offload-binary command failed with exit code 1 cuda = { accel = "cuda12.9+{sm_80,sm_89}" } # feature 决定 accel,不是等于它 ``` -⭐ 一个 feature 可以决定 `accel`,但 `accel` 也可以由 `--accel` 直接给, +一个 feature 可以决定 `accel`,但 `accel` 也可以由 `--accel` 直接给, 或由 `[build] accel` 写死。**三个入口一个出口**,与 `--target` / `[toolchain]` 的关系同形。 @@ -721,7 +721,7 @@ cuda = { accel = "cuda12.9+{sm_80,sm_89}" } # feature 决定 accel,不是等 ## 7. 工业级生态覆盖 -⚠️ **不选「小而完整」的样例包。** 判据是:**工业上真的有人用它出货**, +**不选「小而完整」的样例包。** 判据是:**工业上真的有人用它出货**, 并且它的构建复杂到能把本方案的每一处都压出来。分五档,每档回答一个不同的问题。 ### 7.A 推理运行时 —— 「能不能承载真实部署」 @@ -732,10 +732,10 @@ cuda = { accel = "cuda12.9+{sm_80,sm_89}" } # feature 决定 accel,不是等 | **llama.cpp / ggml** | llama.cpp `v0.4.0` / ggml `v0.23.0`(均 2026-09-04) | 部署量最大的本地推理栈;**一个代码库里就有 CUDA/Vulkan/SYCL/HIP/Metal/CPU 全部后端** | 整张设备矩阵;变体分发 | | **libtorch** | 上游 release | C++ 侧工业消费 PyTorch 的实际形态 | **载荷消费**:cuDNN/NCCL/cuBLAS 闭包、私有 loader 下的 RPATH | -⚠️ 索引里现有的 `ggml-org.llamacpp` pin 在 `b10069`,`sources` 里**只有 `ggml-cpu/*`** —— +索引里现有的 `ggml-org.llamacpp` pin 在 `b10069`,`sources` 里**只有 `ggml-cpu/*`** —— CUDA/Vulkan/SYCL/HIP 一个都没有。补齐它就是把矩阵走一遍。 -⭐ **PyTorch 全源码构建不在第一批**,理由不是难,是**顺序**:它依赖 cuDNN/NCCL/ +**PyTorch 全源码构建不在第一批**,理由不是难,是**顺序**:它依赖 cuDNN/NCCL/ oneDNN/oneMKL 全部就位。第一批做 `libtorch` 载荷消费,把闭包与 RPATH 这条路走通; 源码构建排在算子库档之后。 @@ -766,23 +766,23 @@ oneDNN/oneMKL 全部就位。第一批做 `libtorch` 载荷消费,把闭包与 R |---|---|---| | **ncnn** | `20260526` | GPU 路径就是 Vulkan compute,不依赖任何厂商工具链 | -⭐ 它与 llama.cpp 的 Vulkan 后端**互为独立见证**:两个不相干的工业项目在 +它与 llama.cpp 的 Vulkan 后端**互为独立见证**:两个不相干的工业项目在 **lavapipe** 上都跑出正确结果,那条无卡 lane 才算真的 —— 单个项目可能碰巧绕开了。 -### 7.F ⚠️ 这一节的判据不是「编过了」 +### 7.F 这一节的判据不是「编过了」 每个框架的判据都必须是**它自己的正确性判据**,不是构建成功: ONNX Runtime 跑它自己的 model test、llama.cpp 推理出正确 token、CUTLASS 的 GEMM 与参考实现比对、oneDNN 跑它自己的 benchdnn、Kokkos 跑它自己的 unit test、 OpenCV 跑它自己的 accuracy test、ncnn 的分类结果与 CPU 后端一致。 -⭐⭐ **上游自带的测试套件是唯一不会被我写偏的判据。** +**上游自带的测试套件是唯一不会被我写偏的判据。** --- ## 7.5 架构横评:与 CMake / xmake / Cargo / Zig / Bazel / Spack / Conan -⚠️ 本节的立场:**逐条给出可核对的事实,而不是宣称领先。** 落后的地方写在同一张表里。 +本节的立场:**逐条给出可核对的事实,而不是宣称领先。** 落后的地方写在同一张表里。 ### 7.5.1 核心差异:产物身份里有没有加速器维 @@ -790,17 +790,17 @@ OpenCV 跑它自己的 accuracy test、ncnn 的分类结果与 CPU 后端一致 | 系统 | 「为哪个设备架构编的」记在哪里 | 消费时会不会被检查 | |---|---|---| -| **CMake** | `CMAKE_CUDA_ARCHITECTURES` —— **一个构建变量** | ❌ 产出的 `.a`/`.so` 不携带它;消费方无从得知 | -| **xmake** | `add_cugencodes()` —— 构建配置 | ❌ 同上 | -| **Cargo** | 无。GPU 全在 `build.rs` 里 shell 出去 | ❌ target triple 不编码 GPU 架构;feature 是集合不是兼容维 | -| **Zig** | 无。`-target` 不含 GPU 架构 | ❌ | -| **Bazel** | `--config=cuda` + `cuda_archs` | ❌ 不进 artifact 身份 | -| **Conan** | `settings` 参与 `package_id` —— **机制在,维度不在** | ⚠️ ConanCenter #11448 就是在要这个维度,未合入 | -| **Spack** | `+cuda cuda_arch=90` 是**一等 variant**,进 hash | ✅ **现有系统里最接近的** | -| **Python wheels** | PEP 817/825 Wheel Variants(`namespace::feature::value`) | ⚠️ 正在加,说明这是全行业公认的缺口 | -| **mcpp** | `pack::AbiTag::accel`,与 triple/compiler/stdlib/standard 并列 | ✅ `tag_check` 在选变体时比较,**拒绝时点名维度与两侧取值** | - -⭐ 诚实的结论:**这不是 mcpp 独有的想法** —— Spack 早就有,wheels 正在加。 +| **CMake** | `CMAKE_CUDA_ARCHITECTURES` —— **一个构建变量** | 产出的 `.a`/`.so` 不携带它;消费方无从得知 | +| **xmake** | `add_cugencodes()` —— 构建配置 | 同上 | +| **Cargo** | 无。GPU 全在 `build.rs` 里 shell 出去 | target triple 不编码 GPU 架构;feature 是集合不是兼容维 | +| **Zig** | 无。`-target` 不含 GPU 架构 | no | +| **Bazel** | `--config=cuda` + `cuda_archs` | 不进 artifact 身份 | +| **Conan** | `settings` 参与 `package_id` —— **机制在,维度不在** | ConanCenter #11448 就是在要这个维度,未合入 | +| **Spack** | `+cuda cuda_arch=90` 是**一等 variant**,进 hash | **现有系统里最接近的** | +| **Python wheels** | PEP 817/825 Wheel Variants(`namespace::feature::value`) | 正在加,说明这是全行业公认的缺口 | +| **mcpp** | `pack::AbiTag::accel`,与 triple/compiler/stdlib/standard 并列 | `tag_check` 在选变体时比较,**拒绝时点名维度与两侧取值** | + +诚实的结论:**这不是 mcpp 独有的想法** —— Spack 早就有,wheels 正在加。 mcpp 的差异在于**它是一个编译器驱动的构建系统**,所以那个维度既进包身份, **也能在编译前就用它拒绝**;Spack 只在求解依赖图时用它,不驱动编译。 @@ -808,16 +808,16 @@ mcpp 的差异在于**它是一个编译器驱动的构建系统**,所以那个 | 维度 | mcpp | CMake | xmake | Cargo | Zig | Bazel | |---|---|---|---|---|---|---| -| C++20 modules 一等公民 | ✅ 自举即模块 | ⚠️ 2023 起,部分 | ⚠️ 部分 | — | — | ⚠️ 实验 | -| 包管理内置 | ✅ | ❌ 需 vcpkg/conan | ✅ xrepo | ✅ | ⚠️ 新 | ⚠️ 需 rules_* | -| 工具链本身是载荷(hermetic) | ✅ xim 装 gcc/llvm/glibc | ❌ 用 host | ⚠️ 部分 | ⚠️ rustup | ✅ 自带 libc | ✅ 最强 | -| 产物身份含加速器维 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| 构建程序是真语言 | ✅ `build.mcpp` 是 C++ | ⚠️ CMake DSL | ✅ Lua | ⚠️ `build.rs` | ✅ Zig | ⚠️ Starlark | -| 交叉编译广度 | ⚠️ 追赶中 | ⚠️ 靠工具链文件 | ⚠️ | ⚠️ | ✅ **最强** | ⚠️ | -| 生态广度 | ❌ **远落后** | ✅ **压倒性** | ⚠️ | ✅ | ⚠️ | ⚠️ | -| 大规模远程缓存 | ❌ | ⚠️ | ⚠️ | ⚠️ | ❌ | ✅ **最强** | +| C++20 modules 一等公民 | 自举即模块 | 2023 起,部分 | 部分 | — | — | 实验 | +| 包管理内置 | yes | 需 vcpkg/conan | xrepo | yes | 新 | 需 rules_* | +| 工具链本身是载荷(hermetic) | xim 装 gcc/llvm/glibc | 用 host | 部分 | rustup | 自带 libc | 最强 | +| 产物身份含加速器维 | yes | no | | no | | no | +| 构建程序是真语言 | `build.mcpp` 是 C++ | CMake DSL | Lua | `build.rs` | Zig | Starlark | +| 交叉编译广度 | 追赶中 | 靠工具链文件 | partial | partial | **最强** | partial | +| 生态广度 | **远落后** | **压倒性** | partial | yes | partial | partial | +| 大规模远程缓存 | no | partial | partial | partial | no | **最强** | -### 7.5.3 ⚠️ mcpp 明确落后的三处,以及本方案对它们做了什么 +### 7.5.3 mcpp 明确落后的三处,以及本方案对它们做了什么 1. **生态广度。** CMake 拥有一切。⇒ 本方案 §7 用**九个工业级框架**换取覆盖面, 而不是用数量换;判据是这些框架**自己的测试套件**。 @@ -847,12 +847,12 @@ accel = "cuda12.9+{sm_80,sm_90f} ptx>=90" cublas = "12.9.x" # 载荷来自 xim,不是 host ``` -⭐ 差别不在语法长短,在**那句声明去了哪里**:CMake 的进了构建目录, +差别不在语法长短,在**那句声明去了哪里**:CMake 的进了构建目录, mcpp 的进了产物身份,并在下一个消费者构建时被 `tag_check` 读出来。 ## 7.9 场景示例(参考形态,不是最终措辞) -⚠️ 以下是**参考示例**,用来把前面的抽象落到用户真正会敲的东西上。 +以下是**参考示例**,用来把前面的抽象落到用户真正会敲的东西上。 措辞会随实现调整;不变的是每个场景**只需要哪些概念**。 ### 场景 1 —— 纯 C++,没有设备代码 @@ -865,7 +865,7 @@ version = "0.1.0" standard = "c++23" ``` -⭐ **一个字都不用改,零代价。** `.cu` 不进默认 glob,`accel` 维为空表示不受约束, +**一个字都不用改,零代价。** `.cu` 不进默认 glob,`accel` 维为空表示不受约束, `cfg(accelerator=…)` 不匹配任何东西。这是本方案对绝大多数工程的影响面:没有。 ### 场景 2 —— C++ + 设备代码(形态 A:岛) @@ -922,7 +922,7 @@ export namespace app { } ``` -⭐ **消费者 `import app;` 即可,永远看不见 CUDA。** 这就是「把设备代码与 C++ 分开 +**消费者 `import app;` 即可,永远看不见 CUDA。** 这就是「把设备代码与 C++ 分开 以支持模块」那个问题的答案:不是不用头文件,是**头文件只出现在缝的内侧一次**。 ### 场景 3 —— 整目标形态(形态 B:SYCL) @@ -951,7 +951,7 @@ export std::vector add(std::vector a, std::vector b) { } ``` -⚠️ **已实测(§6.4.5):kernel 不能写在模块接口单元里** —— DPC++ 的 driver 产不出 +**已实测(§6.4.5):kernel 不能写在模块接口单元里** —— DPC++ 的 driver 产不出 BMI。形态 B 今天只能走**缝形态**:模块接口 SYCL-free,SYCL 代码在普通 TU 里, 经 `extern "C"` 相遇。上面这个写法要等上游修好 `clang-offload-bundler`。 @@ -978,7 +978,7 @@ $ mcpp build --features vulkan $ mcpp build --no-accel # 显式要 CPU-only 变体 ``` -⭐ `cfg(accelerator=…)` **处处是成员判定** ⇒ 同时启用两个后端时 +`cfg(accelerator=…)` **处处是成员判定** ⇒ 同时启用两个后端时 `all(accelerator="cuda", accelerator="vulkan")` 表示「两者都启用」,不是不可满足。 ### 场景 5 —— 消费预编译的加速库 @@ -998,7 +998,7 @@ error: no prebuilt artifact of `llamacpp` matches this build fix: build from source, or `--no-accel` to take the CPU-only variant ``` -⭐ 消息点名**维度**、**两侧取值**、**该轴可行的补救** —— 三者缺一,用户就得猜。 +消息点名**维度**、**两侧取值**、**该轴可行的补救** —— 三者缺一,用户就得猜。 ### 场景 6 —— 无卡机器上开发与 CI @@ -1013,7 +1013,7 @@ $ mcpp test --features vulkan # kernel 在 CPU 上真跑 VK_ICD_FILENAMES = "${xim:mesa-lavapipe}/share/vulkan/icd.d/lvp_icd.json" ``` -⚠️ **模拟器 lane 的结果必须与硬件 lane 对照过**(§9 C4/C5),否则等于把 +**模拟器 lane 的结果必须与硬件 lane 对照过**(§9 C4/C5),否则等于把 「lavapipe 说对了」当成「它是对的」。 ### 场景 7 —— 发布多变体 @@ -1024,7 +1024,7 @@ $ mcpp pack --accel 'vulkan1.3+{spirv1.6}' $ mcpp pack --no-accel # CPU-only,必须排第一 ``` -⚠️⚠️ **CPU-only 变体在描述符里排第一。** 消费方是首次匹配即用的循环, +**CPU-only 变体在描述符里排第一。** 消费方是首次匹配即用的循环, 而不认识 `accel` 键的旧 mcpp 会跳过它 —— 排第一保证它拿到的是能跑的那个。 ### 场景 8 —— 混合:C++ 模块 + 设备代码 + 第三方算子库 @@ -1060,7 +1060,7 @@ $ mcpp run --profile nsys # 走 runner,不新增专用命令 $ mcpp test --check sanitizer # compute-sanitizer 也是 xim 载荷 ``` -⭐ 注意这里**没有一行提到 host**:nvcc、cudart、cuBLAS、CUTLASS、nsys、 +注意这里**没有一行提到 host**:nvcc、cudart、cuBLAS、CUTLASS、nsys、 compute-sanitizer 全部来自 xim;host 参与的只有 `libcuda.so.1`。 ### 场景 9 —— 诊断 @@ -1076,21 +1076,21 @@ warning: this payload requires driver CUDA >= 13.0; this host reports 12.4. update the driver. ``` -⭐⭐ 第二条正是 §2.1 实测到的那次失败 —— **把一个只在运行期才说话的错误, +第二条正是 §2.1 实测到的那次失败 —— **把一个只在运行期才说话的错误, 搬到构建之前**。 --- ## 8. 跨仓库顺序 -⚠️ 顺序是硬的:**消费者先发布,索引 `latest` 才能动**。 +顺序是硬的:**消费者先发布,索引 `latest` 才能动**。 ``` ⓪ 立即,无依赖 ── 修已发布的错误示范(§0.1) ⓪a 从全部用户文档删除 `[xlings] deps`(英文 14 处 / 中文 10 处), `[xlings.workspace]` 是唯一形式 ⓪b examples/09-cuda-kernel 改用 `[xlings.workspace]` - ⓪c ⚠️ 该示例去 host 化要等 ①a 的载荷 —— 但**文档与字段先改** + ⓪c 该示例去 host 化要等 ①a 的载荷 —— 但**文档与字段先改** ① xim-pkgindex ── 载荷,三批,批内并行 ①a 编译必需 cuda-nvcc / cudart / nvrtc / nvjitlink / cccl (150 MB) @@ -1110,8 +1110,8 @@ warning: this payload requires driver CUDA >= 13.0; this host reports 12.4. → 7.C Kokkos → 7.D OpenCV + FAISS → 7.A ONNX Runtime → 7.A libtorch ``` -⭐ ① 与 ② **完全并行** —— 载荷不依赖引擎的新键。 -⚠️ ⑤ 内部也有序:llamacpp 先做,因为它一个项目就覆盖矩阵,能最早暴露设计问题。 +① 与 ② **完全并行** —— 载荷不依赖引擎的新键。 +⑤ 内部也有序:llamacpp 先做,因为它一个项目就覆盖矩阵,能最早暴露设计问题。 --- @@ -1121,7 +1121,7 @@ warning: this payload requires driver CUDA >= 13.0; this host reports 12.4. | # | 判据 | lane | |---|---|---| -| C0 | ⭐ **默认路线**:`xim:llvm@22.1.8` + `xim:cuda-*@12.9.x`,`clang -x cuda` 编出 `sm_89` 并在 4080 上跑出正确结果,**全程零 host 路径**(`ldd` 里不出现 `/usr/lib`) | 本机硬件 | +| C0 | **默认路线**:`xim:llvm@22.1.8` + `xim:cuda-*@12.9.x`,`clang -x cuda` 编出 `sm_89` 并在 4080 上跑出正确结果,**全程零 host 路径**(`ldd` 里不出现 `/usr/lib`) | 本机硬件 | | C0b | 同一工程切到 nvcc 路线(`rules-cuda` 选 `-ccbin` 为 `xim:gcc@15.1.0`)结果一致 ⇒ **翻转没有废掉 gcc 路线** | 本机硬件 | | C1 | `xlings install cuda-nvcc@12.9.86` 后 nvcc 可用,且 `mcpp self doctor` **不报** `cicc: not found` —— 全程不碰 host toolkit | 沙箱 | | C2 | 驱动只到 12.4 的机器上请求 13.x 载荷 ⇒ **构建前拒绝**,消息含两边版本号与该轴补救 | 本机(驱动 550) | @@ -1134,7 +1134,7 @@ warning: this payload requires driver CUDA >= 13.0; this host reports 12.4. | C9 | 跨 TU `__device__` 调用链接成功;改 `device-link` 触发重新 prepare | 本机硬件 | | C10 | llama.cpp 发多变体后,**旧 mcpp 取到 CPU-only 那个**并正常运行 | 无需设备 | | C11 | oneDNN 的 `benchdnn` 在 DPC++ CPU device 与 CUDA 后端两条 lane 上都通过 | 两条 | -| C18 | ⭐ 缝形态在**全部**后端上是同一个工程形状:CUDA 与 SYCL 两个工程的 `mcpp.toml` 除 `[features]` 与载荷外**逐行相同** | 无需设备 | +| C18 | 缝形态在**全部**后端上是同一个工程形状:CUDA 与 SYCL 两个工程的 `mcpp.toml` 除 `[features]` 与载荷外**逐行相同** | 无需设备 | | C19 | 不指定设备目标时 mcpp **在构建期**报出「产物没有为任何可用设备编」,而不是留到运行期的 `No kernel named … was found` | 本机硬件 | | C20 | `accel` 维能表达「要求驱动至少多新」,且 PTX 版本高于驱动时**构建前拒绝**(§6.4.5 纪律 3) | 本机硬件 | | C12 | ONNX Runtime 同时启用两个 EP ⇒ 由 `exclusive` 决定报错还是共存,**与它自己的文档一致** | 无卡 + 硬件 | @@ -1144,10 +1144,10 @@ warning: this payload requires driver CUDA >= 13.0; this host reports 12.4. | C16 | 卸掉 `rules-cuda` 后,`mcpp self doctor` 的设备一节**安静地不报**,而不是报错或崩 | 无需设备 | | C17 | 只装 `cuda-nvcc` 不装 `libnvvm` ⇒ doctor 报出 `cicc` 并**指名要装哪个 xim 包** | 沙箱 | -⭐ C2 与 C10 是「答错比不答更坏」的两条:一个放行了跑不了的载荷,一个让旧客户端 +C2 与 C10 是「答错比不答更坏」的两条:一个放行了跑不了的载荷,一个让旧客户端 拿到跑不了的产物。**它们要最先有测试。** -⭐⭐ C4 与 C5 必须**各自在 4080 上再跑一次**:模拟器 lane 的意义在于它与硬件 +C4 与 C5 必须**各自在 4080 上再跑一次**:模拟器 lane 的意义在于它与硬件 lane 结论一致;只跑模拟器,等于把「lavapipe 说对了」当成「它是对的」。 --- @@ -1162,12 +1162,12 @@ lane 结论一致;只跑模拟器,等于把「lavapipe 说对了」当成「它 | RK-d | lavapipe 慢 | 用最小模型;判据是「结果正确」不是「快」 | | RK-e | 载荷总体积 | 组件级拆包:普通构建 150 MB,profile 才装 nsight,static 才装 `-static` 包 | | RK-f | chipStar 覆盖的 CUDA 子集有限 | 它只作**无卡环境的 kernel 行为**见证;`-gencode`/PTX/fatbin 仍走真卡 | -| RK-g | ⚠️ **框架档工作量远大于引擎档** | 顺序上 llamacpp 先行;它若暴露设计问题,后面七个都会受影响 ⇒ 它是 gate 不是第一个任务 | -| RK-h | ⚠️ ONNX Runtime / libtorch 依赖 cuDNN、NCCL,二者许可与 CUDA Toolkit 不同 | 逐个读它们自己的 manifest/EULA,**按 §3.0 的方法由许可字段决定**,不由判断决定 | -| RK-i | ~~形态 B × modules 未实测~~ | **已测,见 §6.4.5。** kernel 进模块接口单元 ❌(DPC++ driver 产不出 BMI);缝形态 ✅(4080 上跑出 `seam: 11 22 33 44`)。⇒ 缝形态是全部后端唯一都成立的形状 | -| RK-l | ⚠️ **`xim:llvm@22.1.8` 是 slim 构建,缺 offload 工具** ⇒ `-fgpu-rdc` 不可用 | 「以 LLVM 为主」的前置条件:补 offload 工具或发 `xim:llvm-offload` 伴生包。非 RDC 路径不受影响 | -| RK-k | ⚠️ **主路线翻转到 clang 的代价是 CUDA 版本滞后** —— 实测 clang 22 部分支持到 12.9、clang 18 只到 12.3 | 两条路线共存,由 `rules-cuda` 按「clang 版本 × CUDA 版本」可用配对选;需要最新 CUDA 特性的走 nvcc。判据 C0b 保证 gcc/nvcc 路线不被废掉 | -| RK-j | ⚠️ **把 CUDA 探针从核心搬进规则包会动已发布的行为** | `mcpp self doctor` 的两条消息是已发布的契约。迁移期核心保留一份回落,规则包在场时以规则包为准;两条路径的消息逐字相同 | +| RK-g | **框架档工作量远大于引擎档** | 顺序上 llamacpp 先行;它若暴露设计问题,后面七个都会受影响 ⇒ 它是 gate 不是第一个任务 | +| RK-h | ONNX Runtime / libtorch 依赖 cuDNN、NCCL,二者许可与 CUDA Toolkit 不同 | 逐个读它们自己的 manifest/EULA,**按 §3.0 的方法由许可字段决定**,不由判断决定 | +| RK-i | ~~形态 B × modules 未实测~~ | **已测,见 §6.4.5。** kernel 进模块接口单元 (DPC++ driver 产不出 BMI);缝形态 (4080 上跑出 `seam: 11 22 33 44`)。⇒ 缝形态是全部后端唯一都成立的形状 | +| RK-l | **`xim:llvm@22.1.8` 是 slim 构建,缺 offload 工具** ⇒ `-fgpu-rdc` 不可用 | 「以 LLVM 为主」的前置条件:补 offload 工具或发 `xim:llvm-offload` 伴生包。非 RDC 路径不受影响 | +| RK-k | **主路线翻转到 clang 的代价是 CUDA 版本滞后** —— 实测 clang 22 部分支持到 12.9、clang 18 只到 12.3 | 两条路线共存,由 `rules-cuda` 按「clang 版本 × CUDA 版本」可用配对选;需要最新 CUDA 特性的走 nvcc。判据 C0b 保证 gcc/nvcc 路线不被废掉 | +| RK-j | **把 CUDA 探针从核心搬进规则包会动已发布的行为** | `mcpp self doctor` 的两条消息是已发布的契约。迁移期核心保留一份回落,规则包在场时以规则包为准;两条路径的消息逐字相同 | --- @@ -1264,11 +1264,11 @@ action **只**挂到可执行/共享库/测试上,而 llama.cpp 的 CUDA 后端 | CCCL 3.2(13.2 线)+ clang | 换成 **libcu++ 编不动**:`string_view` 的推导指引只允许 `__host__ __device__`;`block_load.cuh` 要 placement new | | 任一 CCCL + nvcc | 12.9 撞 glibc 2.44 的 C23 `cospi`;13.3 撞驱动 12.4 | -⭐ **这不是「没做完」,是「本机构造上无解」**,与 §12.1 里 C9 那条同一性质。 +**这不是「没做完」,是「本机构造上无解」**,与 §12.1 里 C9 那条同一性质。 需要的是一台驱动 ≥ 13.0 的机器(nvcc 13.3 路线),或一个 ggml 与 CCCL 版本匹配的 上游 checkpoint。 -⭐ 顺带三条通用读数,都写进了规则包与文档: +顺带三条通用读数,都写进了规则包与文档: **layer 不能选择依赖**(依赖挂 feature,源文件挂 accel 轴); **设备编译必须指名 CCCL 载荷**否则命中 `/usr/include/cub`(与 §12.1 的 `cuda_runtime.h` 同一形状,第三次); diff --git a/.github/actions/bootstrap-mcpp/action.yml b/.github/actions/bootstrap-mcpp/action.yml index 0dfcfb0e..6c6ec132 100644 --- a/.github/actions/bootstrap-mcpp/action.yml +++ b/.github/actions/bootstrap-mcpp/action.yml @@ -42,7 +42,7 @@ runs: uses: actions/cache@v4 with: path: ~/.mcpp - # ⚠️⚠️ `runner.arch` IS PART OF EVERY KEY, AND WAS NOT. + # `runner.arch` IS PART OF EVERY KEY, AND WAS NOT. # # These caches hold INSTALLED BINARIES — `~/.xlings`, `~/.mcpp`, and # `target/`. Keyed on `runner.os` alone, `Linux` names two machines, and @@ -52,7 +52,7 @@ runs: # Cache hit for: xlings-Linux-v2-xl2026.8.17.2-… # …/bin/xlings: cannot execute binary file: Exec format error # - # ⭐ AND IT SURVIVED THE FIX ONE LAYER DOWN. Reading `uname -m` to pick + # AND IT SURVIVED THE FIX ONE LAYER DOWN. Reading `uname -m` to pick # the right tarball is correct and was not enough: the download never # happened, because a cache from the other architecture was already # there. A key that does not name the machine is a key that lets one @@ -88,7 +88,7 @@ runs: REPO_DIR="$(pwd)" # Always install the pinned version — the cache may hold an older # xlings whose sysroot/packages are incompatible. - # ⚠️⚠️ THE ARCHITECTURE IS READ, NOT ASSUMED. This branched on the OS + # THE ARCHITECTURE IS READ, NOT ASSUMED. This branched on the OS # alone and named `linux-x86_64` for every non-Darwin host, which is # invisible until a Linux runner is not x86_64. Measured on # `ubuntu-24.04-arm`, the first time this repo scheduled one: @@ -117,7 +117,7 @@ runs: "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ "${WORK}/${tarball}" tar -xzf "${WORK}/${tarball}" -C "${WORK}" - # ⚠️⚠️ THE TWO LINUX TARBALLS DO NOT HAVE THE SAME SHAPE, and this line + # THE TWO LINUX TARBALLS DO NOT HAVE THE SAME SHAPE, and this line # named one of them. Measured on the 2026.8.17.2 assets: # # linux-x86_64 → subos/default/bin/xlings (513 entries) @@ -126,12 +126,12 @@ runs: # so fetching the right architecture — the fix immediately above — got # as far as `No such file or directory`, exit 127. # - # ⭐ FOUND RATHER THAN ASSUMED, AND THE SEARCH IS BOUNDED. `-maxdepth 5` + # FOUND RATHER THAN ASSUMED, AND THE SEARCH IS BOUNDED. `-maxdepth 5` # keeps it from wandering into the payload tree, and an empty result is # a hard error: a bootstrap that silently continues without xlings fails # forty lines later on something that reads like a different problem. # - # ⚠️ CHECKED BEFORE CHANGING THE THREE HOSTS THAT ALREADY WORKED. On + # CHECKED BEFORE CHANGING THE THREE HOSTS THAT ALREADY WORKED. On # x86_64 this now resolves `bin/xlings` where the line before named # `subos/default/bin/xlings` — and those are the same file: # @@ -212,7 +212,7 @@ runs: uses: actions/cache@v4 with: path: target - # ⚠️ `modules/**` belongs here as much as `src/**` does. mcpp's own + # `modules/**` belongs here as much as `src/**` does. mcpp's own # source lives in both since the subsystem split, and a key that hashed # only one of them would restore a target/ built from different sources # and report success — the failure mode a cache key exists to prevent, diff --git a/.github/tools/check_aarch64_llvm_deferral.sh b/.github/tools/check_aarch64_llvm_deferral.sh index 6348a9b1..dd4d36d6 100755 --- a/.github/tools/check_aarch64_llvm_deferral.sh +++ b/.github/tools/check_aarch64_llvm_deferral.sh @@ -1,17 +1,17 @@ #!/usr/bin/env bash -# ⭐⭐ A DEFERRAL'S PREMISE, RECHECKED. +# A DEFERRAL'S PREMISE, RECHECKED. # # `available_toolchain_indexes()` omits llvm on non-x86_64 Linux because no # linux-aarch64 llvm exists — not in xlings-res, and not upstream since 19.x. # That is a deferral, and a deferral nobody rechecks is indistinguishable from # a defect. # -# ⚠️ THIS FAILS WHEN THE REASON STOPS HOLDING, which is the opposite of what a +# THIS FAILS WHEN THE REASON STOPS HOLDING, which is the opposite of what a # check usually does. The day an aarch64 llvm is published, it goes red and # names the gate to remove — see # `.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md` §P1. # -# ⚠️ Network trouble must not be read as "it appeared". An unreadable asset list +# Network trouble must not be read as "it appeared". An unreadable asset list # leaves the premise alone and says so: a check that turns a flaky API into a # claim about the world is worse than no check. set -uo pipefail diff --git a/.github/tools/check_modules_wiring.sh b/.github/tools/check_modules_wiring.sh index 58a678dd..fd98ad8f 100755 --- a/.github/tools/check_modules_wiring.sh +++ b/.github/tools/check_modules_wiring.sh @@ -122,7 +122,7 @@ done # report success -- the exact failure a cache key exists to prevent, arriving # silently. Found by sweeping for stale paths after the split, not by a test. # -# ⚠️ THE UNIT IS THE LINE, NOT THE FILE. The first version asked whether the +# THE UNIT IS THE LINE, NOT THE FILE. The first version asked whether the # FILE mentioned `modules/**`, and passed — satisfied by the comment sitting # above the key explaining why `modules/**` belongs there. A check that a # comment can satisfy is checking the prose. diff --git a/.github/tools/check_narrow_conversions.sh b/.github/tools/check_narrow_conversions.sh index e52d9763..a78897a1 100755 --- a/.github/tools/check_narrow_conversions.sh +++ b/.github/tools/check_narrow_conversions.sh @@ -76,7 +76,7 @@ cd "$REPO_DIR" || { echo "FAIL: cannot cd to $REPO_DIR" >&2; exit 1; } # So: a pass here does NOT mean "the tree is audited". It means no NEW direct # narrowing was written where this class originates. # -# ⚠️ `modules/manifest/src/glob.cppm` is in scope even though the rest of that +# `modules/manifest/src/glob.cppm` is in scope even though the rest of that # package is not. It is the glob walker itself -- the file this guard's # background note names -- and it moved out of `src/modgraph/` in the subsystem # split. A scope written as directory names shrinks silently when a file moves, diff --git a/.github/tools/fetch_release.sh b/.github/tools/fetch_release.sh index 86490c95..2478d6f6 100755 --- a/.github/tools/fetch_release.sh +++ b/.github/tools/fetch_release.sh @@ -16,7 +16,7 @@ # every unexplained red on this branch, always inside 12 seconds, always with no # test name in the log. # -# ⚠️ `curl --retry` ALONE DOES NOT COVER IT. `--retry` handles timeouts and a +# `curl --retry` ALONE DOES NOT COVER IT. `--retry` handles timeouts and a # specific list of 5xx responses; an empty reply is a *transport* error and is # not on that list. `--retry-all-errors` (curl 7.71+) is the flag that does, and # it is the one that was missing. The outer loop below is not redundant with it: diff --git a/.github/tools/install_released_mcpp.sh b/.github/tools/install_released_mcpp.sh index b5131532..1cc816d7 100755 --- a/.github/tools/install_released_mcpp.sh +++ b/.github/tools/install_released_mcpp.sh @@ -20,7 +20,7 @@ # the version under test — and failing outright, because only the latter was # installed: # -# ✓ 1 package(s) installed +# 1 package(s) installed # [error] xlings: version '2026.8.3.2' not found for 'mcpp' # [error] available: 2026.8.3.4 # diff --git a/.github/tools/mirror_res.sh b/.github/tools/mirror_res.sh index dc95421a..96664d4a 100755 --- a/.github/tools/mirror_res.sh +++ b/.github/tools/mirror_res.sh @@ -287,7 +287,7 @@ hosts=() [[ "${GTC_ENABLED:-0}" == 1 ]] && hosts+=("gitcode.com/$GTC_DST") for host in "${hosts[@]}"; do for a in "${ASSETS[@]}"; do - # ⚠️ RETRIED, BECAUSE A MIRROR CAN ANSWER 502 FOR AN ASSET IT HOLDS. + # RETRIED, BECAUSE A MIRROR CAN ANSWER 502 FOR AN ASSET IT HOLDS. # v2026.8.25.1 failed here twice: every one of the 16 assets reported # "already mirrored, skipping", and the gate then failed one of them on a # single 502 from GitCode's edge. Fetched by hand a minute later it was @@ -297,7 +297,7 @@ for host in "${hosts[@]}"; do # HTTP codes but not the transport-layer failures this path also sees, and # this repository has paid for that distinction before (ci-curl-52). # - # ⚠️ `|| echo ERR` APPENDS, it does not replace — `-f` makes curl exit + # `|| echo ERR` APPENDS, it does not replace — `-f` makes curl exit # non-zero on 502 while `-w` has already written the code, so the variable # read `502ERR` and the log could not be grepped for a status. Substituted # only when curl printed nothing at all. diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index 68d86e79..63a1171e 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -128,7 +128,7 @@ jobs: ) [[ -z "$REQUESTED_TAG" ]] || args+=(--tag "$REQUESTED_TAG") - # ⚠️ EXIT 75 IS "THE PUSH LANDED, THE AUR'S INDEX HAS NOT CAUGHT UP", + # EXIT 75 IS "THE PUSH LANDED, THE AUR'S INDEX HAS NOT CAUGHT UP", # AND FAILING ON IT MAKES THIS REPOSITORY'S CI RED FOR SOMEBODY ELSE'S # REFRESH SCHEDULE. # @@ -142,7 +142,7 @@ jobs: # `AUR RPC did not converge to ` and the AUR RPC answered with # that exact version when asked afterwards. # - # ⚠️ EVERY OTHER NON-ZERO CODE STILL FAILS. A refused downgrade (3) and + # EVERY OTHER NON-ZERO CODE STILL FAILS. A refused downgrade (3) and # a permanent error (2) are this repository's problem and stay red. set +e python3 scripts/aur/reconcile_mcpp_bin.py "${args[@]}" diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml index 1ac95137..2cb131b5 100644 --- a/.github/workflows/ci-linux-e2e.yml +++ b/.github/workflows/ci-linux-e2e.yml @@ -132,7 +132,7 @@ jobs: - name: Install the emulator (xim:qemu-riscv) run: | - # ⚠️ BOTH homes. The shim on PATH dispatches against whichever home + # BOTH homes. The shim on PATH dispatches against whichever home # owns it, and `mcpp run` runs the runner through that shim — so an # emulator installed only in the ambient xlings home answers # "xlings: 'qemu-system-riscv64' is not installed" when mcpp asks. @@ -153,7 +153,7 @@ jobs: XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \ "$XLINGS_BIN" install xim:picolibc-riscv -y test -d "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-picolibc-riscv" - # ⚠️ The M-profile emulator, in BOTH homes for the reason above. + # The M-profile emulator, in BOTH homes for the reason above. # `xim:qemu-arm` carries `qemu-system-arm` as well as # `qemu-system-aarch64`; test 332 addresses it by absolute path out of # the payload, so what matters is that the payload EXISTS in the home @@ -211,19 +211,19 @@ jobs: grep -q 'PASS: the freestanding std subset' \ 133_freestanding_std_subset.sh.log || { echo "133 (std subset) skipped on the runner that must run it"; exit 1; } - # ⚠️ 332 declares `# requires: qemu-arm`, which no sharded runner has + # 332 declares `# requires: qemu-arm`, which no sharded runner has # — so on the shards it exits 0 without running. This job is the only # place its PASS line can be demanded. grep -q 'PASS: cortex-m rows build, boot' \ 332_cortex_m_builds_and_boots.sh.log || { echo "332 (cortex-m) skipped on the runner that must run it"; exit 1; } - # ⭐ And a count, because four `grep -q` calls that each matched say + # And a count, because four `grep -q` calls that each matched say # nothing about how many rows the script actually booted: a fixture # that stopped iterating would still print its PASS line. booted=$(grep -c 'booted on ' 332_cortex_m_builds_and_boots.sh.log || true) [ "$booted" = "4" ] || { echo "332 booted $booted rows, expected 4"; exit 1; } - # ⚠️ 336 IS HERE FOR THE SAME REASON, AND ADDING IT TO THE LOOP ABOVE + # 336 IS HERE FOR THE SAME REASON, AND ADDING IT TO THE LOOP ABOVE # WITHOUT ADDING IT HERE WOULD HAVE BEEN THE SAME DEFECT: it declares # `# requires: qemu-arm`, so on a shard it exits 0 having run nothing. grep -q 'PASS: armv7-a rows build, boot' \ @@ -232,7 +232,7 @@ jobs: a32=$(grep -c 'booted on virt' 336_armv7a_builds_and_boots.sh.log || true) [ "$a32" = "2" ] || { echo "336 booted $a32 rows, expected 2"; exit 1; } - # ⚠️ 338 SKIPS UNTIL `xim:picolibc-arm` IS PUBLISHED, and a skip here + # 338 SKIPS UNTIL `xim:picolibc-arm` IS PUBLISHED, and a skip here # is legitimate rather than a defect — the payload is a separate # release. So its PASS line is NOT demanded; what IS demanded is that # the script either passed or said why, which is what distinguishes a diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 32f7147e..66c258a8 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -79,7 +79,7 @@ jobs: # Same placement, same reason: pure text, no toolchain. # - # ⚠️ IT PRINTS AND DOES NOT FAIL, DELIBERATELY. The three rules it carries + # IT PRINTS AND DOES NOT FAIL, DELIBERATELY. The three rules it carries # each have real false positives — a control step legitimately pins one # target, a `grep -q .` legitimately expects output — and the whole value # of the thing is that somebody READS it. A hard gate would be routed @@ -124,7 +124,7 @@ jobs: - name: Per-subsystem tests (`mcpp test -p `) run: | set -euo pipefail - # ⚠️ `$MCPP_FRESH` comes from the step above, which takes the NEWEST + # `$MCPP_FRESH` comes from the step above, which takes the NEWEST # mcpp anywhere under target/. That is correct in this job because # nothing between the two steps writes another one — but `mcpp pack` # leaves a copy under `target/dist/`, and a pack step inserted above diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index d5f721e9..7e143ef3 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -262,7 +262,7 @@ jobs: echo "--- otool -L ---"; otool -L "$BIN" || true echo "--- rpaths ---"; otool -l "$BIN" | grep -A2 LC_RPATH || true echo "--- statically embedded libc++? ---" - # ⚠️ `nm ... 2>/dev/null | grep -c` USED TO PRINT "0 (good)" WHEN `nm` + # `nm ... 2>/dev/null | grep -c` USED TO PRINT "0 (good)" WHEN `nm` # ITSELF FAILED. A count of zero and a tool that never ran produce the # same number, and the word "good" is then a claim nothing supports. # This is diagnostic output rather than a gate, which makes it worse @@ -319,7 +319,7 @@ jobs: run: | MCPP=/tmp/mcpp-fresh # the freshly self-hosted binary built from this PR export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # ⚠️ THIS IS THE STEP THE RETRY WAS MEASURED ON: a macOS runner + # THIS IS THE STEP THE RETRY WAS MEASURED ON: a macOS runner # answered `Could not resolve host: github.com` after 30s of # resolver timeout, failing a PR check before a single line of # mcpp had run. See .github/tools/git_clone_retry.sh. diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index 6f1c1873..4c019935 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -1,8 +1,8 @@ name: target matrix -# ⭐⭐ 让支持矩阵成为一次测量的输出,而不是一份会悄悄过期的文档。 +# 让支持矩阵成为一次测量的输出,而不是一份会悄悄过期的文档。 # -# ⚠️ 这套东西存在的理由,是本仓库反复付出的一类代价:一格因为「今天这台机器恰好 +# 这套东西存在的理由,是本仓库反复付出的一类代价:一格因为「今天这台机器恰好 # 装了某个载荷」而通过,或因为没装而跳过,而两者在退出码上与「全部正确」没有区别。 # 三个宿主各扫一遍,把结果与仓库里的期望表比对,差异即失败。 on: @@ -20,7 +20,7 @@ env: jobs: invariants: - # ⭐ 第一层:四条恒等式,不需要期望表,也不依赖机器上装了什么。 + # 第一层:四条恒等式,不需要期望表,也不依赖机器上装了什么。 # 它们是结构约束 —— 任何一格只要跑起来了就该满足。 name: invariants (${{ matrix.host }}) runs-on: ${{ matrix.runner }} @@ -29,13 +29,13 @@ jobs: fail-fast: false matrix: include: - # ⭐⭐ THE BUILD-HOST AXIS IS THE SET mcpp SHIPS FOR, NOT THE SET THAT + # THE BUILD-HOST AXIS IS THE SET mcpp SHIPS FOR, NOT THE SET THAT # WAS CONVENIENT. `release.yml` publishes four: linux-x86_64, # linux-aarch64, macosx-arm64, windows-x86_64. A host mcpp is # distributed for and never scanned is a host whose target table is # a claim nobody checked. # - # ⚠️ `host` IS (os, arch) AND NOT os. Two Linux hosts differ in which + # `host` IS (os, arch) AND NOT os. Two Linux hosts differ in which # rows they serve — `x86_64-linux-gnu` needs the host-native glibc # payload, so it is reachable on one and not the other — and a single # `linux` key would have them overwrite each other in expected.tsv. @@ -57,7 +57,7 @@ jobs: "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true "$MCPP" self config --mirror GLOBAL 2>/dev/null || true "$MCPP" build --dev - # ⚠️ 两种拼写,且按 mtime 取最新 —— target/ 是缓存恢复的,`head -1` + # 两种拼写,且按 mtime 取最新 —— target/ 是缓存恢复的,`head -1` # 会挑到上一次推送留下的二进制,版本号一样而代码是旧的。 BUILT=$(find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) \ -newer mcpp.toml | head -1) @@ -72,7 +72,7 @@ jobs: export MCPP="$MCPP_UNDER_TEST" export MCPP_VENDORED_XLINGS="$XLINGS_BIN" "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - # ⚠️ These four read mcpp's MACHINE interface, so jq is not optional + # These four read mcpp's MACHINE interface, so jq is not optional # here. Without it each one takes its own "nothing to compare" exit — # four honest-looking skips, and the next step would then report that # the invariants did not run. Failing on the cause beats failing on @@ -91,7 +91,7 @@ jobs: done [ "$fail" = 0 ] || exit 1 - # ⚠️ A DEFERRAL NOBODY RECHECKS IS INDISTINGUISHABLE FROM A DEFECT. This + # A DEFERRAL NOBODY RECHECKS IS INDISTINGUISHABLE FROM A DEFECT. This # step fails when its reason STOPS holding — the day an aarch64 llvm is # published — which is the opposite of what a check usually does. - name: The aarch64 llvm deferral still has its reason @@ -101,7 +101,7 @@ jobs: - name: Each invariant RAN run: | set -euo pipefail - # ⭐ 这一步存在的全部理由:退出码分不清「通过」与「跳过」。两条 e2e 都 + # 这一步存在的全部理由:退出码分不清「通过」与「跳过」。两条 e2e 都 # 有为「这台机器没有可比的东西」准备的早退,而 CI 要的是它们真的比 # 过了。 check() { @@ -112,19 +112,19 @@ jobs: } echo " ok $1" } - # ⚠️⚠️ A SKIP IS ACCEPTED FOR ONE NAMED REASON, NOT ON ONE NAMED HOST. + # A SKIP IS ACCEPTED FOR ONE NAMED REASON, NOT ON ONE NAMED HOST. # # 297 declares a non-llvm compiler, so it needs one to exist. Every # toolchain mcpp installs on macOS is llvm; on windows-2022 it depends # on what the restored cache holds — measured, one run had # `gcc@16.1.0` and the next had only `llvm@20.1.7`. # - # ⚠️ THE FIRST VERSION EXEMPTED macOS BY NAME, and the very next + # THE FIRST VERSION EXEMPTED macOS BY NAME, and the very next # Windows run skipped for the same reason and went red. Naming the # host encodes where the fact happened to hold; naming the FACT holds # wherever it does. A skip for any other reason is still a failure. # - # ⭐ AND THE DENOMINATOR IS ASSERTED SEPARATELY: linux always has a gcc + # AND THE DENOMINATOR IS ASSERTED SEPARATELY: linux always has a gcc # payload (it backs the host row), so that job uses `check` and the # test is guaranteed to be exercised somewhere on every run. Without # that, a reason accepted everywhere is a test that runs nowhere. @@ -158,7 +158,7 @@ jobs: check 298_overriding_a_convention_requires_replacing_it.sh \ "OK: a convention may be overridden, but not merely removed" || fail=1 else - # ⚠️ THE MIRROR OF 297's EXEMPTION, AND FOR THE OTHER FAMILY. + # THE MIRROR OF 297's EXEMPTION, AND FOR THE OTHER FAMILY. # # 298 declares llvm, and there is no llvm payload for aarch64 Linux — # upstream stopped publishing linux-aarch64 after 19.x and the index @@ -167,7 +167,7 @@ jobs: # SKIP: llvm is not installed here, and this test is about # declaring it # - # ⭐ Granting it by REASON rather than by host is what makes it + # Granting it by REASON rather than by host is what makes it # retire itself: the day # `.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md` # lands an aarch64 llvm, this stops being a skip and starts being @@ -179,7 +179,7 @@ jobs: # ── 2026.8.26.2: an answer mcpp already had, now used ───────────── # - # ⭐ 299/300/303 CARRY NO SKIP AT ALL, so they use `check` on every + # 299/300/303 CARRY NO SKIP AT ALL, so they use `check` on every # host. They read the vocabulary and the query's own document — # neither depends on which payloads this machine happens to hold, and # a version of them that skipped anywhere would be a version that @@ -188,7 +188,7 @@ jobs: "OK: a request that named no C library resolves to a row that exists" || fail=1 check 300_a_registered_family_is_not_reported_unknown.sh \ "OK: a registered family is not reported unknown" || fail=1 - # ⚠️ 303's third half stacks a musl c-abi over this host's own target, + # 303's third half stacks a musl c-abi over this host's own target, # and not every host stacks that — an MSVC-ABI host answers the # layering question first, correctly, and there is then no # two-answer document to check. Granted by reason; linux-x86_64 below @@ -202,13 +202,13 @@ jobs: "refuses a musl c-abi over its own target" || fail=1 fi - # ⚠️ 301/302 NEED TWO COMPILER FAMILIES, AND THAT IS A PROPERTY OF THE + # 301/302 NEED TWO COMPILER FAMILIES, AND THAT IS A PROPERTY OF THE # MACHINE RATHER THAN OF THE CLAIM. "A requirement that DIFFERS from # mcpp's own answer is applied" cannot be stated where only one family # exists — macOS installs llvm only, and aarch64 Linux has no llvm # payload at all (see 298's note). # - # ⭐ AND THE DENOMINATOR IS linux-x86_64, WHICH HAS BOTH. Without a + # AND THE DENOMINATOR IS linux-x86_64, WHICH HAS BOTH. Without a # host required to actually run these, a reason accepted everywhere is # a test that runs nowhere. if [ "${{ matrix.host }}" = linux-x86_64 ]; then @@ -228,7 +228,7 @@ jobs: [ "$fail" = 0 ] || exit 1 scan: - # ⭐ 第二层:全表扫描,与仓库里的期望表比对。 + # 第二层:全表扫描,与仓库里的期望表比对。 name: scan (${{ matrix.host }}) needs: invariants runs-on: ${{ matrix.runner }} @@ -237,13 +237,13 @@ jobs: fail-fast: false matrix: include: - # ⭐⭐ THE BUILD-HOST AXIS IS THE SET mcpp SHIPS FOR, NOT THE SET THAT + # THE BUILD-HOST AXIS IS THE SET mcpp SHIPS FOR, NOT THE SET THAT # WAS CONVENIENT. `release.yml` publishes four: linux-x86_64, # linux-aarch64, macosx-arm64, windows-x86_64. A host mcpp is # distributed for and never scanned is a host whose target table is # a claim nobody checked. # - # ⚠️ `host` IS (os, arch) AND NOT os. Two Linux hosts differ in which + # `host` IS (os, arch) AND NOT os. Two Linux hosts differ in which # rows they serve — `x86_64-linux-gnu` needs the host-native glibc # payload, so it is reachable on one and not the other — and a single # `linux` key would have them overwrite each other in expected.tsv. @@ -269,13 +269,13 @@ jobs: [ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; } echo "MCPP_UNDER_TEST=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT")" >> "$GITHUB_ENV" - # ⚠️⚠️ 格数不能是缓存状态的函数。 + # 格数不能是缓存状态的函数。 # # 实测:同一台 ubuntu-24.04,一轮装了 gcc+llvm(扫 40 格),下一轮只有 gcc # (扫 20 格)。`expected.tsv` 声明的是前者,于是后者会把所有 llvm 行报成 # 「期望表说有而扫描没跑到」—— 而那句报错是对的,问题在于覆盖面**漂移**了。 # - # ⭐ 矩阵要声明它扫哪些工具链,并把它们装上。装不上就红在这里,而不是 + # 矩阵要声明它扫哪些工具链,并把它们装上。装不上就红在这里,而不是 # 变成一屏「没跑到」。 - name: Install the toolchains this matrix declares run: | @@ -309,7 +309,7 @@ jobs: export MCPP="$MCPP_UNDER_TEST" export MCPP_VENDORED_XLINGS="$XLINGS_BIN" "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - # ⚠️⚠️ 编译器轴跟着**声明**走,不跟着「这台机器上装了什么」走。 + # 编译器轴跟着**声明**走,不跟着「这台机器上装了什么」走。 # # 实测 2026-08-26,同一个提交:PR 上这个 job 绿,合入 main 后红 —— 那 # 次 windows-2022 恢复出来的缓存里多了一个 `gcc@16.1.0`,扫描产出 24 @@ -322,14 +322,14 @@ jobs: | sort -u | tr '\n' ' ')" export MATRIX_COMPILERS echo "declared compiler axis: ${MATRIX_COMPILERS:-}" - # ⚠️ 两种体系各自成表。scan 把 mode 写进第一列,而比对必须按 mode 分开 + # 两种体系各自成表。scan 把 mode 写进第一列,而比对必须按 mode 分开 # 做 —— 拿一种体系的测量去比整张表,另一种的每一行都会被报成「没跑到」。 bash tests/matrix/scan.sh payload > measured-payload.tsv bash tests/matrix/scan.sh graph > measured-graph.tsv cat measured-payload.tsv measured-graph.tsv > measured.tsv echo "--- measured ---"; cat measured.tsv - # ⚠️ 上传排在比对之前,而这是刻意的次序。宿主的第一次运行本就没有期望行, + # 上传排在比对之前,而这是刻意的次序。宿主的第一次运行本就没有期望行, # 比对会红 —— 而回填要用的正是这份产物。`if: always()` 也保留:一步失败不 # 该把证据一起带走。 - uses: actions/upload-artifact@v4 @@ -349,10 +349,10 @@ jobs: [ "$fail" = 0 ] || exit 1 coverage: - # ⭐⭐ THE DENOMINATOR. Every check above is per host, and no per-host check + # THE DENOMINATOR. Every check above is per host, and no per-host check # can notice a host that never ran. # - # ⚠️ Each `scan` job compares the rows for ITS OWN host, so deleting a host + # Each `scan` job compares the rows for ITS OWN host, so deleting a host # from the matrix above silently retires every expectation the table holds # for it: nothing measures those rows, nothing compares them, and the # workflow is green. This job is the one place that reads the expected table @@ -388,7 +388,7 @@ jobs: fail=1 } done - # ⭐ AND THE OTHER DIRECTION. A host that scanned but has no rows in the + # AND THE OTHER DIRECTION. A host that scanned but has no rows in the # table is a new build host nobody declared expectations for — the # per-host compare already reds on it, but saying it here names the # cause rather than listing 40 unexplained cells. @@ -405,7 +405,7 @@ jobs: - name: The build hosts mcpp ships for are the ones scanned run: | set -euo pipefail - # ⚠️⚠️ THE TABLE AND THE RELEASE MUST NAME THE SAME SET. A host that + # THE TABLE AND THE RELEASE MUST NAME THE SAME SET. A host that # gets a published binary and no scan is a host whose target table is # a claim nobody checked; a host that is scanned and never shipped is # coverage spent on a machine no user has. diff --git a/.github/workflows/ci-windows-msvc-xlings.yml b/.github/workflows/ci-windows-msvc-xlings.yml index f493fb5d..1ed1ff1f 100644 --- a/.github/workflows/ci-windows-msvc-xlings.yml +++ b/.github/workflows/ci-windows-msvc-xlings.yml @@ -78,7 +78,7 @@ jobs: # Name what this job runs, so the job title and its contents cannot # drift apart. Widen the glob when a second test joins. E2E_ONLY: '239_*.sh' - # ⚠️ THE DEFAULT PER-TEST CAP IS TOO CLOSE TO WHAT THIS TEST COSTS. + # THE DEFAULT PER-TEST CAP IS TOO CLOSE TO WHAT THIS TEST COSTS. # 239 fetches ~376 MB, and its measured durations on this runner are # 244 / 292 / 313 / 335 / 515s against run_all.sh's 600s default — a # download-bound test whose spread nearly reaches its own deadline, diff --git a/.github/workflows/openkal-cross.yml b/.github/workflows/openkal-cross.yml index a9737419..3ac63d1d 100644 --- a/.github/workflows/openkal-cross.yml +++ b/.github/workflows/openkal-cross.yml @@ -1,6 +1,6 @@ name: openkal cross-build (3 hosts × 3 targets) -# ⭐⭐ WHAT THIS WORKFLOW ASSERTS, AND WHY IT IS A MATRIX RATHER THAN A ROW. +# WHAT THIS WORKFLOW ASSERTS, AND WHY IT IS A MATRIX RATHER THAN A ROW. # # `cross-build-test.yml` verifies the crosses served by a PAYLOAD: a toolchain # whose driver has exactly one target. There the host and the target are joined @@ -13,7 +13,7 @@ name: openkal cross-build (3 hosts × 3 targets) # claim that follows is that N hosts × N targets collapses to N implementations # plus one tool: **the machine doing the building stops being a variable.** # -# ⚠️ THAT IS A CLAIM, AND CLAIMS OF THIS SHAPE HAVE BEEN WRONG IN THIS +# THAT IS A CLAIM, AND CLAIMS OF THIS SHAPE HAVE BEEN WRONG IN THIS # REPOSITORY. Reaching PE from a Linux host needed four separate repairs, and # adding the other two hosts found seven more — every one of them a decision # that had been keyed on which machine was building rather than on which machine @@ -36,20 +36,20 @@ name: openkal cross-build (3 hosts × 3 targets) # produced by ALL THREE hosts. # # build on Linux build on macOS build on Windows -# run Linux ✓ ✓ ✓ -# run macOS ✓ ✓ ✓ -# run Windows ✓ ✓ ✓ +# run Linux +# run macOS +# run Windows # -# ⭐ The diagonal is an ordinary native build. The six off-diagonal cells are +# The diagonal is an ordinary native build. The six off-diagonal cells are # the claim, and they are what a single-host workflow cannot reach. # -# ⚠️ THE RUN JOBS INSTALL NOTHING — not mcpp, not a compiler, not a C runtime. +# THE RUN JOBS INSTALL NOTHING — not mcpp, not a compiler, not a C runtime. # A program above openkal carries its C library, its C++ runtime and its # unwinder; what remains is the operating system it was built for. If a # toolchain step is ever added to one of them because "the program needs it", # that is the finding rather than the fix. # -# ⚠️ AND THE ASSERTION IS ON THE OUTPUT, NOT THE EXIT STATUS. The program prints +# AND THE ASSERTION IS ON THE OUTPUT, NOT THE EXIT STATUS. The program prints # four lines, and `unwound: true` is the one a link cannot fake: it says a # destructor ran while an exception was being carried out of a frame, which # means the unwinder found this image's own frame descriptions. @@ -63,14 +63,14 @@ concurrency: cancel-in-progress: true env: - # ⚠️ No mcpp or xlings version here. `bootstrap-mcpp` owns both, and a second + # No mcpp or xlings version here. `bootstrap-mcpp` owns both, and a second # statement of them is a second thing to keep in step — the pin check # (.github/tools/check_version_pins.sh) enforces the ones that exist and would # not know about a copy in this file. XLINGS_NON_INTERACTIVE: '1' # The branch of the openkal packages this change is verified against. # - # ⚠️ IT WAS `feat/openkal-closure` UNTIL 2026-08-25, LONG AFTER THAT BRANCH + # IT WAS `feat/openkal-closure` UNTIL 2026-08-25, LONG AFTER THAT BRANCH # MERGED. A fixed name here is a pin nobody is reminded to move: the comment # said "when they are on `main` this becomes `main`" and the moment for that # passed without anyone reading it again. Every run since was verifying this @@ -97,7 +97,7 @@ jobs: steps: - uses: actions/checkout@v4 - # ⚠️ THE REPOSITORY'S OWN BOOTSTRAP, NOT A SECOND ONE. + # THE REPOSITORY'S OWN BOOTSTRAP, NOT A SECOND ONE. # # This job first wrote its own: fetch xlings, then # `xlings install mcpp@`. It failed on the very first run: @@ -116,7 +116,7 @@ jobs: # was wrong within a day of being written. - uses: ./.github/actions/bootstrap-mcpp - # ⭐ THE mcpp UNDER TEST. Everything after this uses the binary this step + # THE mcpp UNDER TEST. Everything after this uses the binary this step # produces; the bootstrapped one above is only what compiles it. - name: Build the mcpp in this pull request run: | @@ -124,11 +124,11 @@ jobs: export MCPP_VENDORED_XLINGS="$XLINGS_BIN" "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - # ⚠️ `--dev` and not `--release`. What is under test is a set of + # `--dev` and not `--release`. What is under test is a set of # decisions about compile and link flags; an optimisation level # changes none of them and a release self-build is most of the budget # of a job on a two-core runner. - # ⚠️⚠️ REMOVE ANY mcpp ALREADY UNDER `target/` FIRST, so that after the + # REMOVE ANY mcpp ALREADY UNDER `target/` FIRST, so that after the # build there is exactly ONE and it is the one this step produced. # # `target/` is a RESTORED CACHE whose key hashes `src/**`. A source @@ -139,7 +139,7 @@ jobs: # whichever directory the walk reaches first, which is not the newest # and is not this build's. # - # ⚠️ MEASURED, LOCALLY AND ON CI. On this developer's own tree the + # MEASURED, LOCALLY AND ON CI. On this developer's own tree the # command picks `007bc0f2b78f7fa5` while the build just wrote # `148448dec6c4a7a7`. On CI it picked a stale binary whose ELF # interpreter names a glibc payload the runner no longer has: @@ -151,7 +151,7 @@ jobs: # trigger is simply a source change large enough to leave two # directories in the restored tree. # - # ⭐ mtime CANNOT BE THE CRITERION HERE. A cache restore writes every + # mtime CANNOT BE THE CRITERION HERE. A cache restore writes every # file at extraction time, so "newest" is meaningless on exactly the # tree where this goes wrong — `ls -t | head -1` has picked a stale # fingerprint in this repository before. Absence can't be faked: @@ -160,10 +160,10 @@ jobs: # The objects and BMIs stay cached — this costs one link. find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) -delete 2>/dev/null || true "$MCPP" build --dev - # ⚠️ EXACTLY ONE, not "at least one". Two would mean the delete above + # EXACTLY ONE, not "at least one". Two would mean the delete above # missed a tree and the ambiguity this step exists to remove is back. # - # ⚠️ NO `mapfile`. This job also runs on macos, whose `/bin/bash` is + # NO `mapfile`. This job also runs on macos, whose `/bin/bash` is # 3.2 and has no such builtin — a bashism here would fail on one row # of the matrix for a reason unrelated to what the row tests. find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) > /tmp/mcpp-built.txt @@ -181,7 +181,7 @@ jobs: - name: Select the toolchain the openkal packages ask for run: | set -euo pipefail - # ⚠️ Install, then select. `toolchain default` names one and does not + # Install, then select. `toolchain default` names one and does not # fetch it. "$MCPP_UNDER_TEST" self config --mirror GLOBAL 2>/dev/null || true "$MCPP_UNDER_TEST" toolchain install llvm 22.1.8 @@ -190,7 +190,7 @@ jobs: - name: The program — one source, three targets run: | set -euo pipefail - # ⚠️ THE SEVENTH CALL SITE. `git_clone_retry.sh` was written because a + # THE SEVENTH CALL SITE. `git_clone_retry.sh` was written because a # runner's DNS hiccup is not a red build, and its own note counts # "six call sites, one failure mode" — this workflow was not among # them, and the mode duly arrived here. Measured on this job, @@ -206,7 +206,7 @@ jobs: https://github.com/mcpplibs/openkal-llvm-runtime "$RUNNER_TEMP/okl" cd "$RUNNER_TEMP/okl/examples/same-source" mkdir -p "$RUNNER_TEMP/out" - # ⚠️ The three HOSTED targets. Bare metal is verified by + # The three HOSTED targets. Bare metal is verified by # `openkal-llvm-runtime`'s own CI under qemu; it has no runner here to # execute on, and a build-only cell in a workflow whose point is # running would be the weaker claim. @@ -245,7 +245,7 @@ jobs: run: shell: bash steps: - # ⚠️ NO checkout AND NO toolchain. This job is the claim: a program built + # NO checkout AND NO toolchain. This job is the claim: a program built # above openkal needs the operating system it was built for and nothing # else. Anything installed here would weaken what a pass means. - uses: actions/download-artifact@v4 @@ -261,9 +261,9 @@ jobs: if [ ! -f "$bin" ]; then echo "::error::$bin is missing"; fail=1; continue fi - # ⚠️ The executable bit does not survive an artefact upload. + # The executable bit does not survive an artefact upload. chmod +x "$bin" || true - # ⚠️ arm64 macOS refuses an unsigned image, so the signature is + # arm64 macOS refuses an unsigned image, so the signature is # asserted before the run: a failure here is "the linker did not # ad-hoc sign it", which is a different repair from "it crashed". if [ "${{ matrix.system }}" = "macos" ]; then @@ -277,7 +277,7 @@ jobs: ok=1 grep -q 'sorted: 2 4 7' out.log || ok=0 grep -q 'caught: 42' out.log || ok=0 - # ⭐ The line a link cannot fake. + # The line a link cannot fake. grep -q 'unwound: true' out.log || ok=0 grep -q 'import std over openkal: ok' out.log || ok=0 [ "$ok" = 1 ] || { echo "::error::built on $host: wrong output"; fail=1; } @@ -289,7 +289,7 @@ jobs: # The e2e scripts that BUILD the openkal ecosystem, on a runner that # has what they ask for. # - # ⚠️ THEY WERE WRITTEN AND THEY WERE NEVER RUN. `285`–`289` declare + # THEY WERE WRITTEN AND THEY WERE NEVER RUN. `285`–`289` declare # `# requires: llvm`, and the linux e2e shards report # # Detected capabilities: elf unix-shell fresh-sandbox gcc @@ -324,7 +324,7 @@ jobs: "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true "$MCPP" self config --mirror GLOBAL 2>/dev/null || true "$MCPP" build --dev - # ⚠️ NEWEST BY MTIME, NOT FIRST BY DIRECTORY ORDER. `target/` holds one + # NEWEST BY MTIME, NOT FIRST BY DIRECTORY ORDER. `target/` holds one # directory per fingerprint and the runner restores a cache of it, so # `find … | head -1` can return a binary an earlier run left behind. # Measured: it reported the right VERSION STRING — the stale copy was @@ -349,7 +349,7 @@ jobs: "$MCPP_UNDER_TEST" toolchain install gcc 16.1.0 "$MCPP_UNDER_TEST" toolchain install llvm 22.1.8 - # ⚠️ THE EMULATORS, OR TWO OF THE SIX MEASURE HALF OF WHAT THEY SAY. + # THE EMULATORS, OR TWO OF THE SIX MEASURE HALF OF WHAT THEY SAY. # # 287 and 288 both end by RUNNING what they built — an aarch64 binary and # a riscv64 machine image — and both degrade to a SKIP when no emulator @@ -361,7 +361,7 @@ jobs: # have called that covered. Linking is not booting, as the script itself # says. # - # ⚠️ BOTH homes, for the reason ci-linux-e2e.yml's baremetal job records: + # BOTH homes, for the reason ci-linux-e2e.yml's baremetal job records: # the shim on PATH dispatches against whichever home owns it, so an # emulator installed only in the ambient one answers "not installed" when # mcpp asks. @@ -400,7 +400,7 @@ jobs: - name: Each one RAN run: | set -euo pipefail - # ⭐ THE ASSERTION THIS JOB EXISTS FOR. A zero exit code cannot + # THE ASSERTION THIS JOB EXISTS FOR. A zero exit code cannot # distinguish "passed" from "skipped" — every one of these scripts # has an early `exit 0` for a capability or an arrangement it did # not find. The PASS line can. @@ -419,7 +419,7 @@ jobs: "OK: the openkal stack builds, links statically and runs" || fail=1 check 287_the_openkal_stack_crosses_to_aarch64.sh \ "OK: the openkal stack crosses to aarch64, supplies its atomics helpers and runs" || fail=1 - # ⭐ AND IT REACHED THE PARTS THAT NEED A TOOL. Both of 287's last two + # AND IT REACHED THE PARTS THAT NEED A TOOL. Both of 287's last two # assertions degrade to a SKIP, and the OK line prints either way. check 287_the_openkal_stack_crosses_to_aarch64.sh \ "LSE instructions out of" || fail=1 @@ -427,7 +427,7 @@ jobs: "it runs under qemu-aarch64" || fail=1 check 288_the_openkal_stack_on_a_machine_with_no_os.sh \ "OK: openkal runs on a machine with no operating system and no C library" || fail=1 - # ⭐ 288's name says "runs"; without this it can print that line + # 288's name says "runs"; without this it can print that line # having only linked. check 288_the_openkal_stack_on_a_machine_with_no_os.sh \ "it boots" || fail=1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f19082f..e0cf5fba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -865,7 +865,7 @@ jobs: - name: Mirror binaries to xlings-res/mcpp (gh + gtc) if: ${{ env.XLINGS_RES_TOKEN != '' }} - # ⚠️⚠️ THIS MUST BE LONGER THAN THE SCRIPT'S OWN LEG DEADLINE, AND FOR + # THIS MUST BE LONGER THAN THE SCRIPT'S OWN LEG DEADLINE, AND FOR # A LONG TIME IT WAS SHORTER. # # `mirror_res.sh` gives GitCode `MIRROR_LEG_DEADLINE_GTC=2400s` because @@ -877,7 +877,7 @@ jobs: # with only the small `.sha256` files through. v2026.8.25.1 and # v2026.8.25.2 both, measured. # - # ⚠️ The paragraph this replaces described a design that no longer + # The paragraph this replaces described a design that no longer # exists: a per-asset `MIRROR_UPLOAD_TIMEOUT` of 180s. The script's own # comment records why it went ("the old per-asset cap failed four # releases in a row") — but this value, calibrated to it, stayed. diff --git a/CHANGELOG.md b/CHANGELOG.md index ab936b25..ede4e262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,97 @@ ## [Unreleased] +## [2026.9.5.3] — 2026-09-05 + +### 官方构建插件集中为一个包:`mcpp:plugins` + +规则包不再放在本仓库的 `examples/` 下。它们现在集中维护于 +`mcpp-community/mcpp-plugins`,以一个包 `mcpp:plugins` 发布,消费者用 feature 选择成员, +在 `build.mcpp` 里以成员自己声明的模块名 import: + +```toml +[dependencies.mcpp] +plugins = { version = "0.1.0", features = ["rules-spirv"], host-module = true } +``` + +命名收敛为两族:规则包 `mcpp.rules.`,构建期工具 `mcpp.tools.`;lib 根 +`mcpp.plugins` 记录集合的版本。此前的 `mcpp.build.` 撤回:那是引擎自己的模块族 +(`mcpp.build.plan`、`mcpp.build.prepare`),插件不该与它同名。规则包规范曾以「模块名就是 +裸包名,不能含点」为由撤回 `mcpp.rules.*`;I1(模块名由源码声明)落地后这个理由不再成立, +规范的 I8 与第 7 节按此修订。 + +**引擎为此扩了一处**:一个 `host-module = true` 的包,其解析后 `[build] sources` 里 +—— 含 feature 加入的源文件 —— 的每一个模块接口单元都编成一个 host 模块,以各自声明的名字 +注册,lib 根排在最前,feature 单元可以 import 它。只有清单里列出的源文件参与:未声明 +`sources` 的包所推断出的 `src/**` 不被读取,所以此前发布的规则包暴露的仍是它当时暴露的那 +一个模块。模块集合就是 feature 集合:未激活 feature 的单元不编译,import 它以未知模块失败。 +e2e 610 分别断言这五条(含 `mcpp` 命名空间零告警、其他命名空间**每个单元一条**告警的对照); +`tests/unit/test_provisions` 覆盖接口单元的判定:实现单元、分区、全局模块片段与注释里引用的 +声明都不算。 + +示例 09 与 10 像任何工程一样从索引消费 `mcpp:plugins`;`mcpplibs:rules-cuda@0.1.0` +在索引里保留并标注被取代。 + +### 设备源的判据是编译器,不是厂商 + +`SourceKind::Device` 的定义写着它陈述的是构建图里的角色 ——「由一个 mcpp 不驱动的 +设备编译器编译」—— 并且明确说这样定义是为了让分类表**不按厂商长出一行**。而那张 +扩展名表里恰好只有两行,都是 NVIDIA 的。 + +第二个设备 API 让这件事显形:一个写在带约束 glob 里的 shader 被拒绝为 + + 'scale.comp' is listed in [build] sources, and mcpp has no role for the + extension '.comp'. + +同一次运行里,规则包又被告知没有设备源并为此告警 —— 一个错误和一个警告在说同一个 +文件的相反的话。 + +表因此扩到「由另一个编译器消费的语言」:CUDA 与 HIP,GLSL 的各个 stage 与无 stage 的 +`.glsl`,HLSL,OpenCL C,Metal。共 18 个扩展名,完整清单见 `docs/20-heterogeneous-builds.md`。 + +这次扩表不能改变任何今天可用的构建,理由有两条且互相独立:设备扩展名**本来就不在** +默认 source glob 里,所以没有 glob 变宽;而这些扩展名今天在 `sources` 里是**硬错误**, +所以没有文件静默换了角色。两条都由 `tests/unit/test_source_kind` 断言,其中默认 glob +那条断言的是**整张列表**而不是曾经在场的两个名字。 + +### `mcpp.rules.spirv` 与 examples/10-vulkan-compute + +新的规则把「GLSL 如何变成 SPIR-V」陈述一次,与 `mcpp.rules.cuda` 同形:引擎拥有图 —— +加速器轴、把 shader 路由到规则包而非 C++ 编译器的带约束 glob、动作边、指纹 —— +并且不认识 "vulkan" 或 "glslang" 这两个词;规则包拥有拼法。 + +它产出的是**头文件而不是目标文件**。SPIR-V 是程序交给 `vkCreateShaderModule` 的数据, +所以动作以 `role = "source"` 提交 —— 这是引擎**排在编译之前**的那一个角色,正是生成的 +头文件需要的;`artifact` 角色的产物排在链接之前,那已经晚于包含它的那个编译单元。 + +示例 10 用同一份产物在三处得到同一个结果:宿主 ICD 上的 RTX 4080、只给 `VK_DRIVER_FILES` +的软件驱动载荷(无 GPU 参与)、以及 `mcpp build --no-accel` 下接缝后的 CPU 实现。 + +### 修正 + +- **变体切换不再被快路径回放。** 设备变体在指纹里,两次构建落在不同目录;而快路径在任何 + 计划存在之前运行,回放的是**最后一次**构建的目录。实测:`mcpp build`、 + `mcpp build --no-accel`、`mcpp build` —— 第三次报 `Finished in 0.00s`,随后的 `mcpp run` + 执行的是 CPU 变体。build.ninja 的头行现在记录选择来自清单还是来自 `--accel`/`--no-accel` + (`accel=default|override`),两条快路径只回放前者;缺该字段的旧图按未命中处理并被重写。 + e2e 611 与 `tests/unit/test_graph_shape` 断言。 +- `MCPP_DEVICE_SOURCES` 以换行分隔。规则包若按 `;` 切分,对**恰好一个**设备源仍然正确, + 对两个则拼出一条不存在的路径;e2e 609 第一段以整张扩展名表作分母,当场把它抓了出来。 +- glslang 若未链入 spirv-opt(`-Os not available; optimizer not linked`),规则包降级 + 并告警一次,而不是让构建失败:可选的优化 pass 缺席不是错误,静默地不做优化才是。 + +### 文档 + +- 第 20 章由「加速器」更名为「异构硬件构建」(`docs/20-heterogeneous-builds.md`),副题指明 + GPU 与 AI 加速器目标以及宿主/设备混合编译;`accel` 键不变。 +- 第 5 章与第 7 章(中英)补入 feature 选择的规则集合与 `mcpp.rules.*` / `mcpp.tools.*` + 命名;第 7 章中文版此前缺少命名一节,本次补齐。 +- 文档、README、CHANGELOG 与代码注释里的装饰符号(告警、星标、勾叉等)全部移除;表格里 + 只以符号承载的取值改写为词(`yes` / `no` / `partial` / `planned`)。 + ## [2026.9.5.2] — 2026-09-05 -### ⭐⭐ 在编译任何东西之前比较机器的下界 +### 在编译任何东西之前比较机器的下界 有些机器事实**限定**了能为它构建什么,而忽略它们时,失败到得很晚。本轮的样本: 设备运行时不得新于它将运行其上的驱动;当它更新时,构建与链接都干净通过,程序在 @@ -26,7 +114,7 @@ value = "cuda.driver >= 12.0" `mcpp.build.version_floor` 只做比较,**这个文件里不出现任何厂商名字**: `cuda.driver` 是流经的数据。第二种后端不需要改动它。 -### ⭐ 探针通道:`mcpp::fact` / `mcpp::floor`(协议 v7) +### 探针通道:`mcpp::fact` / `mcpp::floor`(协议 v7) 构建程序陈述它**测得**的事实与它**需要**的下界,引擎比较并在不满足时给出两侧取值 (`version-floor-unmet`)。这是让 CUDA 探针得以整体离开 `src/` 的那条通道 —— @@ -100,7 +188,7 @@ ninja 与 patchelf。实测:空 home 下 26 秒 / 126 MB → 0.3 秒。 ## [2026.9.5.1] — 2026-09-05 -### ⭐⭐ 加速器支持:设备编译单元、产物身份的加速器维、以及没人做的宿主编译器配对 +### 加速器支持:设备编译单元、产物身份的加速器维、以及没人做的宿主编译器配对 一个为某个计算能力编译的库,被另一个计算能力的构建消费时,链接干净地完成, 程序在第一次 kernel 启动时失败,消息里既没有包名也没有任何一侧期望的架构。 @@ -145,7 +233,7 @@ generic target 取得同样的覆盖,所以空的下界不放宽任何东西。 ## [2026.9.4.3] — 2026-09-04 -### ⭐⭐ `mcpp run` 报告程序自己的退出码 +### `mcpp run` 报告程序自己的退出码 在此之前所有非零退出码都被折成 `1`,为的是让 `2` 表示「起不来」以区别于「跑了但 失败」。区别值得保留,代价不值得:`main` 返回 `3` 的程序让 `mcpp run` 退 `1`, @@ -169,7 +257,7 @@ qemu 报 `3` 的裸机镜像同样到达为 `1`。一条报不出退出码的命 **兼容性**:mcpp 自身的配置错误仍是 `2`,与其余所有命令一致 —— 变的只有「尝试 启动后被拒」这一种情况,而那时程序根本没运行。契约写在 `docs/11` §6。 -### ⚠️⚠️ 没有构建进程能比启动它的 mcpp 活得更久 +### 没有构建进程能比启动它的 mcpp 活得更久 实测:每一次被 `timeout` 终止的 `mcpp run` 都留下一个空转占满一个核的 ninja, 其中一个的工作目录已经是 `(deleted)`、比它所属的整个沙箱活得还久。任何用 @@ -191,7 +279,7 @@ SIGINT/SIGTERM/SIGHUP 时对该组发 **SIGKILL**。用 SIGKILL 而不是 SIGTER ## [2026.9.4.2] — 2026-09-04 -### ⭐⭐ Cortex-M 有 C 库了:`libdir` 填上,而它的键是**三元组** +### Cortex-M 有 C 库了:`libdir` 填上,而它的键是**三元组** `xim:picolibc-arm@1.8.12` 带七个多库(picolibc + compiler-rt builtins,同一个 LLVM 一起构建)。工程一行选入: @@ -201,7 +289,7 @@ LLVM 一起构建)。工程一行选入: sysroot = "xim:picolibc-arm@1.8.12" ``` -⚠️⚠️ **`libdir` 这一列在 ARM 上是三元组,不是 `/`,而差别是一次 +**`libdir` 这一列在 ARM 上是三元组,不是 `/`,而差别是一次 无人报告的 ABI 替换。** riscv 的 `mabi` **就是**浮点 ABI(`lp64d` 与 `lp64` 是 两个值);ARM 的 `mabi` 是过程调用标准,两个变体都是 `aapcs`,浮点 ABI 在三元组的 `eabi`/`eabihf` 后缀里。于是 `armv7e-m/aapcs` 会给两份互不兼容的库命名同一个目录。 @@ -210,17 +298,17 @@ sysroot = "xim:picolibc-arm@1.8.12" `armv7e-m/aapcs/libc.a` 带着 `Tag_ABI_HardFP_use` —— 硬浮点的构建,坐在软浮点 程序会去找它的位置上。**构建期什么都没报。** -⭐ 填这一列**不**给这些行默认配 C 库:只有解析出 sysroot 之后才会读它,目标表仍然 +填这一列**不**给这些行默认配 C 库:只有解析出 sysroot 之后才会读它,目标表仍然 不绑定任何一个。零 libc 仍是默认,这只是让选入这件事能成立。 判据是浮点 ABI 而不是「链接通过」:`tests/e2e/338` 构建**软浮点**那一行,断言产物 -里没有硬浮点 ABI 标记,然后启动它并读退出码。⚠️ 并且断言产物**不为空** —— 实测: +里没有硬浮点 ABI 标记,然后启动它并读退出码。并且断言产物**不为空** —— 实测: 缺了 crt0 的链接会成功并报 `Size fw text 0 data 0`,一个格式良好、什么都没有的 ELF,任何只看 `Finished` 的检查都会放行。 runner 有了名字,工具有了档位,`--locked` 成为断言,`mcpp emit sbom`。 -### ⭐⭐ 一条命令,加具名的例外 +### 一条命令,加具名的例外 `mcpp run` 覆盖常见情形的**全部,真实硬件也一样**。在设备上「运行一个程序」意味着 写进去、复位、接上输出、读回退出状态 —— 这是**一条**命令(`probe-rs run`、 @@ -233,15 +321,15 @@ mcpp run --runner flash # 具名的例外:只写不跑、看串口、起调 mcpp run --list-runners # 这个工程提供了哪些 ``` -⚠️ **引擎不认识任何 runner 名字。** `flash`、`serve`、`deploy`、`submit`、 +**引擎不认识任何 runner 名字。** `flash`、`serve`、`deploy`、`submit`、 `logcat` 对它一样陌生。**引擎里若有一份固定的名字表,就等于由引擎决定哪些领域可被 表达** —— 一个 web 包将无法自己加 `serve`。 -⭐ **写程序名,不要写路径。** mcpp 先找本包 `[xlings] deps` 声明的载荷 `bin/`, +**写程序名,不要写路径。** mcpp 先找本包 `[xlings] deps` 声明的载荷 `bin/`, 再找 `PATH`。用 `xpkg_dir` 拼绝对路径是多余的,而且引入了一个失败模式:声明不是 安装,查询返回空则没有配置任何 runner 而无话可说。 -⚠️ 是否终止由 `mcpp::runner_longlived(name)` **声明**: +是否终止由 `mcpp::runner_longlived(name)` **声明**: `openocd -c "program … exit"` 会终止而 `openocd -c "init"` 不会,拼写到最后一个 参数为止都一样,没有任何 argv 能表达这个区别。 @@ -253,16 +341,16 @@ mcpp run --list-runners # 这个工程提供了哪些 锁一直是解析之后写、从不读回。现在它是断言:发生的解析必须等于记录的解析,不等则 **点名移动了的包与两个版本**。 -⚠️⚠️ **它绝不能遇上快路径。** 实测:加闸之前,一份被故意改坏的锁通过了 +**它绝不能遇上快路径。** 实测:加闸之前,一份被故意改坏的锁通过了 `mcpp build --locked` 并打印 `Finished` —— 旗标被接受、构建正确、**断言从未跑到**。 ### `mcpp emit sbom` -CycloneDX 1.5,覆盖**已记录**的解析。⚠️ 读锁而不是重新解析 —— 一份描述了与所构建 +CycloneDX 1.5,覆盖**已记录**的解析。读锁而不是重新解析 —— 一份描述了与所构建 者不同的图的文档比没有更糟。归在 `emit` 之下而不是新开一级命令:`emit` 已经是 「生成描述本工程的文档」。 -### ⭐⭐ 工具有了档位,而且依赖声明的工具现在真的会被装 +### 工具有了档位,而且依赖声明的工具现在真的会被装 包依赖从一开始就有 `[dependencies]` / `[build-dependencies]` / `[dev-dependencies]` 这条轴,工具只有一张表。一个同时点名模拟器与调试探针的板级包,会把两个都装给每一位 @@ -284,13 +372,13 @@ CycloneDX 1.5,覆盖**已记录**的解析。⚠️ 读锁而不是重新解析 | `run` | `mcpp run`、`mcpp test` | 是 | | `dev` | 只有声明它的那个包作为根时 | **否** | -⭐ **不写 `when` 保持今天的行为,所以没有迁移。** +**不写 `when` 保持今天的行为,所以没有迁移。** -⚠️⚠️ **同时:`[xlings.workspace]` 的供给扩到全图。** 在此之前只有根工程的声明会被 +**同时:`[xlings.workspace]` 的供给扩到全图。** 在此之前只有根工程的声明会被 安装,而查找(runner 按裸名找程序)已经跨全图 —— **在没有任何东西安装过的目录里 查找,是只可能失败的查找**。两者现在由同一个表达式定义。 -⚠️ 档位带来的一个危险已被堵上:`mcpp build` 装得比 `mcpp run` 需要的少,而 run 的 +档位带来的一个危险已被堵上:`mcpp build` 装得比 `mcpp run` 需要的少,而 run 的 快路径正是为跳过那一步存在的。构建缓存记下「这次构建留下了未安装的 run 档工具」, `mcpp run` 的快路径据此拒绝该条目 —— 与它拒绝声明了 runner 的条目同理。 @@ -298,7 +386,7 @@ CycloneDX 1.5,覆盖**已记录**的解析。⚠️ 读锁而不是重新解析 并**点名它本来要装的集合**,于是 `build` 与 `run` 两条命令的差集就是被测的性质, 一次下载都不需要(`tests/e2e/335`)。 -### ⭐ ARMv7-A:第一个带内存管理单元的 32 位目标行 +### ARMv7-A:第一个带内存管理单元的 32 位目标行 `armv7a-none-eabi` 与 `armv7a-none-eabihf` 两行,`verified`。表里其余每个 32 位行都是 M-profile:MPU 按基址与上限描述区域,没有页表项。A-profile 有真正的 MMU,于是它是第一个 @@ -308,27 +396,27 @@ M-profile:MPU 按基址与上限描述区域,没有页表项。A-profile 有真 实测 2026-09-04(`xim:qemu-arm@9.2.4-1`):两行都在 `-M virt -cpu cortex-a15` 上启动、 经半主机打印并报回退出状态。 -⚠️ **软浮点行同样需要 `-mfpu=none`,而这是在这个架构上重新实测的**,不是从 M-profile +**软浮点行同样需要 `-mfpu=none`,而这是在这个架构上重新实测的**,不是从 M-profile 推过来的:`armv7a-none-eabi` 在软浮点 ABI 下对一次 float 乘法仍发出 VFP 指令。 -⚠️ **半主机的退出调用与 M-profile 拼法不同。** AArch32 的 `SYS_EXIT`(`0x18`)把原因码 +**半主机的退出调用与 M-profile 拼法不同。** AArch32 的 `SYS_EXIT`(`0x18`)把原因码 **直接**放在 `r1`;Cortex-M 传的 `{reason, code}` 块是 `SYS_EXIT_EXTENDED`(`0x20`)。 实测:把块传给 `0x18` 打印正确而**退出状态是错的**,只看输出的测试看不出来。 -⭐ 那条量化 `-mfpu=none` 的单元测试,谓词曾是 `starts_with("thumb")` —— 一个**拼法** +那条量化 `-mfpu=none` 的单元测试,谓词曾是 `starts_with("thumb")` —— 一个**拼法** 而不是它要陈述的性质。新行加进来时规则适用而测试**静默跳过**了它们,每条断言依然通过。 谓词已改为「32 位 ARM」。 -### ⭐ `mcpp run` 接受 `--features` 与 `--profile` +### `mcpp run` 接受 `--features` 与 `--profile` `build` 与 `test` 一直有这两条轴,`run` 没有 —— 于是 `run` **只能执行上一次 `build` 恰好留下的东西**:没有任何一种写法能跑一个 release 产物,或一个开了 feature 的产物。 -⚠️ 而这正是整个设备面赖以成立的形状:板级包把「模拟器」与「真板」表达成 feature, +而这正是整个设备面赖以成立的形状:板级包把「模拟器」与「真板」表达成 feature, 所以 `mcpp run --features hardware` 才是板子到手那天开发者敲的命令。**方案里唯一一个 自己跑不起来的场景就是它。** -### ⚠️⚠️ 顺带修掉一个既有缺陷:构建缓存不看 feature +### 顺带修掉一个既有缺陷:构建缓存不看 feature 缓存条目按 (target, profile, cache mode) 索引,而**输出目录按含 feature 的指纹索引**。 于是 `mcpp build --features loud` 写下的条目指向 loud 的目录,下一次**不带 feature 的** @@ -359,7 +447,7 @@ Cortex-M 落地为七个目标行,freestanding 链接开启死代码段消除。 target = "thumbv7em-none-eabihf" ``` -⚠️ **浮点 ABI 不决定 FPU 是否被使用。** `eabi`/`eabihf` 由 clang 从 triple 读出, +**浮点 ABI 不决定 FPU 是否被使用。** `eabi`/`eabihf` 由 clang 从 triple 读出, 它约束浮点值如何跨越函数边界,不约束函数内部发什么指令 —— 而 `thumbv7em` 架构 蕴含 FPv4-SP。实测:软浮点 ABI 下 clang 对一次 float 乘法仍发出 `vmul.f32`,在 没有 FPU 的 Cortex-M4 上于运行期触发异常,而编译与链接都是干净的。每个软浮点行 @@ -370,7 +458,7 @@ freestanding 编译加 `-ffunction-sections -fdata-sections`、链接加 `--gc-s 依赖的目标文件无条件进入链接(不像归档成员那样按未定义符号拉取),当 C 库改由 依赖图提供时,没有死代码段消除的镜像会装进整份 C 库,而 Cortex-M 器件只有几十 KB。 -⚠️ **链接脚本因此以新的方式承重**:中断向量表不被任何东西引用,`--gc-sections` +**链接脚本因此以新的方式承重**:中断向量表不被任何东西引用,`--gc-sections` 会回收它,板级脚本必须写 `KEEP(*(.vectors))`。 同时回填了 `docs/13` 中两条已被 2026.8.28.2 推翻的限制:当图中有包提供 @@ -590,7 +678,7 @@ mcpp 打印的建议行随之改成同一形态。 在错误的 C 库配置上。五个层名 `compiler` / `compiler-runtime` / `kernel-abi` / `c-abi` / `c++-abi` 现在是谓词的键,可与三元组键在 `all`/`any`/`not` 下组合。 - ⚠️ 层谓词**不能**选择依赖 —— 层是从依赖图解析出来的 —— 这种段落会被报出并忽略, + 层谓词**不能**选择依赖 —— 层是从依赖图解析出来的 —— 这种段落会被报出并忽略, 而不是被静默丢弃。 - **mcpp 不认识的 cfg 键会被报出来。** 求值器过去对未知键返回假,而那与「这一段本就 @@ -689,12 +777,12 @@ mcpp 打印的建议行随之改成同一形态。 没有 `[workspace.target.]`:根里普通的 `[target.]` 本来就按 triple 被成员继承,为同一能力再加一种拼法只增加接口面。 - ⚠️ **继承作用到每一个成员,包括「作为兄弟成员的 `path` 依赖被编译」的那个** + **继承作用到每一个成员,包括「作为兄弟成员的 `path` 依赖被编译」的那个** —— 也就是成员互相依赖这种最普通的形态。而 `path` 依赖里**不是成员**的那些 (vendored 副本、example)不会获得这些标志:成员资格问的是 workspace 自己的 `members` 列表,不是「这个路径在不在 workspace 目录下」。 - ⚠️ `[workspace.build] include_dirs` 里的相对路径按 **workspace 根**锚定(#224): + `[workspace.build] include_dirs` 里的相对路径按 **workspace 根**锚定(#224): 它是在根 manifest 里写的,按各成员自己的目录解析会指向不存在的地方。 - **依赖声明了高于当前图的标准时会说出来。** C++ 模块图只有一个标准,依赖自己的 @@ -790,7 +878,7 @@ mcpp 打印的建议行随之改成同一形态。 而消费者漂了。子系统测试还跑在根构建从不产生的配置里——**只有它自己和它声明的 依赖**,所以一个悄悄依赖了未声明之物的子系统在根构建里能编过、在这里编不过。 - ⚠️ `mcpp test -p ` 对没有测试的成员**退 0**,于是「没有测试」与 + `mcpp test -p ` 对没有测试的成员**退 0**,于是「没有测试」与 「测试通过」在 CI 输出里完全一样。`check_modules_wiring.sh` 因此把没有测试的成员 逐个打印出来(不判失败——一个 vendored 解析器包确实没有自己的东西要陈述)。 @@ -804,7 +892,7 @@ mcpp 打印的建议行随之改成同一形态。 不进用法):它在 mcpp 能跑的每个平台上都已经在磁盘上。已经自己写 stamp 的命令 逐字节不受影响——已存在的文件不会被动。 - ⚠️ **显而易见的判据不区分**:实测 ninja **不会**因为声明的输出没生成而失败, + **显而易见的判据不区分**:实测 ninja **不会**因为声明的输出没生成而失败, 它只是留着文件不存在、之后**每次构建都重跑**那条边。构建保持绿色,唯一的症状是 活被反复重做。`tests/e2e/312` 因此断言 stamp 存在 + 无变更时不重跑,两条都先看它红。 @@ -859,22 +947,22 @@ mcpp 打印的建议行随之改成同一形态。 出现两种形态,正是本条要抓的缺陷。边上的 `linkage` 只在**根工程**生效: 依赖图深处的包无权决定最终程序的布局。 - ⚠️ **这不是 `[target.].linkage`**(那根是 C 库轴),而且两者**不独立**: + **这不是 `[target.].linkage`**(那根是 C 库轴),而且两者**不独立**: 整链静态的映像没有解释器,装不下任何共享对象。C 库静态链接的目标 —— **musl 的默认** —— 会拒绝 `shared` 并说明原因。 - **符号提供者检查。** 链接后核验映像里每个符号恰好有一个提供者。 - ⚠️ ELF 上可执行文件排在最前,被静态并进程序的库会在共享副本之上获胜: + ELF 上可执行文件排在最前,被静态并进程序的库会在共享副本之上获胜: 共享的那份永远不会被调用,那个库里的代码跑在一份它并非针对其链接的构建上。 链接器与加载器都不报任何一句话。实测 `/usr/bin/git`(自己的 `error` 遮蔽 glibc 的 `error(3)`)与 `/usr/bin/ls`(gnulib obstack 遮蔽 glibc 的)都是 这个形状的野生实例。 - ⭐ 判据是**测量**不是声明,因此对 vendor 包里随附的库、`[system_deps]` + 判据是**测量**不是声明,因此对 vendor 包里随附的库、`[system_deps]` 引入的宿主库同样有效 —— 引擎不需要认识任何具体的库。 - ⭐⭐ **两段式,而第二段不可省。** mcpp 自己的 `kind = "shared"` 机制会 + **两段式,而第二段不可省。** mcpp 自己的 `kind = "shared"` 机制会 **结构性地**产出「exe 导出、`.so` 绑过来」这个形状,而那是单份定义、 完全良性的。只看第一段会在正确的构建上刷警告,而用户对此无事可做。 真正的判据是「导出的东西**还有第二个提供者**」。 @@ -884,17 +972,17 @@ mcpp 打印的建议行随之改成同一形态。 ### 修复 -- **⚠️ 依赖包的 `[targets.*] required_features` 从来没有生效过。** +- **依赖包的 `[targets.*] required_features` 从来没有生效过。** 目标门控只有一处,判据是**根**的活跃 feature 集,依赖包的 `targets` 一个都不过滤。一个描述符写下 `required_features`,得到的是它要求的**反面**: 该目标对每一个消费者都构建,不管 feature 开没开。 - ⭐ 对 `kind = "shared"` 的目标这不是外观问题:包里只要存在任何一个 shared + 对 `kind = "shared"` 的目标这不是外观问题:包里只要存在任何一个 shared 目标,它的**全部**对象就会从每个消费者的链接里被拿走。一个「可选」的目标 因此悄悄改变了整个包对所有人的链接方式。 -- **⚠️ `-fPIC` 不在缓存键里。** +- **`-fPIC` 不在缓存键里。** 它是全图的(图里任何一个 shared 链接单元存在,所有对象都带 PIC),而键取的是 包**声明**的 flags,不是 flag 构造器算出来的。在形态由作者定死时可以幸存; @@ -904,7 +992,7 @@ mcpp 打印的建议行随之改成同一形态。 `dependency_linkage` 同样进了工程指纹 —— 否则切换开关会复用上一次配置的 构建目录(实测:两次构建落在同一个 `target/x86_64-linux-gnu//`)。 -- **⚠️⚠️ `mcpp pack` 不收合成的共享库,包解开就起不来。** +- **`mcpp pack` 不收合成的共享库,包解开就起不来。** ```console $ ./app @@ -915,17 +1003,17 @@ mcpp 打印的建议行随之改成同一形态。 是空的,`$ORIGIN` 解析不到,库于是从不出现在闭包里,也就从不被打包。 构建、打包、上传全程无话,失败发生在用户机器上。 - ⚠️ **不是本版引入的**:在 2026.8.26.1 上用作者声明的 `kind = "shared"` + **不是本版引入的**:在 2026.8.26.1 上用作者声明的 `kind = "shared"` 依赖同样复现。但 `dependency_linkage` 把它从「12 个自称 shared 的包」 变成「任何一个包」都可达,所以在这里修。 -- **⚠️ 在非 shared 目标上写 `soname` 会让整份 manifest 加载失败。** +- **在非 shared 目标上写 `soname` 会让整份 manifest 加载失败。** `soname` 是一个库被**找到**时用的名字,也是 mcpp 构建的那份与第三方携带的 同一个库能解析到同一个文件的唯一途径。收窄为「非 **library** 目标才拒绝」, 可执行文件仍然不允许声明。 - ⚠️ 因此把 `soname` 写进索引描述符要等 `latest` 的 mcpp 下限跨过本版本 —— + 因此把 `soname` 写进索引描述符要等 `latest` 的 mcpp 下限跨过本版本 —— 旧客户端读到的是加载失败,不是忽略。 ### 文档 @@ -938,12 +1026,12 @@ mcpp 打印的建议行随之改成同一形态。 一个文件名把整个 Windows 构建打断了,而报错说的是别的事。完整分析见 [`.agents/docs/2026-08-27-issue516-windows-acp-glob-walk-fix.md`](.agents/docs/2026-08-27-issue516-windows-acp-glob-walk-fix.md)。 -⭐ **这是 `#230` 的同一处漏网,不是新缺陷。** `#231` 加固了三个窄化站点, +**这是 `#230` 的同一处漏网,不是新缺陷。** `#231` 加固了三个窄化站点, 漏掉了同一个 walk 循环里**早一行**执行的第四处。 ### 修复 -- **⭐⭐ 一个当前代码页拼不出的目录名,会让 `mcpp` 在 Windows 上以内部错误退出。**(#516) +- **一个当前代码页拼不出的目录名,会让 `mcpp` 在 Windows 上以内部错误退出。**(#516) `src/modgraph/scanner.cppm` 的 `is_excluded_walk_dir()` 用 `dir.filename().string()` 取目录名。MSVC 的 `path::string()` 走 @@ -1006,12 +1094,12 @@ mcpp 打印的建议行随之改成同一形态。 目标侧被解析出来了,只发给了一个编译单元。完整分析见 [`.agents/docs/2026-08-27-openkal-native-path-three-issues.md`](.agents/docs/2026-08-27-openkal-native-path-three-issues.md)。 -⭐ **与 `2026.8.25.x`/`2026.8.26.1` 是同一族的下一层。** 那两批修的是「谓词问错了」 +**与 `2026.8.25.x`/`2026.8.26.1` 是同一族的下一层。** 那两批修的是「谓词问错了」 与「答案没接到决定上」;这一批里,答案**接上了一个消费者,而它有五个**。 ### 修复 -- **⭐⭐ 编译侧的谓词,是 `2026.8.26.1` 在链接侧修掉的那条的孪生兄弟。** +- **编译侧的谓词,是 `2026.8.26.1` 在链接侧修掉的那条的孪生兄弟。** `hostflags.cppm` 问的是 `!crossTargetFlag.empty()` ——「命令行上有没有 `--target=`」——而它的注释写的是「目标侧来自图」。同一台机器、同一个编译器、 @@ -1026,14 +1114,14 @@ mcpp 打印的建议行随之改成同一形态。 ``` ⇒ 头文件来自一个库,目标文件链自另一个库。两侧现在读同一个 - `plan.targetSide.cAbi.prebuilt()`。⚠️ `--no-default-config` 从这个条件里 + `plan.targetSide.cAbi.prebuilt()`。`--no-default-config` 从这个条件里 **拆了出来无条件发** —— 它不是载荷头文件集合的一部分,而 cfg 文件按 `post_install.cppm` 自己的说法是「per-machine, per-install-path artifact」。 - ⚠️ `e2e 295` 写的就是这条恒等式,而它只比对 `^ldflags`,所以恒等式在**下一行** + `e2e 295` 写的就是这条恒等式,而它只比对 `^ldflags`,所以恒等式在**下一行** 不成立而测试看不见。现在两条都比。 -- **⭐⭐ 载荷目录名是 LLVM 词汇,而查找用的是 mcpp 词汇 —— 而且失配是静默的。** +- **载荷目录名是 LLVM 词汇,而查找用的是 mcpp 词汇 —— 而且失配是静默的。** `include//c++/v1` 与 `lib/` 由 LLVM 的构建写下,带的是 `x86_64-unknown-linux-gnu`;`tc.targetTriple` 是 mcpp 的 @@ -1041,7 +1129,7 @@ mcpp 打印的建议行随之改成同一形态。 `if (exists) push_back`,所以找不到就什么也不发生 —— 而那个目录里只有一个文件, `__config_site`,它的缺席产生的报错读起来像载荷坏了。两种拼法现在都试。 -- **⭐⭐ 由图供给的目标侧,只到达了一个编译单元(mcpp#514 §A)。** +- **由图供给的目标侧,只到达了一个编译单元(mcpp#514 §A)。** 提供 `mcpp:` 层的包发布的是**整个目标**编译时所依据的头文件集合,而它今天以 `publicUsage` 的形态**沿依赖边**传播。于是根与 provider 自己的单元拿得到,而 @@ -1049,23 +1137,23 @@ mcpp 打印的建议行随之改成同一形态。 它在它旁边。结果是一次构建里两种口味的 BMI,任何同时导入两者的 TU 在第一个 模板实例化处炸掉(`reference to 'space' is ambiguous`)。 - ⭐ 目标侧解析之后,`fromGraph()` 的层的 `publicUsage` 并入**每一个**包的 + 目标侧解析之后,`fromGraph()` 的层的 `publicUsage` 并入**每一个**包的 `privateBuild`;`std` 模块的命令行也改读同一个集合,不再自己推一遍。 -- **⭐⭐ 缓存键描述了编译器,没有描述它被指向的头文件集合(mcpp#514 §B)。** +- **缓存键描述了编译器,没有描述它被指向的头文件集合(mcpp#514 §B)。** A 轴上的每一项都在描述**编译器**,没有一项描述它编译时所依据的**库** —— 而两者是分开安装的。`driverIdentity` 按设计也覆盖不了它: `normalize_driver_output` **故意**抹掉路径,好让一个条目能被两个 home 共享。 新增 `targetHeaderSet` 轴,取自已经解析好的 `linkmodel`,并**分两档相对化**: - `` 与 ``。⚠️ 只做 `` 一档不够 —— 最常见的那台机器走 + `` 与 ``。只做 `` 一档不够 —— 最常见的那台机器走 `CLibMode::Sysroot`,它唯一的编译 token 是 `--sysroot=/registry/subos/default`, 在 HOME 底下而不在 store 底下,于是每个条目都会带上这台机器的 home。分得开什么: 两个载荷(路径里带版本号)⇒ 两个键;一个 home 下的两个 subos ⇒ 两个键; 两个 home 下同名的 subos ⇒ 仍是一个键(由整工程指纹的第 11 项区分)。 - ⚠️ 不 bump `kCacheEpoch` —— 旧条目是 miss 而不是不可用。 + 不 bump `kCacheEpoch` —— 旧条目是 miss 而不是不可用。 -- **⭐⭐ 请求的版本和载荷目录的版本是两套词汇,而每个查找都按请求那套拼。** +- **请求的版本和载荷目录的版本是两套词汇,而每个查找都按请求那套拼。** RuntimeBinding 带的是**声明的**版本(`glibc@2.44`),而 xlings 把载荷目录按这个 请求**解析成**的版本命名(`2.44.2`)。索引在同一序列内挪动一次包,所有按声明版本 @@ -1075,12 +1163,12 @@ mcpp 打印的建议行随之改成同一形态。 glibc 的 include 目录只是没被加上,用户读到的是 libstdc++ 头文件里的 `features.h: No such file`。⇒ 收敛到一个 `payload_dir_for_version`。 - ⭐ **判据是「精化」,不是「按目录序挑一个」**:`2.44.2` 的版本**分量**以请求的分量 + **判据是「精化」,不是「按目录序挑一个」**:`2.44.2` 的版本**分量**以请求的分量 开头。`2.4` 回答不了 `2.44`(逐分量比,不是逐字符)。两个载荷都精化同一个请求时 返回**空** —— 「这个请求的解析结果」得是唯一一个才配叫答案,而按目录序挑正是这里 每个调用者都拒绝做的猜测。反方向(拿更旧的载荷回答更新的请求)不接受。 -- **⭐ home 发现有第四份拷贝,而且会伸到别的 home 里去。** +- **home 发现有第四份拷贝,而且会伸到别的 home 里去。** `active_home_xpkgs()` 自己重推了一遍 home(漏掉自包含安装那一档); `find_sibling_package` 找不到时**无条件回落** `~/.xlings/data/xpkgs`。 @@ -1092,7 +1180,7 @@ mcpp 打印的建议行随之改成同一形态。 - **`[build] private_include_dirs`** —— 指出 `include_dirs` 中在本包边界处停住的 条目。`publicUsage` 此前整份接过 `privateBuild` 的目录,于是一个内嵌了带内部头 覆盖层的库(musl 的 `src/include` 定义 `hidden`/`weak`/`weak_alias`)会把那些宏 - 发给每一个消费者。⚠️ 它是 `include_dirs` 的**子集**而不是第二个列表:两类目录的 + 发给每一个消费者。它是 `include_dirs` 的**子集**而不是第二个列表:两类目录的 相对顺序是承重的,而两个 TOML 数组表达不了一个顺序。 ## [2026.8.26.2] — 2026-08-26 @@ -1100,14 +1188,14 @@ mcpp 打印的建议行随之改成同一形态。 已经解析出的答案,没有被用来做决定。完整分析见 [`.agents/docs/2026-08-26-resolved-but-not-consulted.md`](.agents/docs/2026-08-26-resolved-but-not-consulted.md)。 -⭐ **这不是 2026.8.25.x 那个「谓词回答了比自己更窄的问题」的家族。** 那一族是判据 +**这不是 2026.8.25.x 那个「谓词回答了比自己更窄的问题」的家族。** 那一族是判据 问错了;这一族里谓词问对了、答案也算对了,只是那个答案**没有接到决定上**。两条都 是「多存了一个字段而没有多接一根线」,因此读判据时看不出来 —— 只在用户问「你既然 已经知道了,为什么还要我说一遍」时暴露。 ### 修复 -- **⭐⭐ 依赖声明的编译器被检查,但从未被采纳。** +- **依赖声明的编译器被检查,但从未被采纳。** ``` $ cat mcpp.toml @@ -1127,7 +1215,7 @@ mcpp 打印的建议行随之改成同一形态。 the graph exists」的接缝,它扫 `pkg.manifest.provides` 来决定编译器,**却不扫 `requires_`** —— 后者在一千行之后才被收集,只用来否决这个决定。 - ⭐⭐ **修好之后不写任何东西,而这是位置带来的,不是额外加的开关。** + **修好之后不写任何东西,而这是位置带来的,不是额外加的开关。** `resolve_target_toolchain` 只有两个调用点,整个函数体(含首次运行的 安装并持久化分支与全部三处 `write_default_toolchain`)都在图之后。把图的要求 写进 `tcSpec` 的时机早于首次运行分支被求值,于是: @@ -1137,16 +1225,16 @@ mcpp 打印的建议行随之改成同一形态。 | 已有 gcc 默认的机器 | 拒绝,要求改全局默认 | 装/用 llvm,`config.toml` 不动 | | 什么都没装的机器 | 装 gcc → 持久化 gcc → 再拒绝 | 首次运行分支根本不进,直接装 llvm | - ⭐ 状态行点名是哪个包要求的、顶掉了什么;`why toolchain --format json` 新增 + 状态行点名是哪个包要求的、顶掉了什么;`why toolchain --format json` 新增 `compiler.chosenBy = {origin, requiredBy, replaced}`,让「为什么是 llvm」不必去 解析那行提示 —— 那正是机器接口存在的理由所要消除的字符串匹配。 拒绝只剩一种局面:工程自己在 `[toolchain]` 或 `[target.X]` 写下了相反的编译器。 - ⚠️ 那种局面里全局默认与本次构建无关,因此原来那条 `mcpp toolchain default llvm` + 那种局面里全局默认与本次构建无关,因此原来那条 `mcpp toolchain default llvm` 的建议**连问题都解决不了**,已改为指向那条陈述本身。两个包要求不同的族则是错误 而不是一次挑选,并同时点名两个包。 -- **⭐⭐ tier 闸问的是补全后的身份,而不是请求。** +- **tier 闸问的是补全后的身份,而不是请求。** ``` $ mcpp build --target aarch64-linux @@ -1161,10 +1249,10 @@ mcpp 打印的建议行随之改成同一形态。 从没打过的字符串。 省略了 env 段的请求现在对着词表补全,规则 1(词法默认受支持就用它)排在最前, - 因此 `x86_64-linux` 一动不动,而这件事能自己退休。⚠️ **`parse()` 未改**:身份 + 因此 `x86_64-linux` 一动不动,而这件事能自己退休。**`parse()` 未改**:身份 必须保持词法、全量、与宿主无关。 -- **⚠️ `unknown target 'riscv64-linux'` 说的是假话。** +- **`unknown target 'riscv64-linux'` 说的是假话。** `riscv64-linux-musl` 就在词表里(`planned`)。词法填充产生了一个**完全不存在** 的行,于是一个已登记的目标族被报成未知。而且这条路径没有 `refusal::record`, @@ -1172,28 +1260,28 @@ mcpp 打印的建议行随之改成同一形态。 `riscv64-linux-musl`;真正的拼写错误仍报 unknown,但带上了新的 `unknown-target` 记号。 -- **⚠️ `why toolchain --format json` 的两个字段互相矛盾。** +- **`why toolchain --format json` 的两个字段互相矛盾。** `cLibrary` 说 glibc/payload,`layers[].c-abi` 说 musl/graph。产物给出裁决 —— 静态、无解释器、无 `DT_NEEDED`、11 个 openkal 符号 —— glibc 不在里面。两者各自 准确,回答的却是不同的问题,而消费方无从判断该信哪个。新增 - `cLibrary.suppliesTarget`;⭐ 是**增字段**而非改名或给 `mode` 加取值,因为 + `cLibrary.suppliesTarget`;是**增字段**而非改名或给 `mode` 加取值,因为 docs/11 §6 承诺字段只增不删、含义永不改变。 -- **⚠️⚠️ 而「不写任何东西」需要一个有名字的规则,不只是一个位置。** +- **而「不写任何东西」需要一个有名字的规则,不只是一个位置。** `write_default_toolchain` 有三个调用点。首次运行那个的条件是 `!tcSpec.has_value()`,方案确实让它进不去;另外两个 —— Windows 首次运行改道、 MSVC 不可用时的修复 —— 条件不是它,**都可达**。一台没装工具链的 Windows 机器 构建一个要求 llvm 的工程,会把 llvm 写成这台机器的默认值。 - ⭐ 修法是给规则一个名字:`tc_origin_may_persist(TcOrigin)`,两处都调用,一条单 - 测陈述它。⚠️ 这条缺陷是**读出来的**:它需要一台没有工具链的 Windows 机器,而 + 修法是给规则一个名字:`tc_origin_may_persist(TcOrigin)`,两处都调用,一条单 + 测陈述它。这条缺陷是**读出来的**:它需要一台没有工具链的 Windows 机器,而 `config.toml` 的 sha256 判据跑在已配好的环境里,两条分支一条都到不了。 ### 示例 -- **⭐ `examples/06-openkal-cross` 现在是四个目标,而且不再写 `[toolchain]`。** +- **`examples/06-openkal-cross` 现在是四个目标,而且不再写 `[toolchain]`。** 第四行 `--target aarch64-linux` 正是这次修好的那一条 —— 在此之前它补全成 `aarch64-linux-gnu`(registered but not supported)并拒绝。同时删掉了那个 @@ -1201,26 +1289,26 @@ mcpp 打印的建议行随之改成同一形态。 `requires = ["mcpp:compiler=llvm"]`,mcpp 现在读它。实测在一台全局默认为 gcc 的 机器上,四个目标全部解析出 `llvm@22.1.8`,而 `~/.mcpp/config.toml` 一字未改。 - ⚠️ 顺带修掉一处失效的钉:该示例的依赖下界还写着 `0.1.1`,而那个版本的 + 顺带修掉一处失效的钉:该示例的依赖下界还写着 `0.1.1`,而那个版本的 compiler-rt builtins 在 aarch64 上编不过。这个示例**不被任何 CI 构建**,所以它 钉住的版本过期了也没有任何东西会说话。 ### CI -- **⚠️⚠️ 目标矩阵的编译器轴跟着「装了什么」走,于是缓存能决定判据。** +- **目标矩阵的编译器轴跟着「装了什么」走,于是缓存能决定判据。** 实测:同一个提交,PR 的 `scan (windows-x86_64)` 绿,合入 main 后同一个 job 红 —— 那次 runner 恢复出来的缓存里多了一个 `gcc@16.1.0`,扫描产出 24 格而期望表为这台 宿主声明的是 16 格,八格全部报成「表里没有这一格」。 - ⭐ **期望表是一份声明**,编译器轴现在跟着它走(`MATRIX_COMPILERS`,由 workflow + **期望表是一份声明**,编译器轴现在跟着它走(`MATRIX_COMPILERS`,由 workflow 从 expected.tsv 的同一列算出,与「装哪些」那一步同源,所以两者不可能各说各话)。 装着却不在声明里的版本写到 stderr —— 一次没跑的测量和一次通过的测量,在退出码上 没有区别。 ### 兼容性 -⭐ **没有任何一次原本成功的构建换了行为。** 图声明编译器的情形里,原来的结局是 +**没有任何一次原本成功的构建换了行为。** 图声明编译器的情形里,原来的结局是 `check_requirements` 拒绝 —— 也就是说那些构建本来就不成功;`--target aarch64-linux` 与 `riscv64-linux` 原来是拒绝;`x86_64-linux` / `x86_64-windows` / `riscv64-none` / `aarch64-macos` 的补全结果一字未变(单测逐行遍历整张词表守住这一条)。机器接口只 @@ -1230,9 +1318,9 @@ mcpp 打印的建议行随之改成同一形态。 - e2e `299`–`303`,全部走 `--format json` 分类而非字符串搜索,并接入 `target matrix` 的第一层 —— **四台构建机各跑一遍**。 -- ⭐ `301` 的判据是 `~/.mcpp/config.toml` 的 **sha256**,不是「构建成功」:构建成功 +- `301` 的判据是 `~/.mcpp/config.toml` 的 **sha256**,不是「构建成功」:构建成功 与配置被改写可以同时为真,而那正是这次要消除的行为。 -- ⭐ `299` 的第二半是对照 —— `x86_64-linux` 必须仍是 gnu。只测 aarch64 会让「把 +- `299` 的第二半是对照 —— `x86_64-linux` 必须仍是 gnu。只测 aarch64 会让「把 linux 的默认整个换成 musl」这种过头实现看起来是对的。 - 单测 `TripleRequest.*` 七条,含一条遍历整张词表的「每个受支持的行都能从它自己的 拼写到达」。 @@ -1245,7 +1333,7 @@ mcpp 打印的建议行随之改成同一形态。 ### 修复 -- **⭐⭐ 命名宿主自己的目标,曾让构建失败。** +- **命名宿主自己的目标,曾让构建失败。** 同一台机器、同一个编译器、**同一个目标**,只差写不写 `--target`: @@ -1263,17 +1351,17 @@ mcpp 打印的建议行随之改成同一形态。 后者;而 `crossTarget` 只是 `--target=<三元组>` 这个字符串,任何命名目标都非空 ——包括命名宿主目标、且完全不依赖任何包的工程。 - ⚠️ **同一个错误的问题被问了三遍**,分散在三处:`link_toolchain_flags`、 + **同一个错误的问题被问了三遍**,分散在三处:`link_toolchain_flags`、 `payload_ld`、`atomic_ld`。而其中一处的注释只预告了**两**条通道: > the C-runtime group reaches the link line through TWO channels, and a reader > who fixed one saw the identical error and could reasonably conclude the fix > had not worked. - ⭐ 三条全部改问 `targetSide.cAbi.prebuilt()` —— 与 `2026.8.25.1` + 三条全部改问 `targetSide.cAbi.prebuilt()` —— 与 `2026.8.25.1` 把另外三处决定迁过去的**同一个谓词**。这是该族的第六至第八条。 -- **⭐ 目标行声明的 sysroot 从不被安装(#510)。** +- **目标行声明的 sysroot 从不被安装(#510)。** 一行目标表声明两样东西,只有一样被兑现:`pin` 走 `resolve_xpkg_path(…, autoInstall=true, …)`,`sysroot` 是纯查询,查不到就静默 @@ -1285,13 +1373,13 @@ mcpp 打印的建议行随之改成同一形态。 error: 'stdio.h' file not found ``` - 报告点名了这个目标的 C 库,而构建找不到它的头。⚠️ mcpp 自己的裸机 CI **手工装 + 报告点名了这个目标的 C 库,而构建找不到它的头。mcpp 自己的裸机 CI **手工装 它**并在注释里说明了原因,于是每一条裸机 e2e 都跑在缺陷已被抹平的机器上。 改为走同一个 `autoInstall` 通道;离线与 `MCPP_NO_AUTO_INSTALL` 由 `Fetcher` 判定,不在此处再问一遍。 -- **⭐ 裸机行的 pin 是能力陈述,不是偏好。** +- **裸机行的 pin 是能力陈述,不是偏好。** ``` [toolchain] default = "gcc@16.1.0" @@ -1303,11 +1391,11 @@ mcpp 打印的建议行随之改成同一形态。 目标的 C 库」,作者自带编译器时理应让位;裸机行说的是「哪个编译器能发出这个 目标」——宿主 g++ 发不出 riscv64,谁声明都不行。现在在决定处拒绝,并指出出路。 - ⚠️ **约定仍然可以被推翻**:hosted 目标上显式声明 gcc 照常生效。 + **约定仍然可以被推翻**:hosted 目标上显式声明 gcc 照常生效。 ### 目标矩阵在四台构建机上找到的 -⭐⭐ **116 格,0 个 `mismatch`。** 四台各自的实测写在 +**116 格,0 个 `mismatch`。** 四台各自的实测写在 [`tests/matrix/expected.tsv`](tests/matrix/expected.tsv),每一行都来自它自己那台 机器 —— 从别的宿主推断出来的一行,断言的是推断而不是那台机器。 @@ -1330,18 +1418,18 @@ mcpp 打印的建议行随之改成同一形态。 | unsupported | lld-required-absent | 1 | | unsupported | other | 1 | -⚠️ 最后那一格是**诚实的** `other`:`std module precompile failed` 由 +最后那一格是**诚实的** `other`:`std module precompile failed` 由 `stdmod.cppm` 发出,那里够不到拒绝记号的沉淀点。它是**构建失败经拒绝通道浮出**, 不是一条规则 —— 把它硬塞进邻近的理由才是错的。 -⭐⭐ **把 `linux-aarch64` 加进构建机轴之后,四台各自交出了一台机器上看不见的缺陷。** +**把 `linux-aarch64` 加进构建机轴之后,四台各自交出了一台机器上看不见的缺陷。** 轴取自 `release.yml` 发布的那一组(linux-x86_64 / linux-aarch64 / macos-arm64 / windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二进制却从没被扫过的 宿主,它的目标表是一句没人核过的话。 -- **⚠️ `--rtlib=compiler-rt` 在 aarch64 上是 codegen 事实,而它被声明在只覆盖 +- **`--rtlib=compiler-rt` 在 aarch64 上是 codegen 事实,而它被声明在只覆盖 一侧的键上。** ``` @@ -1350,12 +1438,12 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ``` `openkal-llvm-runtime` 把它写在 `std-module-flags` 里 —— 那个键到达 `std.pcm` - 的命令,不到达任何消费者的 TU。⭐ **与 `-fdwarf-exceptions` 是同一个缺陷,换了 + 的命令,不到达任何消费者的 TU。**与 `-fdwarf-exceptions` 是同一个缺陷,换了 一个 flag**;那次的解法是提升成图级的 `graph_runtime_compile_flags`,这次同样。 - ⚠️ x86_64 上两侧都列空,所以直到第二个架构被构建才可见。 + x86_64 上两侧都列空,所以直到第二个架构被构建才可见。 -- **⚠️ PE + musl 在任何宿主上都没有载荷,而 `host_can_serve` 在 Windows 上说有。** +- **PE + musl 在任何宿主上都没有载荷,而 `host_can_serve` 在 Windows 上说有。** ``` c-abi musl(payload) @@ -1367,25 +1455,25 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `triple::pin_is_capability()` 与 docs/16 都已写明这一行只能由依赖图供给; Linux 上同一格早就答 `host-cannot-serve`。 -- **⚠️ 宿主服务不了的目标,仍然去装它的载荷。** 拒绝被决定在早、释放在晚(因为 +- **宿主服务不了的目标,仍然去装它的载荷。** 拒绝被决定在早、释放在晚(因为 图供不供给系统只有解析后才知道),而安装夹在中间,于是先失败且失败得更硬: `xlings install of 'xim:x86_64-linux-musl-gcc@16.1.0' failed`。跳过安装让两条 后续路径都完好;尝试安装帮不了其中任何一条。 -- **⚠️ aarch64 Linux 只支持 `musl-gcc`,其余显式延缓。** 上游 LLVM 从 20.x 起停发 +- **aarch64 Linux 只支持 `musl-gcc`,其余显式延缓。** 上游 LLVM 从 20.x 起停发 `linux-aarch64`,索引里也没有,所以 `available_toolchain_indexes()` 在非 x86_64 - Linux 上不再列 `llvm` 与 `mingw-cross-gcc`。⭐ 这是**政策陈述**不是索引数据的 + Linux 上不再列 `llvm` 与 `mingw-cross-gcc`。这是**政策陈述**不是索引数据的 抄本,并且 `check_aarch64_llvm_deferral.sh` **在理由不再成立时变红** —— 没人复查的延缓与缺陷无法区分。计划见 [`.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md`](.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md)。 - 交叉 musl 与 mingw 的载荷都按宿主架构发布,`host_can_serve` 对两者也改为按架构 - 回答。⚠️ 「自足」讲的是载荷装了什么,不是它为哪些宿主发布 —— 那行注释是对的, + 回答。「自足」讲的是载荷装了什么,不是它为哪些宿主发布 —— 那行注释是对的, 代码把它读错了一层。 ### 机器接口 -- **⭐⭐ 两条命令进入 `--format json`,矩阵与四条 e2e 不再匹配任何一句话。** +- **两条命令进入 `--format json`,矩阵与四条 e2e 不再匹配任何一句话。** ``` mcpp toolchain list --format json → mcpp.toolchain.list @@ -1398,29 +1486,29 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `tier-planned` / `host-cannot-serve` / `os-mismatch` / `layer-requirement` / `layer-ordering`,由 `mcpp.build.refusal` 在每一处拒绝的 `return` 之前记下。 - ⚠️ **代价是当场量到的**:本次会话里我把 `cannot emit it` 改成 + **代价是当场量到的**:本次会话里我把 `cannot emit it` 改成 `cannot be emitted by`,e2e 297 的断言随即变成空转——它仍然「通过」,只是不再 匹配任何东西。消息**仍然是承诺**(点名目标、规则与出路,e2e 照旧断言这一点); 换掉的是**分类**:一个答案集有限的问题,不该用子串搜索来问。 - ⚠️ 而**查询不能取代构建**。`llvm × x86_64-windows-gnu` 解析得完全正常,失败 + 而**查询不能取代构建**。`llvm × x86_64-windows-gnu` 解析得完全正常,失败 在链接期的封闭性检查上——只查不建会把它报成绿。矩阵两样都做:分类取自 `reason`,结论取自构建的退出码。 - ⚠️ `why toolchain` 声明的 effects 故意偏宽(`network` / `write-global-cache` / + `why toolchain` 声明的 effects 故意偏宽(`network` / `write-global-cache` / `exec-build-script`):它的答案来自与构建同一次的解析。客户端是在运行**之前** 读这张表的,漏报一项就是一句不成立的安全承诺。 -- **⭐ `refused` 曾与 `none` 同读数。** 一处没有记号的拒绝分支让矩阵写下 +- **`refused` 曾与 `none` 同读数。** 一处没有记号的拒绝分支让矩阵写下 `unsupported / none`——「拒绝了」和「没有理由」共用一个词,正是本次发布在修的 那个形状,重现在为发现它而造的机器里。现在无记号的拒绝报 `other`:一句可见的 承认,而不是并进邻近的理由。 -- **⭐⭐ 新增 `ci-target-matrix.yml`,三个宿主 × 两层。** 第一层跑上述恒等式, +- **新增 `ci-target-matrix.yml`,三个宿主 × 两层。** 第一层跑上述恒等式, 不需要期望表;第二层用 `tests/matrix/scan.sh` 全表扫描 × 两种体系,与仓库里的 `tests/matrix/expected.tsv` 比对。 - ⚠️ **「跳过」必须是期望表说的,不是运行时发现的**:一格因为「今天这台机器没装 + **「跳过」必须是期望表说的,不是运行时发现的**:一格因为「今天这台机器没装 某载荷」而跳过,与「这个组合本就不支持」是两回事,前者会让矩阵在缺件机器上悄悄 变绿。比对脚本还先断言**扫描真的跑了**——一格没跑与全部通过,在退出码上没有 区别。 @@ -1428,11 +1516,11 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 测试 -- **⭐⭐ e2e 295 是一条恒等式,不是一个阈值。** `mcpp build` 与 +- **e2e 295 是一条恒等式,不是一个阈值。** `mcpp build` 与 `mcpp build --target <宿主自己的目标>` 描述同一次构建,链接线必须逐 flag 相同。 它不需要期望表、不取决于机器上装了什么,任何宿主都成立。 - ⭐ **它把「修了一半」直接指出来**:差异 7 项 → 5 项 → 3 项 → 0,每一步指向下一 + **它把「修了一半」直接指出来**:差异 7 项 → 5 项 → 3 项 → 0,每一步指向下一 条通道。没有它,修完两条会看到「还是红」,而那句注释会让人以为已经找全。 - e2e 296:报告说 `c-abi (payload)` ⇒ 链接线必须含该载荷;说 `(graph)` ⇒ 不得含 @@ -1448,7 +1536,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 修复 -- **⭐⭐ 一个包声明「我供给哪一层」,让裸机目标丢掉了唯一能产出它的编译器。** +- **一个包声明「我供给哪一层」,让裸机目标丢掉了唯一能产出它的编译器。** 实测,三行清单就够: @@ -1473,11 +1561,11 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 与 2026.8.25.1 修的四条同型:**一个跨两层的谓词,决定了一件不取决于这两层的事**。 这是第五条。 - ⚠️ 发现方式:2026.8.25.1 发布后重钉七个下游 pin PR,openkal-opensbi 红在 + 发现方式:2026.8.25.1 发布后重钉七个下游 pin PR,openkal-opensbi 红在 `g++: unrecognized`。它在 2026.8.24.6 那轮红在**同一条**,所以既非 25.1 引入, 也非 25.1 修掉——是同一跨度里的遗留。 -- **⭐⭐ 图供给了 C 库,不等于目标平台的 SDK 不再需要。** +- **图供给了 C 库,不等于目标平台的 SDK 不再需要。** ``` kernel-abi openkal (openkal-macos@0.3.4, graph) @@ -1490,12 +1578,12 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 会死在 library not found for -lSystem」);而 80 行之后的图分支把整条 `f.ld` 换掉,`-isysroot` 随之消失——**注释预言的那个失败,由它下面的代码造成**。 - ⭐ **Linux 上二者恰好重合而 Darwin 上不重合**:Linux 的内核接口是一条指令 + **Linux 上二者恰好重合而 Darwin 上不重合**:Linux 的内核接口是一条指令 (`syscall`),所以自足的 musl 真的替换了一切;Darwin 的内核接口**本身就是一个 库**(libSystem),所以 Mach-O 链接无论 libc 从哪来都要 SDK。新增 `platformAnchor`:写一次、读一次,两个分支不可能对「什么该活下来」有分歧。 -- **⭐⭐ 请求的目标与解析出的目标必须是同一个操作系统。** +- **请求的目标与解析出的目标必须是同一个操作系统。** ``` Target x86_64-windows-gnu → x86_64-unknown-linux-gnu @@ -1508,11 +1596,11 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 不是做出这个决定的那一处。openkal-uefi 撞在链接器上: `ld: unrecognized option '--subsystem'`。 - ⭐ **报告里早就有证据,现在对它下断言**,而不是把问题重新推导一遍。范围刻意 + **报告里早就有证据,现在对它下断言**,而不是把问题重新推导一遍。范围刻意 只取 OS:`x86_64-windows-gnu → x86_64-w64-windows-gnu` 的差异正是这一行要报告 的归一化,拿整个三元组比会拒掉每一次正确的交叉构建。 -- **⭐⭐ 「首次运行」那条路把 `--target` 丢了。** +- **「首次运行」那条路把 `--target` 丢了。** ``` First run no toolchain configured — installing gcc@16.1.0 (glibc, native ABI) @@ -1525,11 +1613,11 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `overrides.target_triple` 在这条路上**从未被读取**。而载荷解析那条路上 `autoInstall=true` **本来就在**,只是没被走到。 - ⭐ 修法不是加条件,而是让首次运行**汇入**那条已经会处理目标的路径。上面那条 + 修法不是加条件,而是让首次运行**汇入**那条已经会处理目标的路径。上面那条 「同一个操作系统」的不变量因此有了配套:守卫让错配变成一句拒绝,汇入让本来就能 服务的目标不再走到那句拒绝。 - ⚠️ **这一处的第一版是无限递归,而我的注释写着「深度为一」。** 闸放在了分支之外 + **这一处的第一版是无限递归,而我的注释写着「深度为一」。** 闸放在了分支之外 (必须放外面:它上面那段 Windows 代码自己会设置 target),而标志没有任何人复位。 本机看不见——这一格只在「首次运行 + 交叉目标」出现,而开发机永远不是首次运行。 抓到它的是两条 CI,症状还不同:生态仓库上 `Resolved` 打四遍后 **exit 139 @@ -1541,7 +1629,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 它用 `host_can_serve`(问的是「有没有预制载荷」)去回答「能不能构建」。实测 Linux 上 `x86_64-windows-musl` 不在列表里,而同一台机器能产出真正的 PE32+。 - ⚠️ **而不是每一行缺席都是这样**:`x86_64-windows-msvc` 与 `aarch64-macos` 在 + **而不是每一行缺席都是这样**:`x86_64-windows-msvc` 与 `aarch64-macos` 在 Linux 上缺席是**对的**,MSVC 与 macOS SDK 是宿主专有的,依赖替代不了。判据不 需要新字段——**一行若指向本宿主装得上的编译器,那它缺的只是系统,而系统可以 由图供给**。第三种状态:`via dependency graph`。 @@ -1565,7 +1653,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 293 拒绝跨 OS 的解析 + 四个正确交叉目标零误伤;294 列出图供给的目标 + 宿主 专有的仍然缺席。 -- **⚠️ `285`–`289` 此前一条都没在 CI 跑过。** 它们声明 `# requires: llvm`,而两个 +- **`285`–`289` 此前一条都没在 CI 跑过。** 它们声明 `# requires: llvm`,而两个 linux e2e shard 的能力行里没有 `llvm`,`run_all.sh` 在 skip 时退 0。新增 `openkal-cross.yml` 的 `ecosystem-e2e`:装 gcc + llvm + 两个模拟器,直跑六条, **逐条断言 PASS 行**,并对 287/288 **额外断言运行阶段那一行**(实测它们会降级 @@ -1580,7 +1668,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 修复 -- **⭐⭐ 图供给内核接口时,载荷那份 C 库被一起断开了。** +- **图供给内核接口时,载荷那份 C 库被一起断开了。** ``` error: hermetic link check failed @@ -1610,12 +1698,12 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 分开:**一个在平台之上实现 openkal 的后端,而程序仍用载荷的 C 库**。 驱动照样索取启动文件,链接器却没有了可查的路径。 - ⚠️ **这个形状正是每个 openkal 后端被测试的方式** —— openkal-linux、 + **这个形状正是每个 openkal 后端被测试的方式** —— openkal-linux、 openkal-macos、openkal-windows 三家的一致性套件都对着平台自己的 C 库构建。 它们的 CI 钉在旧 mcpp 上,所以一直绿;pin 一动就全红,而这是缺陷被发现的 唯一原因。 - ⚠️ **mcpp 278 条 e2e 里,「kernel-abi 来自图 + C 库来自载荷」这个组合一条 + **mcpp 278 条 e2e 里,「kernel-abi 来自图 + C 库来自载荷」这个组合一条 都没有。** 新增 `285_kernel_abi_from_graph_keeps_the_payload_c_library.sh`, 断言到**产物能跑**,而不只是链接成功。 @@ -1629,14 +1717,14 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 区间由 CI 侧三分支并行二分给出:`8.21.3 绿 → 8.24.1 红`,唯一实质提交是 #486。 - ⭐ **判据是层的来源,不是情形的列举。** `Origin` 有四个值 + **判据是层的来源,不是情形的列举。** `Origin` 有四个值 (`Payload` / `Xpkg` / `Graph` / `None`),而链接线要问的是「C 库是不是来自 解析之前就存在的目录」—— 这正是 `Layer::prebuilt()` 的定义,也是本模块开头 那段注释划分世界的方式(prebuilt 在解析前可知,composed 在解析后才知)。 因此**没有新增谓词**:两处改用 `cAbi.prebuilt()`,与既有的两处读法(C++ 层 能否用载荷运行时、`check_layering`)成为同一个事实的第三次读取。 - ⚠️ 一版写成 `fromGraph() || absent()` 的中间修法被否掉了:它答对三个来源、 + 一版写成 `fromGraph() || absent()` 的中间修法被否掉了:它答对三个来源、 对 `Xpkg`(来自预构建 sysroot 的 C 库)沉默,而那同样不是载荷的。 - **ninja 后端的测试夹具补上了目标侧。** @@ -1647,10 +1735,10 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 才分开,而夹具描述的于是变成「没有 C 库的目标」——它拿到 `-nostdlib -static` 且不带任何运行期搜索路径,而这个文件里 44 条断言全是关于「有载荷 C 库」的。 - ⚠️ **没有任何生产路径会带着全 `None` 的 `TargetSide` 走到 flags**:`resolve` + **没有任何生产路径会带着全 `None` 的 `TargetSide` 走到 flags**:`resolve` 给普通本机构建的是 `cAbi = { Payload, … }`。夹具现在照实写。 -- **⭐⭐ 缓存键漏掉了新参数,于是跨着一处不兼容命中了。** +- **缓存键漏掉了新参数,于是跨着一处不兼容命中了。** `compile_flags(spec)` 在 #486 长出第二个参数 `targetCxxRuntime`,而缓存键 仍按一个参数算: @@ -1663,11 +1751,11 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 「拿到一份为另一套 flag 建的产物」**——实测 `openkal@0.7.0` 出现 6 个槽位 对应 5 个尺寸各异的 BMI。 - ⚠️ 这类缺陷不会在加参数的那天失败,它在下一次缓存命中时失败,而那时改动 + 这类缺陷不会在加参数的那天失败,它在下一次缓存命中时失败,而那时改动 已经不在视野里了。三条单元测试因此**直接打在 `build_axes()` 上**,而不是手 搭一个 `BuildAxes`——后者表达不出「推导过程本身错了」这件事。 -- **⭐ 载荷的 C++ 运行时,服务的是载荷的 C 库。** +- **载荷的 C++ 运行时,服务的是载荷的 C 库。** ``` undefined reference to `__cxa_allocate_exception' @@ -1678,12 +1766,12 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 OR。一个「内核接口来自包、而 C 库与 C++ 运行时都来自载荷」的程序本该由那些归档 服务,OR 却说不是,`-nostdlib++` 于是砍掉了它要用的那一份。 - ⚠️ **这一处在两个方向上都错过。** 最初是 `targetCxxRuntime`,对 C 程序失败 + **这一处在两个方向上都错过。** 最初是 `targetCxxRuntime`,对 C 程序失败 (它没有 C++ 运行时,答「否」被读成「载荷的是对的」);#486 换成 OR,又矫枉过正。 **C 库才是决定它的那一层** —— 理由 `check_layering` 早已反向陈述:载荷的 C++ 运行时是对着载荷的 C 库配置的,所以当且仅当那份 C 库在用时它才可用。 -- **⭐ 第四条同型:`linkage = "dynamic"` 的「无效」诊断在说谎。** +- **第四条同型:`linkage = "dynamic"` 的「无效」诊断在说谎。** 实测 2026-08-25,在 285 的形状上(kernel-abi 来自图 + C 库来自载荷): @@ -1701,7 +1789,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 prepare.cppm 里另外两处 `system_from_graph()` 保留:它们问的是「图有没有供给 系统的任一部分」,那确实是两层的问题。 -- **⭐ `build.mcpp` 的 `PATH` 前置项目声明的那个环境。** +- **`build.mcpp` 的 `PATH` 前置项目声明的那个环境。** ``` PATH=<被声明环境的 bin>: @@ -1713,15 +1801,15 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `[error] qemu-system-riscv64 is not installed in this subos` —— 找到了、报告 为存在、却跑不了,而可用的那份就在项目自己的环境里,不在 `PATH` 上。 - ⚠️ **只对声明了 `[xlings].subos` 的项目生效,没声明的逐字节不变。** 一个更早 + **只对声明了 `[xlings].subos` 的项目生效,没声明的逐字节不变。** 一个更早 的草案无条件前置 mcpp 共享的 `subos/default/bin`,那会让「构建看见什么」取决 于这台机器上还装过什么 —— 同一台机器上的两个项目彼此一致,而同一个项目在两台 机器上不一致。是声明本身把它放到前面的。 - ⚠️ **前置而非替换。** 构建程序合理地会调 `git`、`python3`、shell,这些都不在 + **前置而非替换。** 构建程序合理地会调 `git`、`python3`、shell,这些都不在 SubOS 里;只有被声明目录的 `PATH` 会把它们全部弄坏。 - ⭐ **没有新的决定点。** `mcpp::xlings::runtime` 早就是「项目用哪个 SubOS」的 + **没有新的决定点。** `mcpp::xlings::runtime` 早就是「项目用哪个 SubOS」的 唯一策略,`RuntimeBinding::subosDir` 是它已解析的答案;本次只是把这个答案多交 付给一个消费者。`projectSubosBin` 在绑定解析后算**一次**,两个交付点各自取用。 本次发布修的三条缺陷全部来自「一个事实在多处各自推导」。各包的载荷路径由 @@ -1754,7 +1842,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 | 290 | 声明把环境放到 `PATH` 前面,**而且只有声明会** —— 两半都对着**继承的那个值**比对,不是比对一个模式 | | 291 | `dynamic` 只在 C 库来自图时被拒 —— 且断言产物的 `DT_NEEDED` 而非只断言文案 | -- **⚠️ 上面这张表里的 285–289,此前一条都没在 CI 跑过。** +- **上面这张表里的 285–289,此前一条都没在 CI 跑过。** 它们声明 `# requires: llvm`,而两个 linux e2e shard 报的能力行是 @@ -1773,7 +1861,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 这六条,再逐条断言它们的 PASS 行真的出现了。与 `ci-linux-e2e.yml` 的 `baremetal` job 同形,同因。 - ⭐ **这个 job 第一次跑就抓到 287 在说谎。** 它用 + **这个 job 第一次跑就抓到 287 在说谎。** 它用 `command -v llvm-objdump || command -v objdump` 找反汇编器,而 CI 上前者不在 PATH、后者是宿主 GNU binutils —— BFD 只编了 x86_64。让它反汇编 aarch64 会打印 一个文件头、**零条指令、零报错**: @@ -1799,7 +1887,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 新增 -- **⭐ `x86_64-windows-musl`:Windows 上的 musl 有名字了。** +- **`x86_64-windows-musl`:Windows 上的 musl 有名字了。** 同一个 `--target x86_64-windows-gnu`,两种体系下的 C 库完全不同 —— 而 mcpp 用同一个名字称呼它们。实测同一份源码: @@ -1809,11 +1897,11 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 | 体积 | 587,894 | **9,815,552**(16.7×) | | 依赖 DLL | `KERNEL32`、**`msvcrt.dll`** | `ntdll`、`KERNEL32`、`SHELL32` | - ⭐ MinGW 的 C 库不是自足的:`printf`/`malloc` 的实现在目标机自带的 + MinGW 的 C 库不是自足的:`printf`/`malloc` 的实现在目标机自带的 `msvcrt.dll` 里。musl 是自足的,整个 C 库静态链入,只经 openkal 调 Win32 原语。**两者是不同的东西,不该共用一个名字。** - ⚠️ **这个名字 LLVM 拼不出来。** 实测 llvm 22.1.8: + **这个名字 LLVM 拼不出来。** 实测 llvm 22.1.8: ``` clang++ --target=x86_64-pc-windows-musl -c t.cpp @@ -1824,7 +1912,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `gnu`/`cygnus`/`itanium`/`musl`,前三个能编,只有 `musl` 死; 预定义宏显示它从未被建模(无 `__MINGW32__`)。 - ⭐ **于是 mcpp 的名字与交给 clang 的三元组必须是两个字符串**,而它们 + **于是 mcpp 的名字与交给 clang 的三元组必须是两个字符串**,而它们 本来就是 —— 构建报告里那个箭头两侧就是: ``` @@ -1832,11 +1920,11 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ^ 回答「C 库是谁」 ^ 回答「遵循哪套对象 ABI」 ``` - ⚠️ 该行 pin 为 `llvm@22.1.8`,不是偏好:全局默认为 gcc 时,空 pin 会让 + 该行 pin 为 `llvm@22.1.8`,不是偏好:全局默认为 gcc 时,空 pin 会让 它落到 musl-gcc 载荷并报「没有 C++ 前端」—— 一条关于缺前端的消息, 而真正的问题是只有 clang 能发这个目标。裸机各行同理。 - ⚠️ 档为 `preview`:`verified` 的定义是「构建**并运行**过」,而端到端 + 档为 `preview`:`verified` 的定义是「构建**并运行**过」,而端到端 可用还差一环 —— `openkal-musl@0.3.3` 精确钉死 `openkal-windows = "0.1.3"`, 索引里已有的 0.1.4 到不了消费者。生态链条另行推进。 @@ -1845,15 +1933,15 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 - **构建期体系下 `x86_64-windows-gnu` 的名实不符,此前静默。** ``` - x86_64-linux-gnu 名字说 gnu,事实是 musl → ⚠️ 警告 - x86_64-windows-gnu 名字说 gnu,事实是 musl → ❌ 静默 + x86_64-linux-gnu 名字说 gnu,事实是 musl → 警告 + x86_64-windows-gnu 名字说 gnu,事实是 musl → 静默 ``` `check_request()` 豁免了非「C 库轴」的平台,理由是「Windows 上 `gnu` 命名对象 ABI 而非 C 库」—— **只对了一半**:那一段捆着对象 ABI(被兑现) 与 MinGW 的 C 运行时(被图替换),第二件正是该函数存在的意义。 - 判据改为「轴 ∈ {CLibrary, ObjectAbi}」。⚠️ 裸机的 `elf` 继续豁免 —— + 判据改为「轴 ∈ {CLibrary, ObjectAbi}」。裸机的 `elf` 继续豁免 —— 它在任何平台上都不命名 C 库,对它说「请求了 `elf` C ABI」是胡话。 消息在对象 ABI 轴上额外说明 ABI 那一半**未受影响**。 @@ -1873,7 +1961,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 fatal error: 'sbi.h' file not found ``` - ⚠️ 这条诊断指向的东西全是错的:路径是作者没写过的暂存目录,头文件就躺在源码 + 这条诊断指向的东西全是错的:路径是作者没写过的暂存目录,头文件就躺在源码 期待的位置,而触发它的构建没有要求任何不寻常的事情 —— 一张图里有两个主版本是 受支持的安排,这是它最普通的后果。 @@ -1890,7 +1978,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 改进 -- **⭐ `-` 现在在每个平台上都是一个完整的目标。** +- **`-` 现在在每个平台上都是一个完整的目标。** ``` mcpp build --target x86_64-windows # 此前:error: unknown target @@ -1901,7 +1989,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 **而它只在 Linux 与 macOS 上成立**。被强制写出第三段的两个平台 (windows、none),恰恰是构建期体系最常用的两个。 - ⭐ **真正的分界是两种体系,而不是平台。** + **真正的分界是两种体系,而不是平台。** | | 传统预构建体系 | 构建期体系 | |---|---|---| @@ -1913,11 +2001,11 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 它**更准确**,因为那次构建里编译器是 clang、链接器是 lld、C 库是 musl、 C++ 运行时是 libc++,**没有任何东西是 GNU 的**。 - ⚠️ **填 `gnu` 而不是宿主自己的 env。** `host_triple()` 在 Windows 上答 + **填 `gnu` 而不是宿主自己的 env。** `host_triple()` 在 Windows 上答 `msvc`,按它填会让同一条命令在不同宿主上产生不同的输出目录与缓存键 —— 目标的身份不允许依赖于它在哪里被构建。 - ⭐ **判据是一个指纹,不是「能构建」。** 同一个二进制、干净的 `target/`, + **判据是一个指纹,不是「能构建」。** 同一个二进制、干净的 `target/`, 两种拼法都落进 `target/x86_64-windows-gnu/<同一指纹>/`,第二次构建 0.10s 全缓存命中。若各自产生一个指纹,短拼法就只是「少打四个字符、 多编译一遍」。 @@ -1940,7 +2028,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 c-abi musl (openkal-musl@0.3.3, graph) ``` - ⭐ 沉默作为**诊断**是对的 —— 在 Windows 上报「名字请求了 `gnu` C ABI」 + 沉默作为**诊断**是对的 —— 在 Windows 上报「名字请求了 `gnu` C ABI」 会在每一次合法的 MinGW 构建上出现,而且说的是错的。作为**报告**则不够: 读者在其中找不到一行叫 `gnu`,于是把它映到最像 C 库名字的那一行。 @@ -1950,7 +2038,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 Target x86_64-windows-gnu → x86_64-w64-windows-gnu (gnu selects the Itanium C++ ABI, not a C library) ``` - ⭐ **它不对应报告里的任何一行,而这正是要点。** 对这次构建的产物实测: + **它不对应报告里的任何一行,而这正是要点。** 对这次构建的产物实测: | 观测 | 值 | |---|---| @@ -1964,14 +2052,14 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 Itanium ABI 上。因此报告命名那套 **ABI 本身**,它的名字不出现在任何一行里, 于是不会被误当成某一层。 - ⚠️ 名词取自**值**而非仅取自轴:`gnu` 与 `msvc` 同轴而选中相反的 ABI, + 名词取自**值**而非仅取自轴:`gnu` 与 `msvc` 同轴而选中相反的 ABI, 按轴取名会给 MSVC 构建打印「Itanium」—— 那不只是含糊,是假的。 内部把 `envNamesCAbi` 这个布尔换成 `EnvAxis`,因为布尔是对事实的有损编码: 该段在 Linux 上是 C 库、Windows 上是对象 ABI、无操作系统时是对象格式。 一个只回答「是否为第一种」的布尔,能压住错误的警告,却给不出正确的名字。 - ⚠️ 该提示**不出现**在 C 库来自载荷时:那种情况下 C 库正是三元组选中的, + 该提示**不出现**在 C 库来自载荷时:那种情况下 C 库正是三元组选中的, `gnu → ucrt` 是可见的因果,加注就成了每次普通 Windows 构建上的噪声。 ### 文档 @@ -1984,7 +2072,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 修复 -- **⚠️ `mcpp run --target X` 把 X 的构建记进了宿主的槽。** +- **`mcpp run --target X` 把 X 的构建记进了宿主的槽。** 损害不在这条命令 —— 它构建得完全正确 —— 而在下一条: @@ -2004,10 +2092,10 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 overrides 并正确交叉构建,唯独没把它传给写缓存的那一步。`mcpp build` 的 两个调用点都传了。 - ⭐ 同一个缺陷有**两条到达路径**:目标写在清单的 `[build] target` 里(已守), + 同一个缺陷有**两条到达路径**:目标写在清单的 `[build] target` 里(已守), 以及目标来自 `--target` flag(本次)。修好一条不会暴露另一条。 - ⚠️ 回归测试的判据是**路径而不是退出码**。异架构产物 exec 会失败,所以 + 回归测试的判据是**路径而不是退出码**。异架构产物 exec 会失败,所以 只看退出码的测试在那里因错误的理由通过,**而在同架构上完全测不到** —— 后者更危险:用户不会看到崩溃,只会拿到一个 musl 产物冒充宿主产物。 @@ -2015,7 +2103,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 修复 -- **⚠️ 三元组是请求,而解析把「未指定」抹掉了。** +- **三元组是请求,而解析把「未指定」抹掉了。** ``` $ mcpp build --target x86_64-linux # 我写的是「不指定 C 库」 @@ -2027,23 +2115,23 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 身份必须是全的,请求必须能说「没指定」;`parse` 用自动填充让身份变全, 代价是请求消失 —— 两态在下游不可区分。 - ⚠️ 修法是窄的:保留填充,另记 `Triple::envExplicit`。而请求**必须在规范化 + 修法是窄的:保留填充,另记 `Triple::envExplicit`。而请求**必须在规范化 之前捕获** —— `str()` 渲染的是填好的身份,之后再 parse 就分不出来了。 未指定 ⇒ 报告显示工程写的那个拼写;写了且与图矛盾 ⇒ **拒绝**。 -- **⚠️ 目标行的约定在图之前就被应用,而它要回答的问题在图之后才有答案。** +- **目标行的约定在图之前就被应用,而它要回答的问题在图之后才有答案。** `x86_64-linux-musl → gcc@16.1.0` 说的不是「偏好 gcc」,是「musl-gcc 载荷 供给这个目标的 C 库」。工程的 C 库若来自依赖图,该载荷根本不被使用。 - ⚠️ 早决定被**双向实测**否掉:无条件应用会替换用户用 `mcpp toolchain default` + 早决定被**双向实测**否掉:无条件应用会替换用户用 `mcpp toolchain default` 设下的工具链;不应用会让一个零依赖的交叉构建从可用变为不可用。 - ⭐ 判据换成它本来就该是的那个:**图供给 `kernel-abi` 或 `c-abi` 时,约定不适用。** + 判据换成它本来就该是的那个:**图供给 `kernel-abi` 或 `c-abi` 时,约定不适用。** 工具链解析因此移到依赖解析之后。 - ⚠️ 代码不搬,只搬执行时机 —— 原地包成 lambda,在图已知处调用。 + 代码不搬,只搬执行时机 —— 原地包成 lambda,在图已知处调用。 先前记录的「39 处读写挡着」是**没测就写下的**:实测依赖解析段读 `tc` 仅 1 处, 而那一处要的是三元组不是编译器。 @@ -2051,9 +2139,9 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 | 场景 | 结果 | |---|---| - | openkal 工程 + 全局 llvm 默认 + `--target x86_64-linux-musl` | `Resolved llvm@22.1.8` ✅ | - | 无依赖工程 + 同一目标 | `Resolved gcc@16.1.0`(约定生效)✅ | - | 无依赖工程 + `x86_64-windows-gnu` | `Resolved gcc@16.1.0` ✅ | + | openkal 工程 + 全局 llvm 默认 + `--target x86_64-linux-musl` | `Resolved llvm@22.1.8` | + | 无依赖工程 + 同一目标 | `Resolved gcc@16.1.0`(约定生效)| + | 无依赖工程 + `x86_64-windows-gnu` | `Resolved gcc@16.1.0` | ## [2026.8.24.2] — 2026-08-24 @@ -2065,7 +2153,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 builtins**、21 个是 libunwind 的。这个包最大的一块此前声明在 `mcpp:c++-abi` 名下 —— 而 `__udivti3` 及其同类是一个**纯 C 程序**需要的东西,与 C++ 无关。 - ⚠️ 把 builtins 算作 C++ 运行时的一部分,与 `targetside` 模块开头记录的缺陷同形: + 把 builtins 算作 C++ 运行时的一部分,与 `targetside` 模块开头记录的缺陷同形: 一个交叉到 macOS 的 C 程序被问「有没有 C++ 运行时」,答「没有」,链接行因而 保留了载荷自带的 libc++。**一个只有部分程序需要的层仍然是层。** @@ -2073,7 +2161,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `compiler` 是唯一一个包不能供给的层 —— 族与族之间的差异(flag 拼写、模块模型、 BMI 格式、驱动 cfg)是引擎必须持有的事实,不是数据能描述的。 - ⚠️ `compiler` 层上报**族名**(`llvm`)而非驱动名(`clang`):使用者书写的每一处 + `compiler` 层上报**族名**(`llvm`)而非驱动名(`clang`):使用者书写的每一处 都用族名,报告用驱动名会让 `requires = ["mcpp:compiler=llvm"]` 永远不可满足。 - **`requires = ["mcpp:<层>=<实现>"]` —— 在引擎里不出现实现名的前提下执行分层规则。** @@ -2084,11 +2172,11 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `if (stdlib == "libc++" && compiler == gcc)` 会把两个实现名放进引擎; 写在包里,引擎只需检查一条它能一般性陈述的关系。 - ⚠️ 检查在**编译开始之前**运行,这正是声明它的全部意义。 + 检查在**编译开始之前**运行,这正是声明它的全部意义。 - **规则一:每层恰好一个供给者。** - ⚠️ 此前两个包供给同一层时,**图遍历顺序里第一个静默胜出** —— 那个顺序既不是 + 此前两个包供给同一层时,**图遍历顺序里第一个静默胜出** —— 那个顺序既不是 作者写的,也不是他能预测的 —— 而落选者的 `[build]` 段仍然进入命令行。 判据是失败模态:选错不会让链接失败,会得到一个能跑、偶尔崩的程序。 `[build] runner` 早已按同一条规则处理。 @@ -2111,13 +2199,13 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 - **`Family` 去掉 `OpenkalLlvm`,拼写归一为 `llvm`。** - ⚠️ 保留枚举项的代价不止一条死分支:可用工具链列表按族枚举,一份载荷挂在两个 + 保留枚举项的代价不止一条死分支:可用工具链列表按族枚举,一份载荷挂在两个 族名下就出现两次,而安装状态按族记录 ⇒ **第二份被报成未安装,并被推荐给已经 装了它的人。** - **词表 pin 替换用户默认时,状态行说出替换与修法;图已知后指出这次替换本可不必。** - ⚠️ 让全局默认压过词表 pin 的做法被实测否掉:一个无依赖的工程、全局默认 + 让全局默认压过词表 pin 的做法被实测否掉:一个无依赖的工程、全局默认 `llvm@22.1.8`、`--target x86_64-windows-gnu`,**从能构建变成不能构建**。 行所 pin 的不是「偏好的编译器」而是「供给该目标 C 库的载荷」。 结构性修法(把 pin 的决定与目标侧一样后移)未在本版落地:`tc` 在解析后到图之间 @@ -2128,7 +2216,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 - `hosted-standard-library` 继续表示 C++ 层; - `openkal-llvm` 拼写继续解析; - `[package]` 下三个 std-module 键继续被接受; -- ⚠️ 实测:**旧引擎(2026.8.24.1)读带 `requires` 的清单构建成功** —— +- 实测:**旧引擎(2026.8.24.1)读带 `requires` 的清单构建成功** —— TOML 侧忽略未知键,xpkg 侧警告而非报错。已发布的包因此可以先行声明。 ## [2026.8.20.2] — 2026-08-20 @@ -2141,7 +2229,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 一份都不要、厂商 SDK 上的工程要换成 newlib、`std-freestanding` 坐到 openkal 的 C 库上也要换。**一个旋钮解锁三条线。** - ⚠️ 字段是可选值而不是字符串,因为**缺席与空是两个不同的答案**:缺席继承目标表行, + 字段是可选值而不是字符串,因为**缺席与空是两个不同的答案**:缺席继承目标表行, `sysroot = ""` 是零 libc 档。用普通字符串两者不可区分,而空串正是没有 sysroot 的 目标行本来的样子 —— 内核工程会静默地把 picolibc 拿回去。 @@ -2153,7 +2241,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `mcpp::target_builtins_lib()` / `mcpp::target_libc_profile()` / `mcpp::target_libc()`。 - ⚠️ 这层耦合**在任何 manifest 里都看不见**:`riscv-virt-rt` 既不声明 LLVM 也不 + 这层耦合**在任何 manifest 里都看不见**:`riscv-virt-rt` 既不声明 LLVM 也不 声明 picolibc,却依然服务不了第二种工具链或第二份 C 库,因为 `clang_rt.builtins-riscv64` 与 `rv64gc/lp64d` 写进了它的 build.mcpp。**声明出来的 依赖可评审;写死的名字只在换东西时才失败。** @@ -2165,13 +2253,13 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 裸机工程一用 `std::vector`,链接就死在标准库深处某个头文件里的 mangled 符号上, 而消息里没有一处说明哪个包提供它。现在追加说明,并给出激活 feature 的写法。 - ⭐ 该建议**不带版本字面量** —— 它点名的是包与 feature,因此跨该包的每个版本都成立。 + 该建议**不带版本字面量** —— 它点名的是包与 feature,因此跨该包的每个版本都成立。 ### 测试 - **e2e 机器校验 `import std` 诊断里那条可粘贴的依赖行。** - ⚠️ 同一个缺陷发过两次(先是包不存在,后是版本过期),而两次的修法都是「改字面量 + 同一个缺陷发过两次(先是包不存在,后是版本过期),而两次的修法都是「改字面量 + 加注释」;第二次发生时,第一次留下的注释就在断掉的那一行正上方。**注释强制不了 跨仓库不变量。** @@ -2195,7 +2283,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `DT_RUNPATH`——一串指向**构建机** `~/.mcpp/` 的绝对路径。消费者在另一台机器上 拿到的是 `libstdc++.so.6: cannot open shared object file`。 - ⚠️ **issue 里建议的 `$ORIGIN` 修不好它,空串也修不好。** 在真实包 + 真实消费者上 + **issue 里建议的 `$ORIGIN` 修不好它,空串也修不好。** 在真实包 + 真实消费者上 实测(把构建机 store 变成不可达): | 发货 `.so` 上的状态 | 消费方 `DT_RPATH` 被继承 | 结果 | @@ -2249,7 +2337,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 |---|---|---| | 可执行文件 | `--strip-all` | 没有人链接它 | | 共享库 | `--strip-unneeded` | 保留 `.dynsym`——那**就是**导出表 | - | 静态归档 | `--strip-debug --enable-deterministic-archives` | ⚠️ `--strip-all` 会删掉归档的**符号索引**,消费方链接时报 `archive has no index; run ranlib to add one`(实测) | + | 静态归档 | `--strip-debug --enable-deterministic-archives` | `--strip-all` 会删掉归档的**符号索引**,消费方链接时报 `archive has no index; run ranlib to add one`(实测) | 被捆绑进 bundle 的第三方 `.so` **不**剥——它们不是 mcpp 构建的。 @@ -2260,7 +2348,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 `libmathkit-shared.so` 是常态),扁平布局会让后一条覆盖前一条,而前一个产物的 `.gnu_debuglink` 会静默指向另一个目标的符号。 - ⚠️ **一条要知道的后果**:裸 `mcpp build` 与裸 `mcpp pack` 现在写进**不同的** + **一条要知道的后果**:裸 `mcpp build` 与裸 `mcpp pack` 现在写进**不同的** `target///` 目录(指纹把 profile 算进去了)。手工放到构建 产物旁边的文件只在两条命令解析到同一个 profile 时才被 `pack` 看见;声明式通道 (`[runtime] deploy_files`、`runtime_search_dirs`)不受影响。e2e 240 因此在 @@ -2288,7 +2376,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 - **原生 `cl.exe` 可以消费打包库了 —— 而且这条此前就已可用,是文档没跟上。** 机制(方言中立的 `[target..runtime]`)在 2026.8.18.2 就落地了,而 - docs/12 的边界表仍写着 ❌,同一份文档的正文却在描述解决方案。现在两端都验证了: + docs/12 的边界表仍写着 ,同一份文档的正文却在描述解决方案。现在两端都验证了: 渲染侧有可移植单测,端到端的 e2e **消费方钉死 `msvc@system`** —— 这一点是判据: 若中立形式被忽略而 `ldflags` 生效,clang 消费者**照样能过**,只有 cl 会因为 一个 `-L` 失败,所以只有它能证明这件事。e2e 还断言生成的图里**没有**该腿的 `-L`。 @@ -2299,7 +2387,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 而每次挂错的表现都相同:**在恰好一个平台上莫名其妙地失败**,报错既不点名那个 flag,也不点名它背后的决定。 - ⚠️ 其中一条是反直觉的:`-L` vs `/LIBPATH:` **按方言判才是对的** —— + 其中一条是反直觉的:`-L` vs `/LIBPATH:` **按方言判才是对的** —— 它交给的是 mcpp 直接调用的那个程序,而不是链接器。面向 MSVC ABI 的 clang 是同时区分这三根轴的反例:它说 GNU 方言、产 MSVC ABI 对象、出 PE 映像。 @@ -2328,7 +2416,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 - docs/05 增加一条可引用的规则:**`sources` 匹配到的每一项都必须产出会被链接的对象** —— 让 2026.8.18.2 引入的硬失败有出处,而不是凭空多出一条禁令。 中文版此前连 `sources = []` 那条注记都没有,一并补齐。 -- docs/12 的边界表改为 ✅(中英);MSVC **数据符号仍需 `dllimport`** 这条限制 +- docs/12 的边界表改为 (中英);MSVC **数据符号仍需 `dllimport`** 这条限制 由 e2e 258 **做成可复现对照**,不再只是散文 —— 断言钉的是「有/无 `dllimport` 行为不同」,而不是某条随工具链版本变化的报错文本。 @@ -2388,7 +2476,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 根因是「这是不是模块接口」有**两个答题者**:扫描器读到 `export module` 记下 `provides`(所以边上挂了 `bmi_out`),而分类器说 `Other`,链接集合只读后者。 - ⚠️ 这是**行为变化**:`sources` 里混进 `.md` / `.txt` 的工程会开始报错。 + 这是**行为变化**:`sources` 里混进 `.md` / `.txt` 的工程会开始报错。 ## [2026.8.18.1] — 2026-08-18 @@ -2426,7 +2514,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 - **`kind = "shared"` 不再只有 Linux:PE/MinGW 与 Mach-O 都能产、能打包、能跑。** 过去这条路只在 ELF 上验证过,其余一律拒绝。**这是能力的增加,不是修一个洞** —— - ⚠️ 早前的提交信息把那道守卫描述成「在原生构建上失效」,那是**错的**: + 早前的提交信息把那道守卫描述成「在原生构建上失效」,那是**错的**: `tc.targetTriple` 由编译器的 `-dumpmachine` 填,原生构建上**非空** (实测 `resolution.json` 记的是 `x86_64-linux-gnu`),所以原生 macOS / 原生 Windows 本来就被它拦住。 @@ -2514,7 +2602,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 用到它才暴露出来。现在扫描器记 `provides = M:part` 并标 `providesInterface = false`;`import :part;` 按 TU 自己所属的模块名解析。 - **⚠️ 两处行为变化**:①两个文件声明同一个分区(`module m:p;` × 2)现在会被 + **两处行为变化**:①两个文件声明同一个分区(`module m:p;` × 2)现在会被 **拒绝并点名两个文件**,此前是静默接受 —— 那种程序本来就 ill-formed, 但它是一条新的失败路径;②`sources = []` 从「等于不写」变成「什么都不编」, 一个真写了 `sources = []` 又依赖默认 glob 的工程会发现产物变空(此前无法表达 @@ -2611,7 +2699,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 **mspdbsrv.exe**,它构建完还活几十秒,而且就住在正要被删的 payload 里。 等它不现实(等多久都是猜),所以改成**挪走**。 - ⚠️ 第一版挪错了东西:去重命名 payload **目录**。Windows 允许重命名一个 + 第一版挪错了东西:去重命名 payload **目录**。Windows 允许重命名一个 **打开着的文件**(更新器就是这么替换运行中的 .exe),但**不允许**重命名 一个**含有**打开文件的目录 —— CI 当场否掉了这个前提。 @@ -2727,7 +2815,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 (xim 安装),解析与 `msvc@system` 共用一条(`installation_from_tools_dir`)。 所以受管 toolset 不是第二条代码路径,也就不会长出自己的 bug。 -- **⚠️ 破坏性变更:`msvc@19.44` 不再是 pin-verify。** +- **破坏性变更:`msvc@19.44` 不再是 pin-verify。** 它过去表示「用系统 MSVC,并校验 banner 前缀」——而且只有 `mcpp toolchain default` 会校验,**构建路径完全忽略它**。版本轴现在到处都表示 @@ -2791,7 +2879,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 性能 -- **⚠️ 「接口没变就不级联重编」的机制从设计之日起从未生效过 —— 现已修复。** +- **「接口没变就不级联重编」的机制从设计之日起从未生效过 —— 现已修复。** `cxx_module` 规则会保留上一份 BMI、重编、然后在内容相同时把旧文件换回去,让 ninja 的 `restat` 判定输出未变、从而**不重建导入者**。这套机制 2026-05-12 就设计 @@ -2870,7 +2958,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 修复 -- **⚠️ 回归:产物加载的库不是它链接的那一份 —— `$ORIGIN` 被 SubOS 库视图遮蔽。** +- **回归:产物加载的库不是它链接的那一份 —— `$ORIGIN` 被 SubOS 库视图遮蔽。** `2026.8.11.2`(PR #413)首次把 SubOS 库视图(farm)写进产物的 `DT_RPATH`,但它 落在 **`$ORIGIN` 之前**。于是 imgui/GLFW 应用链接的是 mcpp 从 `compat.x11` 源码 @@ -2890,7 +2978,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 类型里、由单测钉死。新增一个生产者必须先选一个槽 —— 而"选"正是"在产物自己的 目录之前还是之后"这个问题被提出来的地方。 -- **⚠️ 共享库不再把自己的 C++ 运行时导出给别人(ELF)。** +- **共享库不再把自己的 C++ 运行时导出给别人(ELF)。** `SharedLibrary` 此前与可执行文件共用 `Distributable` 角色,于是拿到同一份 self-contained 契约:`-static-libstdc++`。在 ELF 上这不是"私有一份" —— 只有一个 @@ -2926,7 +3014,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 修复 -- **⚠️ 回归:SubOS 没有自我描述时,`mcpp build` / `mcpp test` 直接失败 +- **回归:SubOS 没有自我描述时,`mcpp build` / `mcpp test` 直接失败 ([xlings#543](https://github.com/openxlings/xlings/issues/543))。** Windows 上 xlings 不写 `subos_info` 块,而 mcpp 把「缺声明」当成了错误,于是 @@ -3016,7 +3104,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 manifest **错误**而非警告,因为宣称 `.c` 是模块接口会把 C 文件送进 C++ 模块规则, 最终失败在一个既不提文件也不提这个键的地方。 - ⚠️ `.ccm`/`.cxxm`/`.ixx` **不进内置默认**。进了的话默认 glob 会跟着变宽, + `.ccm`/`.cxxm`/`.ixx` **不进内置默认**。进了的话默认 glob 会跟着变宽, 于是 `src/` 下躺着 vendored MSVC-only `.ixx` 的**已发布包会在一次 mcpp 升级后 突然开始编译它** —— 而包作者改不了已经发出去的 tarball。 @@ -3072,7 +3160,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 同样的指纹目录。 - `module_extensions` **进**指纹(它改图的形态);`build_program_timeout` **不进** (它不改任何一条边 —— 进了会让「抬高超时」重建全世界)。 -- ⚠️ 旧版 mcpp 遇到 `module_extensions` 会警告+忽略,然后把那些文件当普通翻译单元 +- 旧版 mcpp 遇到 `module_extensions` 会警告+忽略,然后把那些文件当普通翻译单元 编译 —— **错误的构建**而不是干净的失败。发布用了这个键的包必须声明 mcpp 版本下限 (见 `docs/10-publishing-a-library.md`)。 @@ -3573,7 +3661,7 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 **A. 仓库的 workspace pin 伏击了被测版本。** job 第一步就 checkout,于是仓库的 `.xlings.json`(声明**自举** mcpp,手工维护且**故意滞后**)落在工作目录里。它是目录作用域的,在 checkout 内部**压过全局安装**。于是「装的是 `MCPP_PIN`,跑的是自举版本」——而后者根本没装: ``` - ✓ 1 package(s) installed + 1 package(s) installed [error] xlings: version '2026.8.3.2' not found for 'mcpp' [error] available: 2026.8.3.4 ``` @@ -3899,7 +3987,7 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 `--no-cache` 保留为 `off` 的兼容别名。它的旧 help 文案「Force-clear target/ before building」两处不准:它清的是**构建目录**(`target///`)而不是整个 `target/`,而且名字与缓存无关。`mcpp run` / `mcpp test` 一并补上这两个 flag(此前它们连 `--no-cache` 都没有)。 - ⚠️ **`--no-cache` 的语义有一处收紧**:此前它只清构建目录、**仍然会回填全局缓存**;现在它等于 `off`,即**不读也不写**。想要「从零重编但仍然刷新缓存」的,用 `mcpp clean` 或 `rm -rf target` 后正常构建。这个收紧是为了让三个模式正交:一个叫 `off` 的模式还偷偷写缓存是说不通的。 + **`--no-cache` 的语义有一处收紧**:此前它只清构建目录、**仍然会回填全局缓存**;现在它等于 `off`,即**不读也不写**。想要「从零重编但仍然刷新缓存」的,用 `mcpp clean` 或 `rm -rf target` 后正常构建。这个收紧是为了让三个模式正交:一个叫 `off` 的模式还偷偷写缓存是说不通的。 - **`mcpp cache` 补齐到可运维。** `cache dir`(缓存到底在哪 —— 此前 `cache *`/`doctor`/`clean --bmi-cache` 各自解析根目录,而 config 的 reset 路径用 `GlobalConfig::bmiCacheDir`,两者可能不是同一个目录)、`cache gc --max-size {MiB,GiB} / --older-than {s,m,h,d}`(**真 LRU**)、`cache clean --deps|--std|--all|--legacy`、`cache list --json`、`cache verify`(逐条目校验清单与磁盘,残缺条目非零退出)。`cache info` 现在打印该条目的键输入 —— 怀疑命中错了时第一件想看的东西。 @@ -4031,7 +4119,7 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 > 包身份口径双侧收敛(#278)。事故:mcpp-index 把 `chriskohlhoff.asio` 的 `name` 从 `"chriskohlhoff.asio"` 改成 `"asio"`(namespace 不变),lint 全绿,三个平台的 workspace job 跑满 20~58 分钟后全挂 `E_NOT_FOUND` —— 描述符能解析、能过 mcpp 的身份闸门,却没有任何消费写法能装上。根因是身份归一化(容忍三种拼写)与安装目标构造(只支持一种)口径断层,契约只写在注释里、无人执行。设计见 `.agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md`。 -### ⚠️ 破坏性变更 +### 破坏性变更 - **裸依赖名不再解析到第三方命名空间的包。** 命名空间缺省时,`[dependencies]` 里的裸名**只**解析三类:`mcpplibs`(默认)、`compat`(包装)、无 `namespace` 声明的上游包。此前裸名会跨命名空间命中(例如裸 `tensorvia-cpu` 能装上 `aimol` 下的包),现在必须写全:`"aimol.tensorvia-cpu" = "…"` 或 `[dependencies.aimol] tensorvia-cpu = "…"`。 取舍理由:全域按名发现的便捷性换来三条稳定性损失——同名包的裁决依赖索引优先级(而用户 `[indices]` 添加的索引之间**无全序**)、**新增一个索引可能悄悄改变既有依赖解析到的包**(供应链隐患)、同一份 `mcpp.toml` 在不同机器上可能解析到不同包。依赖解析的可复现性优先于书写便捷性。 @@ -4328,7 +4416,7 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 - **`toolchain list` 两轴重排**:Toolchains 块(family@version)+ Targets 块 (target × 状态 installed/available/**planned**,planned 行使词汇表用户可见); 修版本字典序排序 bug(9.4.0 不再排在 15.1.0 前);`gcc X-musl` 行不再被 - `llvm` 劈开。README 平台表从词汇表重画(target × tier 维度,补 MSVC=✅ 与 + `llvm` 劈开。README 平台表从词汇表重画(target × tier 维度,补 MSVC=与 windows-gnu 行——旧表 MSVC 仍标 planned 是错的)。 - 修 Windows host 上 `mingw` 的门:Linux 上 `toolchain install mingw` 现在 合法(= 装交叉 payload,同一身份 host 分流);`mcpp run` 位置参数 help 改为 @@ -5024,19 +5112,19 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 ### 新增 -- ✅ **LLVM / Clang 工具链支持** —— 新增基于 `clang++`、`clang-scan-deps`、 +- **LLVM / Clang 工具链支持** —— 新增基于 `clang++`、`clang-scan-deps`、 `llvm-ar`、`lld` 的工具链探测与构建路径,支持 xlings `llvm` 包提供的 自包含 Linux LLVM 工具链。 -- ✅ **`import std` 支持** —— LLVM libc++ 模块标准库可用时,自动发现 +- **`import std` 支持** —— LLVM libc++ 模块标准库可用时,自动发现 `std.cppm` / `std.compat.cppm`,并接入标准库 BMI 预构建流程。 -- ✅ **`mcpp self config --mirror`** —— 通过 xlings 抽象层配置 sandbox +- **`mcpp self config --mirror`** —— 通过 xlings 抽象层配置 sandbox 镜像,默认初始化为 `CN`,CI 可显式切换为 `GLOBAL`。 ### 改进 -- 🔧 **工具链 provider 拆分** —— 将通用模型、探测逻辑、GCC、Clang、LLVM +- **工具链 provider 拆分** —— 将通用模型、探测逻辑、GCC、Clang、LLVM provider 与 registry 分离到独立模块,为后续更多工具链扩展预留入口。 -- 🔧 **xlings 索引兼容迁移** —— 自动将历史 `mcpp-index` 索引名迁移到 +- **xlings 索引兼容迁移** —— 自动将历史 `mcpp-index` 索引名迁移到 `mcpplibs`,避免旧 sandbox 状态影响新流程。 ## [0.0.4] — 2026-05-10 @@ -5045,7 +5133,7 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 ### 新增 -- ✅ **Glob 排除模式** —— `[modules].sources` (以及 Form B 的 `sources`) +- **Glob 排除模式** —— `[modules].sources` (以及 Form B 的 `sources`) 现在支持 `!` 前缀的排除模式(类似 `.gitignore`): ```toml sources = ["src/**/*.cpp", "!src/**/*_test.cpp", "!src/**/*_fuzzer.cpp"] @@ -5055,14 +5143,14 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 ### 改进 -- 🔧 **xlings 布局调整** —— xlings 二进制从 `/bin/xlings` +- **xlings 布局调整** —— xlings 二进制从 `/bin/xlings` (与 mcpp 同目录)移至 `/registry/bin/xlings` (= `/bin/xlings`)。由于 xlings 的 shim-creation guard 恰好检查 `/bin/xlings` 是否存在,新布局下 `ensure_sandbox_xlings_binary` 自然变成 no-op,省去了之前的 hardlink 步骤。 -- 🔧 **测试自动继承 sandbox PATH** —— `mcpp test` 在调用测试二进制前, +- **测试自动继承 sandbox PATH** —— `mcpp test` 在调用测试二进制前, 自动把 sandbox 的 `subos/default/bin`(含 patchelf、ninja 等 一次性自举工具)追加到 `$PATH`,使 test 代码 shell-out 到这些工具时 不再报 "command not found"。 @@ -5074,20 +5162,20 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 ### 新增 -- ✅ **依赖图传递性遍历** —— 直接依赖的子依赖(以及更深层)自动跟随入解析图, +- **依赖图传递性遍历** —— 直接依赖的子依赖(以及更深层)自动跟随入解析图, 消费者不必再在自己的 `mcpp.toml` 里把 grandchild 也写一遍;子依赖的 `[build].include_dirs` 也会沿链路传播,让中间层在编译时看得到 grandchild 的头文件。冲突检测同时区分 path / git / version 三类来源,跨来源不允许 混用。 -- ✅ **SemVer 合并解析(Level 2)** —— 同一个包在传递依赖图里被多个消费者 +- **SemVer 合并解析(Level 2)** —— 同一个包在传递依赖图里被多个消费者 以不同版本约束声明时,resolver 会把两条原始约束 AND 合并(裸版本号视作 `=X.Y.Z`),向 index 重新查询,选出同时满足两侧的具体版本。若该版本与 此前已 pin 的不一致,旧的 manifest 与 `[build].include_dirs` 会被原地 替换为新版本的内容,孩子依赖也按新 manifest 重新入队。新增 e2e `32_semver_merge.sh` 覆盖兼容合并 + 不可调和两条主链路。 -- ✅ **多版本 mangling 兜底(Level 1)** —— SemVer 合并失败时(典型如 +- **多版本 mangling 兜底(Level 1)** —— SemVer 合并失败时(典型如 `=0.0.1` ⨯ `=0.0.2` 这种无重叠的 pin),resolver 不再硬报错,而是把次要 版本的源码 stage 到 `target/.mangled//...` 下,通过正则改写 `(export )?module X;` / `(export )?module X:Y;` / `(export )?import X;` @@ -5101,7 +5189,7 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 ### 改进 -- 🔧 **构建后端按需为多包做 obj 路径命名空间** —— `plan.cppm` 检测到 +- **构建后端按需为多包做 obj 路径命名空间** —— `plan.cppm` 检测到 跨包同名源文件(多版本 mangling 后两个 `parse.cppm` 同时存在的常见情形) 时,自动把 `obj/.o` 改为 `obj//.o`,`.ddi` 扫描产物随之放在 object 同目录下。无碰撞时仍是原始 `obj/.o` @@ -5111,7 +5199,7 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 ### 新增 -- ✅ **C 语言源文件支持** — `mcpp.toml` 的 `[build]` 段新增 `cflags`、 +- **C 语言源文件支持** — `mcpp.toml` 的 `[build]` 段新增 `cflags`、 `cxxflags`、`c_standard` 三个字段;ninja 后端探测 `.c` 源文件后自动派 生兄弟 C 编译器(`g++ → gcc`、`clang++ → clang`、跨编译器前缀如 `x86_64-linux-musl-gcc` 同样适用),发出独立的 `c_object` 规则。 @@ -5120,7 +5208,7 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 mbedtls 3.6.1 全部 108 个 `.c` 源文件**(SHA-256 测试向量与 FIPS 180-4 一致)。 -- ✅ **lib-root 约定** — 库项目(`kind = "lib"` / `shared`)的 primary +- **lib-root 约定** — 库项目(`kind = "lib"` / `shared`)的 primary module interface 默认在 `src/.cppm`,且必须 `export module ;`(无 `:partition` 后缀);可用 `[lib].path = "src/foo.cppm"` 显式覆盖(cargo `lib.rs` 风格)。 @@ -5128,7 +5216,7 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 不匹配 [package].name)报 error;约定文件缺失只报 warning,给已有 项目软迁移时间。纯 binary 项目跳过所有检查。 -- ✅ **xpkg 风格依赖命名空间** — `mcpp.toml` 现在原生支持三种依赖书写形式: +- **xpkg 风格依赖命名空间** — `mcpp.toml` 现在原生支持三种依赖书写形式: - 平铺默认命名空间:`gtest = "1.15.2"` ⇒ `(mcpp, gtest)`,无引号 - TOML 子表命名空间:`[dependencies.mcpplibs] cmdline = "0.0.2"` ⇒ `(mcpplibs, cmdline)`,无引号 @@ -5140,7 +5228,7 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 ### 改进 -- 🛠 **`src/pm/` 包管理子系统(7 步重构,全部完成)** — 包管理相关代码 +- **`src/pm/` 包管理子系统(7 步重构,全部完成)** — 包管理相关代码 从 `cli.cppm`(3510→2900 行) / `manifest.cppm` / `lockfile.cppm` / `fetcher.cppm` / `publish/xpkg_emit.cppm` 中抽出,集中到独立的 `src/pm/` 目录下,跟 `build/` / `toolchain/` / `pack/` 平级。 @@ -5158,7 +5246,7 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 回滚;旧模块名(`mcpp.lockfile` / `mcpp.fetcher` / `mcpp.publish.xpkg_emit`) 保留薄 shim 透传到新模块,所有调用点零改动。规划与依赖图见 `.agents/docs/2026-05-08-pm-subsystem-architecture.md` §3-§5。 -- 📄 **新增设计文档** `.agents/docs/`: +- **新增设计文档** `.agents/docs/`: - `2026-05-08-package-index-config.md` — 多源包索引仓配置 + `mcpp.lock` 索引 commit 锁定 + 两层不可变性 (L1 publish policy + L2 lock mechanism) @@ -5167,10 +5255,10 @@ LLVM / Clang 工具链支持与 xlings 镜像配置完善。 ### 修复 -- 🐛 path 依赖的 `[package].name` 比对支持 xpkg 标准 `name` + 旧式 +- path 依赖的 `[package].name` 比对支持 xpkg 标准 `name` + 旧式 `.` 复合名两种形式,兼容当前 mcpp-index 描述符尚未迁移的 状态。 -- 🐛 module 扫描器解析 partition import(`import :foo`)时,不再把当前 +- module 扫描器解析 partition import(`import :foo`)时,不再把当前 TU 自己的 partition 后缀拼进 logical name。 之前 `export module M:bar;` 里的 `import :foo;` 被解析成 `M:bar:foo` (没人 provide,产生 7 条 stale warning);现在正确解析为兄弟分区 @@ -5190,23 +5278,23 @@ mcpp 首个公开发版本。 ### 已具备的能力 -- ✅ 基础工程命令:`mcpp new` / `build` / `run` / `clean` / `test` -- ✅ C++23 模块(`import std` / `import foo.bar`)一等公民支持 -- ✅ 跨项目依赖:[mcpp-index](https://github.com/mcpp-community/mcpp-index) +- 基础工程命令:`mcpp new` / `build` / `run` / `clean` / `test` +- C++23 模块(`import std` / `import foo.bar`)一等公民支持 +- 跨项目依赖:[mcpp-index](https://github.com/mcpp-community/mcpp-index) 远程仓库、git、本地 path 三种来源 -- ✅ SemVer 约束:`"foo" = "^0.0.1"` / `"~1.2.0"` / `">=1, <2"` -- ✅ P1689 编译器驱动模块扫描 + ninja `dyndep` -- ✅ 跨项目 BMI 持久缓存 -- ✅ 私有 toolchain 沙盒(`mcpp toolchain install / default / list`), +- SemVer 约束:`"foo" = "^0.0.1"` / `"~1.2.0"` / `">=1, <2"` +- P1689 编译器驱动模块扫描 + ninja `dyndep` +- 跨项目 BMI 持久缓存 +- 私有 toolchain 沙盒(`mcpp toolchain install / default / list`), 跟系统 PATH 完全隔离;首次使用自动装 musl-gcc 默认工具链 -- ✅ 部分版本号支持(`mcpp toolchain install gcc 15` 自动选最高匹配) -- ✅ `mcpp pack` 三种自包含发布模式: +- 部分版本号支持(`mcpp toolchain install gcc 15` 自动选最高匹配) +- `mcpp pack` 三种自包含发布模式: - `static` — musl 全静态,单文件可分发 - `bundle-project`(默认)— 只 bundle 项目第三方 .so - `bundle-all` — 全自包含含 ld-linux + libc,附 `run.sh` wrapper -- ✅ `mcpp self {doctor,env,version,explain}` 自诊断 -- ✅ 下载 / 安装实时进度(速度、字节数、终端宽度自适应) -- ✅ 项目相对路径显示(`@mcpp/...`、project-relative) +- `mcpp self {doctor,env,version,explain}` 自诊断 +- 下载 / 安装实时进度(速度、字节数、终端宽度自适应) +- 项目相对路径显示(`@mcpp/...`、project-relative) ### 发布产物(GitHub Release) diff --git a/README.md b/README.md index a21efe28..e1003f73 100644 --- a/README.md +++ b/README.md @@ -357,7 +357,7 @@ data: [`standard-20260814-linux-x86_64`](bench/results/standard-20260814-linux-x An incorrect scheduling change fails silently rather than loudly, so the default is not changed on the evidence of a single machine. -📊 **[Methodology, pinned versions, and the full data → +**[Methodology, pinned versions, and the full data → `bench/README.md`](bench/README.md)** · [简体中文](bench/README.zh-CN.md) ## Platform Support @@ -374,25 +374,25 @@ the right toolchain payload is resolved and installed automatically. | Target | Convention toolchain | Status | |---|---|:---:| -| `x86_64-linux-gnu` | gcc *(Linux default)* or llvm | ✅ | -| `x86_64-linux-musl` | gcc 16, fully static | ✅ | -| `aarch64-linux-musl` | gcc 16, fully static — cross from x86_64 (qemu-verified) or native | ✅ | -| `x86_64-windows-gnu` | gcc 16 MinGW-w64 — native on Windows, cross from Linux (wine-verified) *(Windows default without Visual Studio)* | ✅ | -| `x86_64-windows-msvc` | `msvc@system` (detected VS/BuildTools) or llvm ¹ *(Windows default with Visual Studio)* | ✅ | -| `aarch64-macos` | llvm *(macOS default)* | ✅ | -| `riscv64-none-elf` | llvm 22 — bare metal, no OS; needs no per-host cross payload ² | ✅ | -| `riscv32-none-elf` | llvm 22 — bare metal, no OS; needs no per-host cross payload ² | ✅ | -| `thumbv6m-none-eabi` | llvm 22 — Cortex-M0/M0+/M1, bare metal ² | ✅ | -| `thumbv7m-none-eabi` | llvm 22 — Cortex-M3, bare metal ² | ✅ | -| `thumbv7em-none-eabihf` | llvm 22 — Cortex-M4F/M7F, hard float ² | ✅ | -| `thumbv8m.main-none-eabi` | llvm 22 — Cortex-M33/M55, soft float ² | ✅ | -| `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 22 — Cortex-A 32-bit, bare metal ² | ✅ | -| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22 — builds and links; no emulator run recorded | 🔄 | -| `riscv64-linux-musl` | — | 🔄 | -| `aarch64-linux-gnu` | — | 🔄 | -| `x86_64-macos` | — | 🔄 | - -✅ verified — CI builds **and executes** the artifact end-to-end (qemu/wine included) | 🔄 planned +| `x86_64-linux-gnu` | gcc *(Linux default)* or llvm | yes | +| `x86_64-linux-musl` | gcc 16, fully static | yes | +| `aarch64-linux-musl` | gcc 16, fully static — cross from x86_64 (qemu-verified) or native | yes | +| `x86_64-windows-gnu` | gcc 16 MinGW-w64 — native on Windows, cross from Linux (wine-verified) *(Windows default without Visual Studio)* | yes | +| `x86_64-windows-msvc` | `msvc@system` (detected VS/BuildTools) or llvm ¹ *(Windows default with Visual Studio)* | yes | +| `aarch64-macos` | llvm *(macOS default)* | yes | +| `riscv64-none-elf` | llvm 22 — bare metal, no OS; needs no per-host cross payload ² | yes | +| `riscv32-none-elf` | llvm 22 — bare metal, no OS; needs no per-host cross payload ² | yes | +| `thumbv6m-none-eabi` | llvm 22 — Cortex-M0/M0+/M1, bare metal ² | yes | +| `thumbv7m-none-eabi` | llvm 22 — Cortex-M3, bare metal ² | yes | +| `thumbv7em-none-eabihf` | llvm 22 — Cortex-M4F/M7F, hard float ² | yes | +| `thumbv8m.main-none-eabi` | llvm 22 — Cortex-M33/M55, soft float ² | yes | +| `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 22 — Cortex-A 32-bit, bare metal ² | yes | +| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22 — builds and links; no emulator run recorded | planned | +| `riscv64-linux-musl` | — | planned | +| `aarch64-linux-gnu` | — | planned | +| `x86_64-macos` | — | planned | + +verified — CI builds **and executes** the artifact end-to-end (qemu/wine included) | planned > Linux release binaries are fully static musl builds for x86_64 and aarch64 > (`x86_64-linux-musl` and `aarch64-linux-musl`). diff --git a/docs/01-examples.md b/docs/01-examples.md index 6365027a..1935929f 100644 --- a/docs/01-examples.md +++ b/docs/01-examples.md @@ -27,6 +27,8 @@ examples. | 02 | [`examples/02-with-deps`](../examples/02-with-deps/) | Adds the `mcpplibs.cmdline` dependency to parse command-line arguments | `[dependencies]`, SemVer, `mcpp.lock` | | 03 | [`examples/03-pack-static`](../examples/03-pack-static/) | Produces a fully static release package via `mcpp pack --mode static` | `[target.]` and `[pack]` configuration | | 08 | [`examples/08-build-rules`](../examples/08-build-rules/) | Two rule packages and a project that uses both | `host-module = true`, `[build-dependencies]`, `mcpp::action` with `role = "check"` | +| 09 | [`examples/09-cuda-kernel`](../examples/09-cuda-kernel/) | A CUDA kernel behind a seam module, with a CPU fallback | `accel`, constrained source globs, `mcpp::action` with `role = "object"`, `cfg(accelerator = …)` | +| 10 | [`examples/10-vulkan-compute`](../examples/10-vulkan-compute/) | The same computation as a Vulkan compute shader, on a GPU or on the CPU | `mcpp.rules.spirv` from `mcpp:plugins`, `mcpp::action` with `role = "source"`, generated headers, a software driver as a payload | ## Suggested Reading Order diff --git a/docs/02-pack-and-release.md b/docs/02-pack-and-release.md index 23a1f37b..2eed3c6a 100644 --- a/docs/02-pack-and-release.md +++ b/docs/02-pack-and-release.md @@ -89,10 +89,10 @@ Declare those as run-phase capability requirements (§2.11 of | Mode | Program needing a host-provided capability | |---|---| -| `system` | ✅ | -| `vendored` (default) | ✅ **the right default for these** | -| `self-contained` | ❌ **refused at pack time** | -| `static` | ❌ **refused at pack time** | +| `system` | yes | +| `vendored` (default) | **the right default for these** | +| `self-contained` | **refused at pack time** | +| `static` | **refused at pack time** | The two refusals are the same fact: **a bundle that carries its own libc cannot consume a library the host supplies.** That `.so` arrives with its own diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index 8bacfb1b..43affda1 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -108,7 +108,7 @@ $ mcpp build not your gcc@16.1.0 — this project only ``` -⭐ **Nothing is written.** Not `~/.mcpp/config.toml`, not the project's +**Nothing is written.** Not `~/.mcpp/config.toml`, not the project's `mcpp.toml`. The requirement is a property of this build, so it applies to this build; the machine's default stays whatever it was, for every other project. The version comes from what is already installed — the same resolution @@ -170,7 +170,7 @@ vocabulary, in four statuses: | `via dependency graph` | the compiler is here; the target's system is not, and packages can supply it | depend on an implementation of the target's kernel interface and C library | | `planned` | registered in the vocabulary, not yet shipped | — | -⚠️ **A target absent from this block cannot be built here at all** — and that +**A target absent from this block cannot be built here at all** — and that is a narrower statement than it used to be. Until mcpp 2026.8.25.2 the block listed only what a payload served, so a target whose system comes from a dependency graph was missing while the same host produced real artefacts for @@ -543,7 +543,7 @@ $ mcpp build --target riscv64-none-elf # [toolchain] default = "gcc@…" error: target 'riscv64-none-elf' cannot be emitted by 'gcc@16.1.0'. ``` -⭐ **Both refusals are decided where the decision is made**, not left to the +**Both refusals are decided where the decision is made**, not left to the compiler. Before 2026.8.26.1 the first ran the whole build and died at the link on `crtbeginT.o (bare name)`, and the second produced `g++: error: unrecognized argument in option '-mabi=lp64d'` — a message about an diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 57986961..01ac9f3b 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -222,10 +222,12 @@ the package/feature boundary, not on an individual target. > would leave nothing to compile for that device and say so only at the link). > Under `--no-accel` the glob is left out, which is how one project yields its > CPU-only variant. Under an `--accel` that does not cover the constraint the -> build is refused naming both (`accel-mismatch`). Device-kind files (`.cu`, -> `.hip`) the effective set matches are never compiled by the engine; they -> reach the build program as `MCPP_DEVICE_SOURCES`, where the rule package the -> project imports turns each into an `mcpp::action`. +> build is refused naming both (`accel-mismatch`). Device-kind files the +> effective set matches — CUDA and HIP, the GLSL stages, HLSL, OpenCL C and +> Metal, listed in full in [20 — Heterogeneous Builds](20-heterogeneous-builds.md) — are never +> compiled by the engine; they reach the build program as +> `MCPP_DEVICE_SOURCES`, where the rule package the project imports turns each +> into an `mcpp::action`. ```toml [build] @@ -278,7 +280,7 @@ relinked, whether something else in the process already provides that library. / the executable's own directory (PE) finding it again after the build directory moves. -⚠️ **This is not `[target.].linkage`** (§2.7.1). That key answers the +**This is not `[target.].linkage`** (§2.7.1). That key answers the same-sounding question about the **C library** (a musl `-static` link, MSVC's `/MT`). The two are not independent, and the direction matters: a fully static image has no interpreter, so it cannot load a shared object at all. On a target @@ -311,7 +313,7 @@ way mcpp's build of a package and a third party's copy of the same library can resolve to **one file** instead of two — which a package cannot state if declaring it forces the package to stop being consumable as a static library. -⚠️ A descriptor that writes `soname` on a non-shared target cannot be read by +A descriptor that writes `soname` on a non-shared target cannot be read by mcpp releases before 2026.8.28.2 — the whole manifest fails to load, not just the key. Publishing one to an index therefore waits for that floor to move. @@ -360,7 +362,7 @@ reason it has no way to see. ```toml [build] -# ⚠️ The relative ORDER of the two kinds is load-bearing: the internal overlay +# The relative ORDER of the two kinds is load-bearing: the internal overlay # must precede the public headers for this package's own build. That is why # this is a SUBSET of `include_dirs` rather than a second list — two arrays # cannot express one order. @@ -666,7 +668,7 @@ toolchain's libraries first. both the C and C++ compile channels. It reaches every TU in the package — module interface units included — so it also reaches the compiler's own P1689 module scan. -> ⚠️ **It does not make a macro-guarded `import` acceptable.** mcpp runs its own +> **It does not make a macro-guarded `import` acceptable.** mcpp runs its own > lexical pre-scan before the compiler ever sees the file, and that scanner > rejects an `import` inside **any** `#if` / `#ifdef` block without evaluating the > condition: @@ -817,7 +819,7 @@ package that also publishes device builds is selected. The value is compared against the `accel` field of any prebuilt artifact the build consumes, and a build asking for none is satisfied by every artifact. See -[20 — Accelerators](20-accelerators.md). +[20 — Heterogeneous Builds](20-heterogeneous-builds.md). ### 2.4 `[lib]` — Library Root Module Convention @@ -924,15 +926,15 @@ selector normalizes to exactly one identity: There is no ordered fallback or fuzzy, index-wide search by short name: ```toml -# ✅ Correct — dotted selector +# Correct — dotted selector [dependencies] chriskohlhoff.asio = "1.38.1" -# ✅ Correct — namespace sub-table (preferred for several packages from one org) +# Correct — namespace sub-table (preferred for several packages from one org) [dependencies.chriskohlhoff] asio = "1.38.1" -# ❌ Wrong — a bare name never reaches the `chriskohlhoff` namespace +# Wrong — a bare name never reaches the `chriskohlhoff` namespace [dependencies] asio = "1.38.1" ``` @@ -1210,7 +1212,7 @@ sysroot = "xim:newlib-riscv@4.4" # a different C library sysroot = "" # no C library at all ``` -⚠️ **An absent key and an empty one are different answers.** Absent inherits the +**An absent key and an empty one are different answers.** Absent inherits the target table's C library. Present-and-empty is the **zero-libc tier**: no C library is resolved, no include or library path is added, and the link carries only what the project and its dependencies supply. `#include ` stops @@ -1227,7 +1229,7 @@ A build program can ask which C library **payload** supplies the sysroot: Both are empty on the zero-libc tier. See [13 — Bare-Metal and Freestanding Targets](13-baremetal.md). -⚠️ **That is not the same question as "which C library did the target side +**That is not the same question as "which C library did the target side resolve to".** `target_libc()` names the payload mcpp installed, and that value is an *input* to target-side resolution — a package in the dependency graph can supply the C library instead, in which case the resolved `c-abi` is not what @@ -1613,7 +1615,7 @@ error: `toolkitnew` requires cuda.driver >= 13.0, and this machine has 12.4. version; `cuda.driver` is data passing through, and a backend mcpp has never heard of compares the same way. -⚠️ **A floor nobody answered is silent.** A machine that never declared what it +**A floor nobody answered is silent.** A machine that never declared what it has is not a machine that fails the floor — it is one nobody asked. Turning "we do not know" into "no" is the failure mode this exists to avoid, and it is asserted directly: `tests/e2e/603_version_floor.sh` builds a project whose floor @@ -1644,7 +1646,7 @@ backend-openblas = { implies = ["use_blas"] } compat.openblas = "0.3" ``` -⚠️ **`"^0.3.0"` and not `"0.3.x"` or `"0.3"`.** Measured against a package the +**`"^0.3.0"` and not `"0.3.x"` or `"0.3"`.** Measured against a package the index certainly carries, with a **build** as the criterion: | Written | Result | @@ -1654,12 +1656,12 @@ index certainly carries, with a **build** as the criterion: | `cmdline = "0.0"` | resolves, then `install path missing after fetch` | | `cmdline = "0.0.x"` | `E_NOT_FOUND`, naming the package — which exists | -⭐ The three outcomes are worth distinguishing, because two weaker criteria each +The three outcomes are worth distinguishing, because two weaker criteria each admit a form that does not work: "no `E_NOT_FOUND`" admits the two-segment prefix, and "resolves" admits it as well. Only building against the real index settles it. -⚠️ This matters more here than in `[dependencies]`. A feature whose +This matters more here than in `[dependencies]`. A feature whose implementation cannot be fetched is a feature that does not exist, and a project using a **path** dependency during development never consults the index — so the failure appears only after publication, to somebody else. @@ -1988,7 +1990,7 @@ statement of intent and a CI-matrix hint, shown by `mcpp why`, never a gate. Distinct from an artifact's `accel` field on purpose. A declaration is written by hand and may be aspirational; `accel` is measured from the build that produced a binary and is what a consumer is refused against. See -[20 — Accelerators](20-accelerators.md). +[20 — Heterogeneous Builds](20-heterogeneous-builds.md). ### 2.13 `[xlings]` — the project's environment @@ -2244,6 +2246,37 @@ internal fork are all legitimate and indistinguishable from here. The lib root must be at `src/.cppm` (or wherever `[lib] path` points); a missing one is reported as *"host module 'x': no interface unit at …"*. +**A package may offer several rules, selected by features** (mcpp 2026.9.5.3+). +Every module interface unit among the package's resolved `[build] sources` — +including the sources a feature adds — is compiled as a host module under the +name it declares, the lib root first. A feature unit may import the lib root; +units are otherwise compiled alone, so they import `std`, `mcpp` and nothing +else. Only listed sources take part: the inferred `src/**` of a package that +declares no `sources` is not consulted, so a rule package published before this +release exposes exactly what it exposed then. + +```toml +# the collection's manifest +[build] +sources = ["src/plugins.cppm"] # export module mcpp.plugins; + +[features] +rules-cuda = { sources = ["rules/cuda.cppm"] } # export module mcpp.rules.cuda; +rules-spirv = { sources = ["rules/spirv.cppm"] } # export module mcpp.rules.spirv; +``` + +```toml +# a consumer +[dependencies.mcpp] +plugins = { version = "0.1.0", features = ["rules-spirv"], host-module = true } +``` + +The module set is the feature set: a unit whose feature is not active is not +compiled, and importing it fails as an unknown module. `mcpp:plugins` is the +collection the mcpp project maintains (repository `mcpp-community/mcpp-plugins`); +its members are named `mcpp.rules.` for rule packages and `mcpp.tools.` +for build-time utilities. + *Build-time only:* a `host-module = true` dependency is **not** compiled into or linked with the target, and neither is anything it depends on. It exists to run during `build.mcpp` and nowhere else. (Before 2026.8.5.2 it was also built diff --git a/docs/07-build-mcpp.md b/docs/07-build-mcpp.md index d8f7c909..a552d875 100644 --- a/docs/07-build-mcpp.md +++ b/docs/07-build-mcpp.md @@ -52,7 +52,7 @@ is ignored, so diagnostics may be logged freely. | `mcpp:source=` *(0.0.100+)* | select a **pre-existing** source file into the build (absolute, or relative to the package root). Same downstream effect as `generated=`; use it for files the program *chose* (payload/vendored tree) rather than wrote — e.g. a per-target source selection over a large tarball | | `mcpp:include-dir=` *(0.0.100+)* | add a **private** include directory (`-I`) for this package's own TUs (absolute, or relative to the package root; normalized). Replaces the `cxxflag=-I` + `cflag=-I` double emission | | `mcpp:include-dir-after=` *(0.0.100+)* | like `include-dir`, but searched **after** the system directories (`-idirafter`) — for payload trees that shadow system headers | -| `mcpp:runner=` *(2026.8.19.2+)* | one argv token of the command that EXECUTES this build's artifact, when the host cannot. Emitted once per token, in order; the artifact path is appended (or substituted for `{}`). Reaches the **consumer**. ⚠️ Emit the executable as an ABSOLUTE path, and only **one** dependency may supply it | +| `mcpp:runner=` *(2026.8.19.2+)* | one argv token of the command that EXECUTES this build's artifact, when the host cannot. Emitted once per token, in order; the artifact path is appended (or substituted for `{}`). Reaches the **consumer**. Emit the executable as an ABSOLUTE path, and only **one** dependency may supply it | | `mcpp:link-script=` *(2026.8.19+)* | link with this **linker script** (`-T`; relative resolves against the package root, and the emitted path is absolute because the link runs in the build directory). Reaches the **consumer**, unlike `include-dir` — a board's memory layout is the one thing a consumer cannot write for itself | | `mcpp:warning=` *(2026.8.21.2+)* | say something to the user and **keep going**. The one directive that changes no compile line, no link line and no source set. Survives the build cache — see below | | `mcpp:fact==` *(2026.9.5.2+)* | state something the program **established about the machine** (`cuda.driver=12.4`). Compared against floors before anything is compiled; see below | @@ -127,7 +127,7 @@ if (const char* dir = mcpp::xpkg_dir("xim", "qemu-riscv"); dir && *dir) { } ``` -⚠️ **This exists because the alternatives are worse, and both were tried.** A +**This exists because the alternatives are worse, and both were tried.** A note on stderr printed nothing on a successful build. Exiting non-zero would be wrong too: `mcpp build` has no need of an emulator, and failing a build that is correct trades a missing sentence for a broken command. @@ -142,12 +142,12 @@ depends on it."* For an error, exit non-zero; that output is printed already. workspace several programs may speak and the reader needs to know which manifest to open. -⭐ **It survives the build cache.** A build program's result is cached, and a +**It survives the build cache.** A build program's result is cached, and a cache hit does not re-run it — so an advisory that lived only on the run path would appear on a project's first build and never again, which reads as *"the condition was resolved"*. mcpp replays it on every hit. -⚠️ **A whole-project no-op build prints nothing at all, including this.** When +**A whole-project no-op build prints nothing at all, including this.** When there is nothing to do the build never reaches the `build.mcpp` stage — it also does not report which target it built or which sources it inferred. Touch a source and the advisory returns. @@ -175,14 +175,14 @@ cleanly, links cleanly and fails at first use with a message naming neither side. The rule package that resolved the runtime knows both numbers before the first compile. -⚠️ **Neither string means anything to the engine.** `cuda.driver` is data +**Neither string means anything to the engine.** `cuda.driver` is data flowing through; the engine reads a name, a relation and a version, and a second backend needs no engine change. The spelling of a fact matches what a package could also have declared statically in `[runtime] provides`, and a floor matches `[[runtime.requirements]]` with `kind = "version-floor"`: the two channels land in one list. -⚠️ **A fact is cached with the program's other output** and replayed on a +**A fact is cached with the program's other output** and replayed on a cache hit. Declare what would change it — `rerun_if_changed` on the library the version was read from — or the fact outlives the machine it described. @@ -204,11 +204,11 @@ anyway, **it wins** — swapping `-bios default` for `-bios none -semihosting` while debugging is a legitimate thing to want — and mcpp says which dependency it overrode. -⚠️ **Emit the executable as an absolute path.** A bare name resolves through +**Emit the executable as an absolute path.** A bare name resolves through `PATH` to a shim that dispatches against its own owner home, which is not necessarily the home this build uses. -⚠️ **Exactly one dependency may supply a runner.** Two board-support packages +**Exactly one dependency may supply a runner.** Two board-support packages both claiming to know how to run the artifact is a configuration error, and mcpp reports it naming both rather than merging them into an argv that is neither one's. @@ -225,7 +225,7 @@ freestanding standard-library subset — or a file inside the target's C library — a linker script, for a board-support package — asks for the directory rather than declaring a dependency on the thing that provides it. -⚠️ The difference is not cosmetic. Declaring `xim:llvm` pins a package to one +The difference is not cosmetic. Declaring `xim:llvm` pins a package to one standard-library implementation; declaring `xim:picolibc-riscv@1.8.12` pins it to one C library, one architecture and one version. Neither is a property of a package whose content is implementation-neutral. Asking follows whatever @@ -260,7 +260,7 @@ target. Forwarding them — `--sysroot=` and `-B`, through whateve the outer tool spells host options with — makes the second compiler see what the first one sees. -⚠️ **Not `sysroot_dir()`.** That answers a question about the target's *tier* +**Not `sysroot_dir()`.** That answers a question about the target's *tier* and is empty on a hosted target, which is exactly the case this pair exists for. Either of these two is empty when mcpp passes no such flag. @@ -289,7 +289,7 @@ It is an interface rather than a documented path because the alternative is a build program encoding `/data/xpkgs/-x-/`, which is store internals mcpp is free to change — the same reason `dep_dir` exists. -⚠️ A **pinned** reference resolves to exactly that version or to nothing. A +A **pinned** reference resolves to exactly that version or to nothing. A build that asked for `1.8.12` and silently got `1.9.0` is an answer only discovered later, in the artifact. @@ -509,7 +509,7 @@ The package cannot handle this itself, and it is worth knowing why — the obvious guard does not compile: ```cpp -if constexpr (requires { mcpp::runner("qemu"); }) // ✗ hard error when absent +if constexpr (requires { mcpp::runner("qemu"); }) // hard error when absent mcpp::runner("qemu"); ``` @@ -587,7 +587,7 @@ PATH=: so a bare command name in a build program resolves inside the environment the project named, on every machine that builds it. -⚠️ **Only for projects that declare one.** A project with no `[xlings].subos` +**Only for projects that declare one.** A project with no `[xlings].subos` gets the `PATH` mcpp was started with, byte for byte. A shared directory in front of every project would make what a build sees depend on what else had been installed on that machine — two projects on one machine would agree with @@ -598,7 +598,7 @@ Why it is a prefix and not a replacement: a build program legitimately calls makes the declared environment the default answer; the host stays reachable behind it. -⚠️ **`command -v` answers about the machine, not about this build.** Before +**`command -v` answers about the machine, not about this build.** Before this, a program asking `PATH` for a declared tool could get an unrelated one — measured on `qemu-system-riscv64`, where the answer was a shim that reports "is not installed in this subos" when executed, while a working copy sat in the @@ -622,6 +622,26 @@ The guidance below generalises from `mcpplibs.grpcgen`, the first such package, with each of its traits judged individually. It is guidance and not a rule because none of it admits a criterion the engine could check. +**The module name is declared by the rule's source, and `mcpp.*` is reserved.** +A host module is registered under the name its interface unit declares, not +under the package name, so `export module mcpp.rules.spirv;` is what a consumer +then imports. Official plugins live in one package, `mcpp:plugins` (repository +`mcpp-community/mcpp-plugins`): rule packages are named `mcpp.rules.`, +build-time utilities `mcpp.tools.`, and each member is selected by a feature +of that package (see [`host-module = true`](05-mcpp-toml.md)). `mcpp.build.*` +is the engine's own module family and is not used for plugins. The engine +cannot tell who is official, so it keys the check on the package *namespace* +and warns when the two disagree — + + warning: build rule 'mcpplibs.plugins' declares the module + 'mcpp.rules.spirv'; the 'mcpp.' prefix is reserved for rules maintained by + the mcpp project. + +Nothing breaks; the name claims an origin the package does not have. A rule +outside the project picks its own prefix. `examples/09-cuda-kernel` and +`examples/10-vulkan-compute` consume `mcpp.rules.cuda` and `mcpp.rules.spirv` +from `mcpp:plugins`, the way any project does. + **Layers must not have a cliff, and each layer must be the composition of the one below it.** `generate_all(opt)` *is* `submit(plan_all(opt))`, and `.grpc = true` *is* `.plugins = {cpp()}`. Past two knobs a consumer who cannot diff --git a/docs/11-machine-output.md b/docs/11-machine-output.md index 1b0cd536..ce19777f 100644 --- a/docs/11-machine-output.md +++ b/docs/11-machine-output.md @@ -114,7 +114,7 @@ cannot produce. The full mapping across all of mcpp is | 70 | internal error (uncaught exception) | | 127 | unknown command | -⚠️ **`1` can arrive with an envelope on stdout.** `mcpp xpkg parse` reports a +**`1` can arrive with an envelope on stdout.** `mcpp xpkg parse` reports a descriptor that violates the name form as JSON *and* exits 1: the document is the answer, and the exit code says the answer is a rejection. §1 still holds — parse stdout, do not branch on the code — but a client that treats any non-zero @@ -291,7 +291,7 @@ installation, which is located on the machine rather than installed by mcpp. A target row is `{target, note, toolchain, pin, status, default}`, and `status` is one of `installed` / `available` / `via dependency graph` / `planned`. -⚠️ **`toolchain` and `pin` are not the same field twice.** `toolchain` is what +**`toolchain` and `pin` are not the same field twice.** `toolchain` is what the row is associated with — the installed payload on an installed row, the convention on a vocabulary row. `pin` is only ever the target table's convention, and is empty for a row that has none. `x86_64-linux-gnu` has an @@ -316,7 +316,7 @@ It resolves and reports; it does not build. `data` is: | `cLibrary` | `{mode, path, origin, suppliesTarget}` — `mode` is `sysroot` / `payload-first` / `none`; `origin` is `payload` / `subos` / `host` / `none` | | `layers[]` | the five target-side layers: `{layer, interface, impl, origin, subset}` | -⭐ **`compiler.chosenBy` answers "why this one".** `{origin, requiredBy, +**`compiler.chosenBy` answers "why this one".** `{origin, requiredBy, replaced}` — `origin` is the same phrase the build's status line uses (`[toolchain] in mcpp.toml`, `your default`, `target default`, `required by the dependency graph`, `first-run default`). `requiredBy` names the @@ -333,7 +333,7 @@ the spec that was displaced; both are empty when nothing was. Without it a consumer asking *why* would have to parse the status line — the substring matching this document exists to remove. -⚠️ **`cLibrary` and `layers[].c-abi` answer two questions, and `suppliesTarget` +**`cLibrary` and `layers[].c-abi` answer two questions, and `suppliesTarget` says which one governs.** `cLibrary` describes the *payload's* link model — the search paths a payload-supplied C library would use. `layers[].c-abi` describes the *build*. When a dependency supplies the C library the two diverge, and @@ -351,7 +351,7 @@ A field was added rather than `cLibrary` renamed or `mode` widened, because §7 promises that fields are added and never removed and that a field's meaning never changes. -⚠️ **`layers[].interface` changed VALUE for a payload-supplied glibc in +**`layers[].interface` changed VALUE for a payload-supplied glibc in 2026.9.1.1** — from `gnu` to `glibc`, and on Windows from `gnu` to `ucrt`. The field's meaning is unchanged (it still names the implementation), so §7 holds; what changed is that it stopped reporting the triple's env segment, which is a @@ -361,7 +361,7 @@ listed, and a package may compare against them in a `cfg(c-abi = …)` predicate A client keying on the literal `gnu` needs updating; `musl`, `picolibc` and `libSystem` are unaffected. -⭐ **`reason` is a token, not a sentence.** The refusal's message is still +**`reason` is a token, not a sentence.** The refusal's message is still written for a person and still names the target, the rule and the way out — but a program classifying the outcome reads `reason`: @@ -382,11 +382,11 @@ a program classifying the outcome reads `reason`: | `accel-mismatch` | a `[build] sources` entry is constrained to a device set this build does not cover | | `other` | a refusal whose branch has not been given a token yet | -⚠️ **Exit 0 whenever the question was answered, including "refused".** "Would +**Exit 0 whenever the question was answered, including "refused".** "Would this build, and if not why" is answered successfully by "no, because the row's pin is a capability". A non-zero exit means the query itself could not run. -⚠️ **Its effects are broad on purpose.** `--protocol-version` lists `network`, +**Its effects are broad on purpose.** `--protocol-version` lists `network`, `write-global-cache` and `exec-build-script` for this command: the answer comes from the same resolution a build performs, which may fetch packages, install a payload and run a dependency's build program. A client gates on that table @@ -428,7 +428,7 @@ Summary record, `{"summary": {...}}`: | `not_run_reason` | the reason shared by all of them, or `""` | | `elapsed_ms`, `build_ms`, `run_ms` | wall time, split | -⚠️ **`not_run` is neither `pass` nor `run_fail`, and the exit code says so +**`not_run` is neither `pass` nor `run_fail`, and the exit code says so (2026.9.2.1).** A test is `not_run` when this host cannot load its artifact (`Exec format error` on a cross target with no runner declared), or when the declared `[target.].runner` could not be found or started. The diff --git a/docs/12-binary-distribution.md b/docs/12-binary-distribution.md index c54f51da..1feaa7fb 100644 --- a/docs/12-binary-distribution.md +++ b/docs/12-binary-distribution.md @@ -282,7 +282,7 @@ is therefore not consulted, and the program dies with error while loading shared libraries: libstdc++.so.6: cannot open shared object file ``` -⚠️ **`$ORIGIN` is not the fix.** Measured on a real package with the build +**`$ORIGIN` is not the fix.** Measured on a real package with the build machine's store made unreachable: | state on the shipped `.so` | consumer's `DT_RPATH` inherited? | result | @@ -320,16 +320,16 @@ index; run ranlib to add one`. | | status | |---|---| -| `kind = "lib"` (static) | ✅ every target, tested on all three | -| `kind = "shared"` on Linux/ELF | ✅ — the package carries both the link name and the SONAME, and no build-machine loader path | -| `kind = "shared"` on PE / MinGW (`*-windows-gnu`) | ✅ — the package carries the `.dll` **and** its import library | -| `kind = "shared"` on Mach-O (`*-macos`) | ✅ — install name is `@rpath/`, so the `.dylib` relocates. `LC_RPATH` is reported, not yet rewritten | -| `kind = "shared"` on PE / MSVC (`*-windows-msvc`) | ✅ — mcpp generates the `.def`; see below | -| `kind = "shared"` on `*-musl` | ❌ a musl target links statically | -| one package carrying two ABIs for the same triple (gcc **and** clang) | ❌ leg selection is `cfg(arch/os/env)`; publish one package per ABI | -| shipping prebuilt BMIs | ❌ not attempted; BMIs are compiler-build-exact | -| bundling dependencies into the package | ❌ declare them instead (above) | -| consuming a package with **native `cl.exe`** | ✅ — via the neutral link intent; see below | +| `kind = "lib"` (static) | every target, tested on all three | +| `kind = "shared"` on Linux/ELF | — the package carries both the link name and the SONAME, and no build-machine loader path | +| `kind = "shared"` on PE / MinGW (`*-windows-gnu`) | — the package carries the `.dll` **and** its import library | +| `kind = "shared"` on Mach-O (`*-macos`) | — install name is `@rpath/`, so the `.dylib` relocates. `LC_RPATH` is reported, not yet rewritten | +| `kind = "shared"` on PE / MSVC (`*-windows-msvc`) | — mcpp generates the `.def`; see below | +| `kind = "shared"` on `*-musl` | a musl target links statically | +| one package carrying two ABIs for the same triple (gcc **and** clang) | leg selection is `cfg(arch/os/env)`; publish one package per ABI | +| shipping prebuilt BMIs | not attempted; BMIs are compiler-build-exact | +| bundling dependencies into the package | declare them instead (above) | +| consuming a package with **native `cl.exe`** | — via the neutral link intent; see below | ### Exports on the MSVC ABI @@ -467,17 +467,17 @@ The e2e suite gates each test on host capabilities, so "the suite is green" and | claim | linux | macOS | windows | |---|---|---|---| -| layout, both interface modes, closure, the two gates, workspace root, named target, `sources = []`, bare-triple predicate | ✅ | ✅ | ✅ | -| multi-target package, two legs one artifact name (`gnu` + `musl`) | ✅ | *impossible* | — | -| multi-target package, two legs **two** artifact names (`msvc` + `mingw`) | — | *impossible* | ✅ | -| multi-target package crossing an OS boundary (a PE leg) | ✅ | — | — | -| `lib.exe /REMOVE:` really removing | — | — | ✅ | -| PE shared library: build, pack, link, run | ✅ (wine) | — | — | -| Mach-O shared library relocating out of its build tree | — | ✅ | — | -| MSVC refusing `kind = "shared"` for the export reason | — | — | ✅ | +| layout, both interface modes, closure, the two gates, workspace root, named target, `sources = []`, bare-triple predicate | yes | | yes | +| multi-target package, two legs one artifact name (`gnu` + `musl`) | yes | *impossible* | — | +| multi-target package, two legs **two** artifact names (`msvc` + `mingw`) | — | *impossible* | yes | +| multi-target package crossing an OS boundary (a PE leg) | yes | — | — | +| `lib.exe /REMOVE:` really removing | — | — | yes | +| PE shared library: build, pack, link, run | (wine) | — | — | +| Mach-O shared library relocating out of its build tree | — | yes | — | +| MSVC refusing `kind = "shared"` for the export reason | — | — | yes | | a released mcpp consuming a package this one produced | local only | local only | local only | -| a packed `.so` carries no build-machine loader path, **and the guard can see the defect when it is put back** | ✅ | — | — | -| a stripped static archive still links; a stripped shared library still loads; `--no-strip` / `[pack] strip` / `--debug-symbols` from both sides | ✅ | — | — | +| a packed `.so` carries no build-machine loader path, **and the guard can see the defect when it is put back** | yes | — | — | +| a stripped static archive still links; a stripped shared library still loads; `--no-strip` / `[pack] strip` / `--debug-symbols` from both sides | yes | — | — | | the ELF editor on ELF32 and big-endian | unit test | unit test | unit test | *impossible* is not a gap: a macOS host can serve exactly one target diff --git a/docs/13-baremetal.md b/docs/13-baremetal.md index cadc744a..9a62ca5c 100644 --- a/docs/13-baremetal.md +++ b/docs/13-baremetal.md @@ -49,7 +49,7 @@ status.** `SYS_EXIT` (`0x18`) takes its reason code in `r1` *directly*; the 32-bit `r1` cannot carry both a reason and a status. Passing the block to `0x18` prints everything correctly and then reports the **wrong** exit status. -⚠️ This is an *AArch32* fact and applies to M-profile as much as to A-profile. +This is an *AArch32* fact and applies to M-profile as much as to A-profile. Measured twice: an ARMv7-A image exiting 0 reported 1, and an `openarch` Cortex-M example printed `both tasks observed preemption` and exited 1 — every assertion on its output passed. A board that only checks what it printed cannot @@ -70,7 +70,7 @@ The `eabi`/`eabihf` suffix is the float ABI, and clang derives it from the triple without help: measured on llvm 22.1.8, `thumbv7em-none-eabi` yields `-mfloat-abi soft` and `thumbv7em-none-eabihf` yields `hard`. -⚠️ **The float ABI does not settle whether the FPU is used.** It governs how +**The float ABI does not settle whether the FPU is used.** It governs how floating-point values cross a function boundary, not what the compiler may emit inside one, and the `thumbv7em` architecture implies FPv4-SP. Measured: under the soft-float ABI clang still emits `vmul.f32` for a float multiply. On a @@ -97,17 +97,17 @@ is undefined. That costs nothing when the C library is a prebuilt archive and the target has megabytes; a Cortex-M part has kilobytes, and without dead-section elimination every image would carry the whole of the C library. -⚠️ **A linker script becomes load-bearing in a new way.** An interrupt vector +**A linker script becomes load-bearing in a new way.** An interrupt vector table is referenced by nothing — the hardware reads it by address — so `--gc-sections` collects it. A board's script must say `KEEP(*(.vectors))`. Measured: with the `KEEP` present, a function nothing calls is dropped, the table survives, and the image boots. -⚠️ The last two rows default to no C library, and that is a statement rather +The last two rows default to no C library, and that is a statement rather than an omission: the first consumer of both rows — the `openarch` layer of machine mechanism — references no C library symbol, and if no row defaulted to this tier there would be nothing demonstrating the tier works. -⭐ **A build for those rows is declarable, not absent** (mcpp 2026.8.21.3+). +**A build for those rows is declarable, not absent** (mcpp 2026.8.21.3+). `xim:picolibc-aarch64` and `xim:picolibc-x86` are in the index; a project that wants one names it the same way it would choose a different one: @@ -126,7 +126,7 @@ that can install the LLVM payload can produce an image for any of the four. ### The x86_64 row is not four strings -⚠️ **A target row is normally an entry in two tables and nothing else. This one +**A target row is normally an entry in two tables and nothing else. This one needed engine code, and the reason is a property of clang rather than of the instruction set.** @@ -493,7 +493,7 @@ which emulator boots it are not C library facts: riscv-virt-rt = { version = "0.5.0", features = ["nolibc"] } ``` -⚠️ `std-freestanding-nolibc` is what that feature resolves to, and adding it +`std-freestanding-nolibc` is what that feature resolves to, and adding it **directly** alongside a C library fails silently rather than loudly. A C library ships as an archive, and an archive member is pulled only while the symbol is still undefined; a @@ -651,7 +651,7 @@ mcpp installs it on first use — and it obeys the same two knobs: under `--offline` / `MCPP_OFFLINE` or `MCPP_NO_AUTO_INSTALL` mcpp refuses instead, naming the packages so they can be installed out of band. -⚠️ **A build program still must not assume the directory exists.** Provisioning +**A build program still must not assume the directory exists.** Provisioning runs for the package that DECLARES the deps; a build program can be reached through paths where that has not happened — a dependency of a project that declares nothing, an environment where the knobs above refused — so diff --git a/docs/14-target-side.md b/docs/14-target-side.md index d018a2f3..78fa2849 100644 --- a/docs/14-target-side.md +++ b/docs/14-target-side.md @@ -272,14 +272,14 @@ report prints. They combine with the triple keys under `all`/`any`/`not`: cxxflags = ["-D_GNU_SOURCE"] ``` -⚠️ **A layer names the library, not the triple's env segment.** They coincide +**A layer names the library, not the triple's env segment.** They coincide for `musl` and diverge for `gnu`: on Linux that segment asks for glibc, and on Windows it names the MinGW flavour of the toolchain, whose C runtime is the same UCRT the MSVC flavour links. The spelling is `c-abi = "glibc"`, never `c-abi = "gnu"`; the request, as opposed to the answer, is `env = "gnu"` — a different question (`docs/spec/target-side.md` §3.4). -⚠️ **`env` and `c-abi` are not interchangeable.** `env` is what the triple +**`env` and `c-abi` are not interchangeable.** `env` is what the triple *asked* for; `c-abi` is what the graph and the payload *answered*. An `openkal-musl` in the dependency graph supplies musl under an `x86_64-linux-gnu` triple, and only `c-abi` sees that. diff --git a/docs/16-the-target-triple.md b/docs/16-the-target-triple.md index a5137790..78f5a30f 100644 --- a/docs/16-the-target-triple.md +++ b/docs/16-the-target-triple.md @@ -217,7 +217,7 @@ x86_64-w64-mingw32-g++ → x86_64-w64-mingw32 (emits nothing else) clang++ → x86_64-unknown-linux-gnu, and --target= changes it ``` -⭐ This is why the prebuilt system passes no `--target`: the payload's compiler +This is why the prebuilt system passes no `--target`: the payload's compiler is named after the one target it can emit, and choosing a target means choosing a payload. It is also why the build-time system needs only one compiler. @@ -229,7 +229,7 @@ MinGW's own triple is `x86_64-w64-mingw32`: |---|---|---| | arch | `x86_64` | | | vendor | `w64` | the project is `mingw-w64`, distinguishing it from the stalled original `mingw32` | -| os | **`mingw32`** | ⭐ MinGW puts *itself* in the OS field | +| os | **`mingw32`** | MinGW puts *itself* in the OS field | | env | (absent) | three fields is the whole name | The convention descends from autoconf's `config.guess`, where the OS field names @@ -253,7 +253,7 @@ LLVM x86_64 - unknown - windows - gnu ^vendor ^os ^env ``` -⭐ **`mingw32` is split out of the OS field into `windows` plus `gnu`.** The +**`mingw32` is split out of the OS field into `windows` plus `gnu`.** The value `gnu` exists because LLVM needed a name for the half that was left over. Its meaning is "the MinGW/Itanium ABI lineage" and it has never meant "the C library is glibc" on Windows — the same word carries different duties under @@ -276,7 +276,7 @@ Target x86_64-windows-gnu → x86_64-w64-windows-gnu ^ mcpp ^ LLVM ``` -⚠️ Keeping the third vocabulary separate is what lets mcpp name something LLVM +Keeping the third vocabulary separate is what lets mcpp name something LLVM cannot. Measured on llvm 22.1.8, `windows` with a `musl` environment is accepted by the triple parser and crashes the compiler: @@ -308,7 +308,7 @@ A target names a machine. It does not name who compiles for it, and it does not name where its C library comes from — those are two further choices, and the same target string means a different build under each. -⚠️ **"From the payload" is not one payload.** It is whichever payload the +**"From the payload" is not one payload.** It is whichever payload the chosen compiler brings, and gcc and clang bring them differently: gcc has one payload per target, with a triple-prefixed driver, while one `clang++` emits every target it was built with. Measured on one host, one source: @@ -341,7 +341,7 @@ error: target 'x86_64-linux-musl' takes its C library from the 'gcc@16.1.0' payload, and 'llvm@22.1.8' has none here. ``` -⚠️ **Before 2026.8.26.1 this ran the whole build and failed at the link**, with +**Before 2026.8.26.1 this ran the whole build and failed at the link**, with `crtbeginT.o (bare name — the linker cannot resolve it)` — accurate about the symptom and silent about the decision. clang is retargetable and brings no C library, so it reached for a gcc installation; on a machine that happens to have @@ -360,7 +360,7 @@ default = "llvm@22.1.8" That is [`examples/06-openkal-cross`](../examples/06-openkal-cross), and it is why the refusal names openkal in its own text. -⚠️ **A bare-metal row's toolchain is not a convention, and neither is +**A bare-metal row's toolchain is not a convention, and neither is `x86_64-windows-musl`'s** — no gcc payload emits a PE with a musl C library, so those rows cannot be overridden at all. See [chapter 03](03-toolchains.md). @@ -375,7 +375,7 @@ graph — measured the same way: | `x86_64-windows-gnu` | openkal (openkal-windows, graph) | musl (graph) | libc++ (graph) | | `x86_64-windows-musl` | openkal (openkal-windows, graph) | musl (graph) | libc++ (graph) | -⚠️ **Look at `x86_64-windows-gnu` in both tables.** Its C library is `gnu` — the +**Look at `x86_64-windows-gnu` in both tables.** Its C library is `gnu` — the MinGW CRT — when a payload supplies it, and `musl` when the graph does. One target string, two different C libraries, and until 2026.8.24.6 mcpp had no way to say which: the same `--target x86_64-windows-gnu` produced artefacts that @@ -393,7 +393,7 @@ from a dependency graph, which is what `toolchain list` reports as The two axes above — which compiler, and where the C library comes from — are choices a project makes. The third is not: it is the machine the build runs on. -⭐ **The axis is the set mcpp ships for, and it is (os, arch) rather than os.** +**The axis is the set mcpp ships for, and it is (os, arch) rather than os.** `release.yml` publishes four host binaries: | build host | release asset | CI runner | @@ -403,7 +403,7 @@ choices a project makes. The third is not: it is the machine the build runs on. | `macos-arm64` | `mcpp--macosx-arm64.tar.gz` | `macos-14` | | `windows-x86_64` | `mcpp--windows-x86_64.zip` | `windows-2022` | -⚠️ **The two Linux hosts are not one host.** `x86_64-linux-gnu` needs the +**The two Linux hosts are not one host.** `x86_64-linux-gnu` needs the host-native `xim:glibc` and `xim:linux-headers` payloads, which exist for the host's own architecture only — so that row is reachable from `linux-x86_64` and not from `linux-aarch64`, while `aarch64-linux-gnu` is the mirror case and is @@ -414,31 +414,31 @@ other's rows. | target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | |---|---|---|---|---|---|---| -| `x86_64-linux-gnu` | verified | — | ✅ payload | — | — | — | +| `x86_64-linux-gnu` | verified | — | payload | — | — | — | | `aarch64-linux-gnu` | planned | — | planned | planned | planned | planned | -| `x86_64-linux-musl` | verified | `gcc@16.1.0` | ✅ payload | ✅ payload | — | ✅ payload | -| `aarch64-linux-musl` | verified | `gcc@16.1.0` | ✅ payload | ✅ payload | — | — | +| `x86_64-linux-musl` | verified | `gcc@16.1.0` | payload | payload | — | payload | +| `aarch64-linux-musl` | verified | `gcc@16.1.0` | payload | payload | — | — | | `riscv64-linux-musl` | planned | — | planned | planned | planned | planned | -| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | ✅ payload | ✅ payload | — | ✅ payload | -| `x86_64-windows-musl` | preview | `llvm@22.1.8` | ⚙ graph | ⚙ graph | ⚙ graph | ✅ payload | -| `x86_64-windows-msvc` | verified | — | — | — | — | ✅ system | -| `aarch64-macos` | verified | — | — | — | ✅ SDK | — | +| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | payload | payload | — | payload | +| `x86_64-windows-musl` | preview | `llvm@22.1.8` | graph | graph | graph | payload | +| `x86_64-windows-msvc` | verified | — | — | — | — | system | +| `aarch64-macos` | verified | — | — | — | SDK | — | | `x86_64-macos` | planned | — | planned | planned | planned | planned | -| `riscv64-none-elf` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `riscv32-none-elf` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `aarch64-none-elf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `x86_64-none-elf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `thumbv6m-none-eabi` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `thumbv7m-none-eabi` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `thumbv7em-none-eabi` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `thumbv7em-none-eabihf` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | -| `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | - -`✅ payload` a toolchain payload here produces it · `⚙ graph` no payload, but a -dependency can supply the system · `✅ system` located on the machine, not -installed by mcpp · `✅ SDK` the platform's own · `—` unreachable from this host +| `riscv64-none-elf` | verified | `llvm@22.1.8` | payload | payload | payload | payload | +| `riscv32-none-elf` | verified | `llvm@22.1.8` | payload | payload | payload | payload | +| `aarch64-none-elf` | preview | `llvm@22.1.8` | payload | payload | payload | payload | +| `x86_64-none-elf` | preview | `llvm@22.1.8` | payload | payload | payload | payload | +| `thumbv6m-none-eabi` | verified | `llvm@22.1.8` | payload | payload | payload | payload | +| `thumbv7m-none-eabi` | verified | `llvm@22.1.8` | payload | payload | payload | payload | +| `thumbv7em-none-eabi` | preview | `llvm@22.1.8` | payload | payload | payload | payload | +| `thumbv7em-none-eabihf` | verified | `llvm@22.1.8` | payload | payload | payload | payload | +| `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | payload | payload | payload | payload | +| `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | payload | payload | payload | payload | +| `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | payload | payload | payload | payload | + +`payload` a toolchain payload here produces it · `graph` no payload, but a +dependency can supply the system · `system` located on the machine, not +installed by mcpp · `SDK` the platform's own · `—` unreachable from this host · `planned` registered in the vocabulary, nothing wired yet. ### The rule behind the columns @@ -453,7 +453,7 @@ installed by mcpp · `✅ SDK` the platform's own · `—` unreachable from this | `aarch64-macos` | macOS | the SDK is the machine's | | `*-none-elf` | every host | clang and lld are cross-compilers by construction | -⚠️ **A `—` is about payloads, not about possibility.** `host_can_serve` answers +**A `—` is about payloads, not about possibility.** `host_can_serve` answers "does a payload here produce it", and a dependency graph can supply the system instead — which is why `x86_64-windows-musl` reads `via dependency graph` on Linux and produces a real PE32+ there. @@ -466,7 +466,7 @@ four hosts. Each scans every row it lists, twice — the payload alone, then wit [`tests/matrix/expected.tsv`](../tests/matrix/expected.tsv), which is keyed on `(mode, host, target, compiler)`. -⚠️ What a runner resolves as its own target is not what its name suggests: +What a runner resolves as its own target is not what its name suggests: | runner | host target it resolves | |---|---| @@ -478,7 +478,7 @@ four hosts. Each scans every row it lists, twice — the payload alone, then wit Three of this chapter's criteria assumed the Linux coincidence and had to be corrected on the other hosts. -⚠️ **The cell count per host is not a constant.** It follows from what that +**The cell count per host is not a constant.** It follows from what that machine has installed, and the same runner has been measured with different toolchains on consecutive runs. The comparison therefore asserts *the scan produced rows* and *every row the expected table names was reached*, rather than diff --git a/docs/17-the-project-environment.md b/docs/17-the-project-environment.md index de8b9365..c3196e94 100644 --- a/docs/17-the-project-environment.md +++ b/docs/17-the-project-environment.md @@ -53,14 +53,14 @@ A build program that spells `qemu-system-riscv64` as a bare name therefore gets the copy inside the declared environment. Chapter 7 covers the contract this rides on. -⚠️ **Only for projects that declare one.** A project with no `[xlings].subos` +**Only for projects that declare one.** A project with no `[xlings].subos` gets the `PATH` mcpp was started with, byte for byte. Putting a shared directory in front of every project would make what a build sees depend on what else had been installed on that machine — two projects on one machine would agree with each other, and the same project on two machines would not. Declaring it is what puts it there. -⚠️ **Prefixed, not replaced.** A build program legitimately calls `git`, +**Prefixed, not replaced.** A build program legitimately calls `git`, `python3` or a shell, and none of those live in a SubOS. Front position makes the declared environment the default answer; everything else stays reachable behind it. diff --git a/docs/18-devices.md b/docs/18-devices.md index 362499d7..ce9acd40 100644 --- a/docs/18-devices.md +++ b/docs/18-devices.md @@ -40,7 +40,7 @@ Named runners exist for what remains: writing an image without running it, observing a console, starting a debug server, erasing a part, deploying without starting. -⚠️ **The engine knows no runner names.** `flash`, `serve`, `deploy`, `submit` +**The engine knows no runner names.** `flash`, `serve`, `deploy`, `submit` and `logcat` are equally unknown to it: it knows only that a package may supply named runners, and performs the argv it finds. A fixed set of names in the engine would decide, in the engine, which domains are expressible. @@ -58,7 +58,7 @@ mcpp::runner_longlived("monitor"); // no natural end mcpp::run_exclusive(); // this target's runs cannot overlap ``` -⭐ **Name the program, not its path.** mcpp locates it: the `bin/` of a payload +**Name the program, not its path.** mcpp locates it: the `bin/` of a payload declared under `[xlings.workspace]` by **any package in the graph** — the consuming project first, then its dependencies — and then `PATH`. A board-support package is precisely the thing that knows which emulator or probe reaches its machine, @@ -158,7 +158,7 @@ supply stays absent: an emulator has no debug probe, so under the emulator feature `mcpp run --runner debug` reports that no such runner exists and lists the ones that do. -⭐ This required no engine mechanism. The engine reads runners and knows nothing +This required no engine mechanism. The engine reads runners and knows nothing about emulators or probes; `mcpp::has_feature` already existed. That the question is answerable without adding anything is the layering working as specified. diff --git a/docs/19-supported-versions.md b/docs/19-supported-versions.md index fc43b40f..fbbf7a69 100644 --- a/docs/19-supported-versions.md +++ b/docs/19-supported-versions.md @@ -39,7 +39,7 @@ worth stating precisely. | Target table rows | Additive. A row's tier may rise; a row is not removed while a published package targets it | | Build fingerprints, cache layout, `target/` contents | **Not an interface.** These change without notice, and nothing should parse them | -⚠️ A `build.mcpp` calling a function its engine's bundled `mcpp` module does not +A `build.mcpp` calling a function its engine's bundled `mcpp` module does not have fails at the **compile** of the build program, not through a protocol error. The protocol number governs directives on the wire; the typed API is governed by which engine is installed. Both are stated here because the failure @@ -58,7 +58,7 @@ error: --locked was given and this resolution differs from mcpp.lock: A release build, an audit or a CI job should pass `--locked`. It disables the build fast path, so the assertion always runs. -⚠️ The lock does not yet constrain resolution — it records and verifies it. +The lock does not yet constrain resolution — it records and verifies it. Pinning a resolution to the lock as an input is a separate change to the resolver. diff --git a/docs/20-accelerators.md b/docs/20-heterogeneous-builds.md similarity index 88% rename from docs/20-accelerators.md rename to docs/20-heterogeneous-builds.md index 82c1ba78..436b221c 100644 --- a/docs/20-accelerators.md +++ b/docs/20-heterogeneous-builds.md @@ -1,7 +1,8 @@ -# 20 — Accelerators +# 20 — Heterogeneous Builds -How mcpp builds device code, and how a prebuilt artifact states which devices -it can run on. +GPU and AI accelerator targets, and mixed host/device compilation: how mcpp +builds device code, and how a prebuilt artifact states which devices it can +run on. ## Two shapes, one of which mcpp implements today @@ -21,10 +22,31 @@ This document describes the island shape, which is what mcpp implements. ## Device translation units -A source whose extension is `.cu` or `.hip` is a **device translation unit**. -mcpp classifies it as such and treats it accordingly: it is never scanned for -imports and never produces a BMI, because no device compiler accepts C++20 -modules. +A source in a language a separate compiler consumes is a **device translation +unit**. mcpp classifies it as such and treats it accordingly: it is never +scanned for imports and never produces a BMI, because no device compiler +accepts C++20 modules. + +The criterion is the compiler, not the vendor (the table below is +2026.9.5.3+; before it, `.cu` and `.hip` alone): + +| language | extensions | +|---|---| +| CUDA, HIP | `.cu`, `.hip` | +| GLSL, by stage | `.comp`, `.vert`, `.frag`, `.geom`, `.tesc`, `.tese`, `.mesh`, `.task`, `.rgen`, `.rint`, `.rahit`, `.rchit`, `.rmiss`, `.rcall` | +| GLSL, stage-less | `.glsl` | +| HLSL | `.hlsl` | +| OpenCL C | `.cl` | +| Metal Shading Language | `.metal` | + +An extension outside this table listed in `[build] sources` is refused by name, +which is the behaviour that makes the table a table: mcpp has no rule for the +file, its object would be linked by nothing, and building it would fail later +and less clearly. + +`.glsl` carries no stage. glslang derives the stage from the extension, so a +rule package refuses a stage-less name — the message belongs there, and this +table therefore does not need to know which extensions name a stage. `.cuh` and `.hiph` are classified as headers. They are not compiled, but editing one can change what the graph should be, so they invalidate the fast @@ -71,7 +93,7 @@ its host-compiler requirements belong to the rule. Three things go wrong late with a device toolkit, and none of them is a fact about the build graph. They are read and reported by the **rule package** that -drives the tools -- `examples/09-cuda-kernel/rules-cuda` shows each one -- and +drives the tools -- `mcpp.rules.cuda` in `mcpp:plugins` shows each one -- and the engine owns none of them (`tests/unit/test_core_vendor_probes.cpp` holds that line, so a second backend never grows a second copy inside mcpp). diff --git a/docs/README.md b/docs/README.md index f6d0ebde..1a2a8933 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,7 @@ - [17 - The Project Environment](17-the-project-environment.md) - [18 - Reaching a Device](18-devices.md) - [19 - Supported Versions and Compatibility](19-supported-versions.md) -- [20 - Accelerators](20-accelerators.md) +- [20 - Heterogeneous Builds](20-heterogeneous-builds.md) ## Specifications diff --git a/docs/spec/README.md b/docs/spec/README.md index 37516535..63b4ecbc 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -20,11 +20,11 @@ | 标记 | 含义 | |---|---| -| ✅ **已实现** | 当前实现与规范一致 | -| ⚠️ **部分实现** | 已有实现,但语义或覆盖面有差异(差异已注明) | -| ❌ **未实现** | 规范要求但尚未支持;当前行为已注明 | +| **已实现** | 当前实现与规范一致 | +| **部分实现** | 已有实现,但语义或覆盖面有差异(差异已注明) | +| **未实现** | 规范要求但尚未支持;当前行为已注明 | -> 标注 ❌ 的条款**尚未生效**。在其落地前,请按各文档中「当前应当怎么写」一节书写。 +> 标注 的条款**尚未生效**。在其落地前,请按各文档中「当前应当怎么写」一节书写。 ## 索引 diff --git a/docs/spec/exit-codes.md b/docs/spec/exit-codes.md index 44ec4338..0d8bc04c 100644 --- a/docs/spec/exit-codes.md +++ b/docs/spec/exit-codes.md @@ -40,7 +40,7 @@ usage / internal 一半(`2`、`70`、`127`),runtime 的一半 —— 也就是 - 库层 API 的返回值。`status_severity()` 这类返回 `0..3` 的函数是**严重性排序**, 与退出码无关,**禁止**被读作退出码。 -## 2. 码表 ✅ 已实现 +## 2. 码表 已实现 | 码 | 类别 | 含义 | 通道 | |---|---|---|---| @@ -60,23 +60,23 @@ issue;`1` 通常不是。 ## 3. 规则 -### 3.1 分类必须稳定 ✅ 已实现 +### 3.1 分类必须稳定 已实现 一个已经发布的失败场景**禁止**在后续版本里改变它所属的类别。新增类别**可以**引入新 的码;把既有场景从 `1` 挪到 `4`(或反向)是破坏性变更。 -### 3.2 用法错误必须先于副作用 ✅ 已实现 +### 3.2 用法错误必须先于副作用 已实现 返回 `2` 的路径**禁止**产生任何副作用。一个还不知道自己会被要求做什么的请求,不该 已经写过磁盘。 -### 3.3 退出码禁止用于协议识别 ✅ 已实现 +### 3.3 退出码禁止用于协议识别 已实现 客户端**禁止**用退出码判断「这个 mcpp 支不支持某项功能」。理由见 `docs/11-machine-output.md` §1:在该协议出现之前发布的每个版本上,未知选项本身就是 一次错误,而它当年走的是 stdout + 退出码 1。唯一跨版本成立的判据是**解析 stdout**。 -### 3.4 `1` 可以与 stdout 上的信封同时出现 ✅ 已实现 +### 3.4 `1` 可以与 stdout 上的信封同时出现 已实现 一次失败**可以**同时是一份文档。`mcpp xpkg parse` 对一份违反名字形态的描述符会把 判定作为 JSON 打到 stdout **并且**退 `1`。 @@ -84,7 +84,7 @@ issue;`1` 通常不是。 因此客户端**禁止**把非零退出当作「没有输出」而跳过解析。这与 §3.3 是同一条规则的两 个方向:退出码不携带「有没有输出」的信息。 -### 3.5 非零退出必须在 stderr 上有原因 ✅ 已实现 +### 3.5 非零退出必须在 stderr 上有原因 已实现 任何非零退出**必须**在 stderr 上留下至少一行说明。空 stderr 加非零退出是缺陷。 @@ -109,7 +109,7 @@ issue;`1` 通常不是。 | `4` `5` `9` `19` `20` `1024` | 否 | `runtime/elf.cppm` 的 ELF 重定位类型表(`R_RISCV_COPY` / `R_X86_64_COPY` / …) | | `23` | 否 | `toolchain/msvc.cppm` 的版本解析 | -⚠️ `4` 同时出现在两栏,这正是为什么这张表按**出处**而不是按数字归类:同一个字面量在 +`4` 同时出现在两栏,这正是为什么这张表按**出处**而不是按数字归类:同一个字面量在 一个文件里是退出码,在另一个文件里是 `EM_RISCV` 的重定位类型。仅凭 `grep "return 4"` 数出来的 13 处里,有 2 处不是退出码。 diff --git a/docs/spec/package-identity.md b/docs/spec/package-identity.md index 509661cd..3252834d 100644 --- a/docs/spec/package-identity.md +++ b/docs/spec/package-identity.md @@ -26,9 +26,9 @@ | 标记 | 含义 | |---|---| -| ✅ **已实现** | 自对应“最低实现版本”起的行为与本规范一致 | -| ⚠️ **部分实现** | 已有实现,但语义或覆盖面与本规范有差异(差异已注明) | -| ❌ **未实现** | 本规范要求但尚未支持;当前行为已注明 | +| **已实现** | 自对应“最低实现版本”起的行为与本规范一致 | +| **部分实现** | 已有实现,但语义或覆盖面与本规范有差异(差异已注明) | +| **未实现** | 本规范要求但尚未支持;当前行为已注明 | > 描述符身份规则自 mcpp 0.0.106 起实现;唯一精确 selector 自 2026.8.10.1 > 起实现。0.0.105 及更早版本要求的过渡形态(`name` 必须写成 @@ -71,7 +71,7 @@ mcpp 的正确做法是**把描述符里读到的字面值原样使用**,而不 **层级一律属于 `namespace`。** `(mcpplibs.capi, lua)` 是规范形态。 -✅ **已实现**:身份二元组模型贯穿 mcpp 全链路(`XpkgIdentity`、`canonical_xpkg_identity`、`xpkg_lua_identity_matches`)。 +**已实现**:身份二元组模型贯穿 mcpp 全链路(`XpkgIdentity`、`canonical_xpkg_identity`、`xpkg_lua_identity_matches`)。 --- @@ -83,23 +83,23 @@ mcpp 的正确做法是**把描述符里读到的字面值原样使用**,而不 - 非空时,**必须**是点分路径,每段由字母、数字、`-`、`_` 组成。 - **可以**多级(`mcpplibs.capi`),多级用于表达真实的层级归属。 -✅ **已实现**。 +**已实现**。 ### 3.2 `package.name` > **规范:`name` 必须是单一原子段,禁止包含 `.`。任何层级都必须放进 `namespace`。** ```lua --- ✅ 规范形态 +-- 规范形态 package = { namespace = "chriskohlhoff", name = "asio" } package = { namespace = "mcpplibs.capi", name = "lua" } package = { namespace = "", name = "imgui" } --- ❌ 层级留在 name 里 +-- 层级留在 name 里 package = { namespace = "mcpplibs", name = "capi.lua" } ``` -✅ **已实现**(0.0.106)。`mcpp xpkg parse` 与安装路径共用同一谓词校验。 +**已实现**(0.0.106)。`mcpp xpkg parse` 与安装路径共用同一谓词校验。 **兼容写法**:0.0.105 及更早版本要求的完全限定拼写仍被接受 —— @@ -117,9 +117,9 @@ package = { namespace = "compat", name = "compat.zlib" } -- legacy,短名 = "z 这与 xlings 的寻址模型一致:目标写作 `:`,冒号前比对包声明的 `package.namespace`,显式写出命名空间即可消歧。 -✅ **规范如此**。 +**规范如此**。 -✅ **已实现**。xlings **0.4.69** 起索引按 `(effectiveNamespace, name)` 建键([#381](https://github.com/openxlings/xlings/issues/381)),同一索引内两个同短名不同命名空间的包各自可寻址;mcpp 始终传入唯一精确身份。 +**已实现**。xlings **0.4.69** 起索引按 `(effectiveNamespace, name)` 建键([#381](https://github.com/openxlings/xlings/issues/381)),同一索引内两个同短名不同命名空间的包各自可寻址;mcpp 始终传入唯一精确身份。 e2e `163_identity_first_resolution.sh` 端到端锁住:同一 path 索引内 `(alpha, widget)` 与 `(beta, widget)` 各自安装到 `alpha-x-widget` / `beta-x-widget`。 @@ -131,7 +131,7 @@ e2e `163_identity_first_resolution.sh` 端到端锁住:同一 path 索引内 `(a 身份来自文件**内容**声明的 `package.{namespace,name}`,与文件叫什么无关。 -✅ **已实现**(0.0.106)。两个半边都已兑现: +**已实现**(0.0.106)。两个半边都已兑现: - **命中后的校验**:任何候选都必须通过 `xpkg_lua_identity_matches` 复核声明身份(防止 `compat.zlib` 请求被外来的裸 `zlib.lua` 满足)。 - **发现**:推荐文件名作为**快路径**先探测;全部落空时回落到按声明身份扫描 `pkgs/**/*.lua`。因此叫任何名字、放在任何字母目录下的描述符都能被找到,而符合推荐命名的索引**不付出任何扫描开销**。 @@ -160,7 +160,7 @@ e2e `163_identity_first_resolution.sh` 锁住:身份为 `(acme, widget)` 的描 | 3 | `[dependencies.acme]`
`widget = "1.0"` | `(acme, widget)` | **命名空间子表**。与 #2 完全等价,多个同 namespace 包时更清晰 | | 4 | `[dependencies]`
`"acme.widget" = "1.0"` | `(acme, widget)` | **引号点式键**(legacy source shape)。身份仍与 #2/#3 相同 | -✅ **已实现**(`parse_package_selector` / `normalize_package_selector` / `make_direct_dependency_selector`)。 +**已实现**(`parse_package_selector` / `normalize_package_selector` / `make_direct_dependency_selector`)。 **#2 与 #3 只有 TOML 版式差异,没有解析差异。** 新增索引或同短名 sibling 不能改变其身份。 @@ -172,7 +172,7 @@ e2e `163_identity_first_resolution.sh` 锁住:身份为 `(acme, widget)` 的描 例如 gtest 必须写成 `compat.gtest` 或 `[dependencies.compat] gtest = ...`;裸 `gtest` 请求的是不同身份 `(mcpplibs, gtest)`。 -✅ **已实现**(2026.8.10.1)。默认 namespace 的依赖身份门禁不再接纳 `compat` 或无 namespace descriptor。 +**已实现**(2026.8.10.1)。默认 namespace 的依赖身份门禁不再接纳 `compat` 或无 namespace descriptor。 **设计理由**:全域按短名搜索会让解析结果取决于「本机装了哪些索引」—— 1. 两个命名空间拥有同名包时,胜负由索引顺序决定,而用户 `[indices]` 添加的索引之间**没有全序**; @@ -212,7 +212,7 @@ e2e `163_identity_first_resolution.sh` 锁住:身份为 `(acme, widget)` 的描 唯一身份落空时,mcpp **必须**明确失败并列出该身份;若同短名存在于其他 namespace,**应当**只在诊断中给出可复制的显式 selector,禁止把提示结果回灌解析。 -✅ **已实现**(2026.8.10.1): +**已实现**(2026.8.10.1): ``` error: dependency 'asio': no package found @@ -288,7 +288,7 @@ warning,规范列举命令是 `mcpp new --list-templates pkg`。 | `ns == "mcpplibs"`(默认命名空间) | 依赖 selector 主路径要求描述符身份必须为 `mcpplibs`;底层 legacy API 的 `allowLegacyBareDefault` 只供非 selector 兼容调用 | | 其他具体 ns | **精确相等** | -✅ **已实现**。`compat.gtest` 与 `mcpplibs.gtest` 是两个不同身份。 +**已实现**。`compat.gtest` 与 `mcpplibs.gtest` 是两个不同身份。 ### 5.3 空命名空间的兼容边界(P3) @@ -296,7 +296,7 @@ warning,规范列举命令是 `mcpp new --list-templates pkg`。 若描述符本身未声明 `namespace`(上游裸包如 `opencv`),则**空命名空间就是它的合法身份**,不得强行填充。 -✅ **已实现**(0.0.105)。此前空命名空间会流入 lockfile 与安装层。 +**已实现**(0.0.105)。此前空命名空间会流入 lockfile 与安装层。 --- @@ -306,9 +306,9 @@ warning,规范列举命令是 `mcpp new --list-templates pkg`。 | 派生量 | 规范公式 | 状态 | |---|---|---| -| xlings 索引键 | `<字面 name>` | ✅ | -| xpkg 安装目录 | `{namespace}-x-{字面 name}` | ✅ | -| 安装 target | `:<字面 name>@<版本>` | ✅ | +| xlings 索引键 | `<字面 name>` | yes | +| xpkg 安装目录 | `{namespace}-x-{字面 name}` | yes | +| 安装 target | `:<字面 name>@<版本>` | yes | **注意 target 的冒号前缀是包的命名空间**(xlings 的 *effective namespace*),不是索引名 —— 这正是同一索引内两个同短名包得以各自寻址的原因。无命名空间的上游包用裸字面名寻址,无前缀。 @@ -338,8 +338,8 @@ warning,规范列举命令是 `mcpp new --list-templates pkg`。 | | FQN 形态描述符(现网) | 短名形态描述符 | |---|---|---| -| mcpp ≤ 0.0.105 | ✅ | ❌ `E_NOT_FOUND` | -| 实现 §7.2 之后 | ✅ | ✅ | +| mcpp ≤ 0.0.105 | yes | `E_NOT_FOUND` | +| 实现 §7.2 之后 | yes | | **只有一个破损格。** 因此: @@ -352,7 +352,7 @@ warning,规范列举命令是 `mcpp new --list-templates pkg`。 ```lua package = { namespace = "chriskohlhoff", - name = "asio", -- ✅ 规范形态 + name = "asio", -- 规范形态 } ``` @@ -387,9 +387,9 @@ asio = "1.38.1" ``` 选择器 [dependencies.chriskohlhoff] + asio → 显式命名空间表 → 单候选 (chriskohlhoff, asio) - → 发现:探测 pkgs/c/chriskohlhoff.asio.lua ✓ + → 发现:探测 pkgs/c/chriskohlhoff.asio.lua → 校验:声明 (chriskohlhoff, chriskohlhoff.asio) - 归一化 → (chriskohlhoff, asio) == 候选 ✓ + 归一化 → (chriskohlhoff, asio) == 候选 → 身份 (chriskohlhoff, asio) → wire key chriskohlhoff.asio → target chriskohlhoff:chriskohlhoff.asio@1.38.1 @@ -409,7 +409,7 @@ gtest = "1.15.2" ``` 选择器 compat.gtest → 唯一身份 (compat, gtest) -探测 compat.gtest.lua,声明 (compat, compat.gtest) → 归一 (compat, gtest) → ✓ +探测 compat.gtest.lua,声明 (compat, compat.gtest) → 归一 (compat, gtest) → 身份 (compat, gtest) → store dir compat-x-compat.gtest ``` diff --git a/docs/spec/target-side.md b/docs/spec/target-side.md index 05bf8e72..b5ddbde6 100644 --- a/docs/spec/target-side.md +++ b/docs/spec/target-side.md @@ -19,7 +19,7 @@ ## 1. 层 -### 1.1 层的闭集 ✅ 已实现 +### 1.1 层的闭集 已实现 一次构建的目标侧**必须**由且仅由以下五层构成: @@ -33,13 +33,13 @@ 层名是编译进引擎的闭集。实现名**禁止**出现在引擎代码中。 -### 1.2 可供给性 ✅ 已实现 +### 1.2 可供给性 已实现 除 `compiler` 外的每一层**可以**由包供给。`compiler` **禁止**由包供给: 编译器是引擎安装并驱动的载荷,族与族之间的差异是引擎必须持有的事实。 包**可以**要求某个 `compiler`。 -### 1.3 来源 ✅ 已实现 +### 1.3 来源 已实现 每一层的来源**必须**是以下四者之一: @@ -56,7 +56,7 @@ ## 2. 声明 -### 2.1 `provides` 语法 ✅ 已实现 +### 2.1 `provides` 语法 已实现 ``` mcpp:<层名>[=<实现名>] @@ -67,14 +67,14 @@ mcpp:<层名>[=<实现名>] 省略 `=<实现名>` 时,该层的实现名取包名。 -### 2.2 `requires` 语法 ✅ 已实现 +### 2.2 `requires` 语法 已实现 与 `provides` 同语法。语义为:被命名的层**必须**解析为被命名的实现, 否则引擎**必须**在编译开始之前拒绝该构建。 省略 `=<实现名>` 时,语义为该层**必须**有供给者。 -### 2.3 标准库模块源 ✅ 已实现 +### 2.3 标准库模块源 已实现 供给 `c++-abi` 的包**可以**在 `[build]` 下声明: @@ -89,10 +89,10 @@ mcpp:<层名>[=<实现名>] 声明了 `std-module` 而没有相应 `provides` 条目的包,引擎**必须**报错。 -⚠️ **部分实现**:`std-module` 与 `std-compat-module` 是单一路径,不可条件化; +**部分实现**:`std-module` 与 `std-compat-module` 是单一路径,不可条件化; 仅 `std-module-flags` 参与条件合并。 -### 2.4 预制载荷的声明 ❌ 未实现 +### 2.4 预制载荷的声明 未实现 预制载荷的描述符**应当**能够声明它供给哪一层、该层在载荷内的位置、 以及该层的真实版本。当前实现从载荷的包名切出接口名, @@ -105,7 +105,7 @@ mcpp:<层名>[=<实现名>] ## 3. 规则 -### 3.1 规则一:每层恰好一个供给者 ✅ 已实现 +### 3.1 规则一:每层恰好一个供给者 已实现 同一层出现两个供给者时,引擎**必须**在解析期报错, 并**必须**同时指出两个包及各自进入依赖图的路径(直接依赖或传递依赖)。 @@ -113,26 +113,26 @@ mcpp:<层名>[=<实现名>] 同一个包在 `provides` 中为同一层给出多条条目时,视为一个供给者; 其中命名了实现名的一条**必须**优先。 -### 3.2 规则二:为其下方的层配置过 ⚠️ 部分实现 +### 3.2 规则二:为其下方的层配置过 部分实现 一个实现**必须**在它曾被配置的层之上使用。引擎**必须**执行: -- 编译器载荷的 `c++-abi`,仅在 `c-abi` 同样来自该载荷时可用;✅ -- §2.2 声明的每一条 `requires`。✅ +- 编译器载荷的 `c++-abi`,仅在 `c-abi` 同样来自该载荷时可用; +- §2.2 声明的每一条 `requires`。 -⚠️ 「`compiler-runtime` 必须与 `compiler` 同族」当前**依赖供给者自行声明 +「`compiler-runtime` 必须与 `compiler` 同族」当前**依赖供给者自行声明 `requires`**,引擎不独立推断 —— 因为推断需要一张实现名到族的映射表, 而 §1.1 禁止引擎持有实现名。 -### 3.3 规则三:跨来源接线 ⚠️ 部分实现 +### 3.3 规则三:跨来源接线 部分实现 两层来自同一来源时,引擎**禁止**介入它们之间的关系。 两层来自不同来源时,只有引擎同时知道两边的地址,因此**必须**由引擎接线。 -⚠️ 当前实现覆盖「两层均来自图」与「两层均来自载荷」。 +当前实现覆盖「两层均来自图」与「两层均来自载荷」。 「一层预制、一层来自图」的接线尚不完整。 -### 3.4 规则四:三元组是请求 ✅ 已实现 +### 3.4 规则四:三元组是请求 已实现 三元组的 env 段**必须**被当作对 `c-abi` 的请求,而非其答案。 @@ -140,21 +140,21 @@ mcpp:<层名>[=<实现名>] - 段存在且与解析出的 `c-abi` 不同,且后者来自图 ⇒ 引擎**必须**报出该不一致, 并**必须**给出不含该段的目标拼写;**禁止**据此使构建失败。 -⚠️ 拒绝曾被实现并被实测否掉:它打破了每一个把宿主目标拼作 `x86_64-linux-gnu` +拒绝曾被实现并被实测否掉:它打破了每一个把宿主目标拼作 `x86_64-linux-gnu` 的工程与 CI 配置 —— 而那正是 `mcpp toolchain list` 打印的拼写。 判据是该请求**不改变任何东西**:图两种写法下都供给同一个 C 库, 因此该段是被忽略而非被违反。 -⚠️ 规范化会把 `x86_64-linux` 写成 `x86_64-linux-gnu`,因为身份必须是全的。 +规范化会把 `x86_64-linux` 写成 `x86_64-linux-gnu`,因为身份必须是全的。 请求**必须**在规范化之前捕获;报告**应当**显示工程书写的拼写。 -### 3.5 目标表的约定何时生效 ✅ 已实现 +### 3.5 目标表的约定何时生效 已实现 目标行的 `pin` 命名的是**供给该目标 C 库的载荷**,不是偏好的编译器。 它**必须**仅在两个条件同时成立时生效:清单对该目标未作陈述, 且依赖图中无人供给 `kernel-abi` 或 `c-abi`。 -⚠️ 第二个条件在依赖解析之后才可知,因此工具链**必须**在其之后解析。 +第二个条件在依赖解析之后才可知,因此工具链**必须**在其之后解析。 过早决定被双向实测否掉:无条件应用会替换用户用 `mcpp toolchain default` 设下的工具链;不应用会让一个零依赖的交叉构建从可用变为不可用。 @@ -162,18 +162,18 @@ mcpp:<层名>[=<实现名>] ## 4. 报告 -### 4.1 默认输出 ✅ 已实现 +### 4.1 默认输出 已实现 构建**必须**报告解析出的结果而非清单中的意图。 默认**应当**只列出来源不是编译器载荷的层;五层均来自载荷时**应当**只输出目标行。 `MCPP_VERBOSE=1` 时**必须**列出全部五层。 -### 4.2 诊断 ✅ 已实现 +### 4.2 诊断 已实现 诊断**必须**列出该判断所依据的每一层,包含来源为编译器载荷的层。 -### 4.3 标识 ✅ 已实现 +### 4.3 标识 已实现 报告**应当**使用与 `[dependencies]` 中一致的包名拼写。 全限定名**应当**仅在以下三种情形出现:`MCPP_VERBOSE`、 @@ -183,9 +183,9 @@ mcpp:<层名>[=<实现名>] ## 5. 兼容性 -### 5.1 未知名字 ✅ 已实现 +### 5.1 未知名字 已实现 -⚠️ **谁的清单决定答案。** +**谁的清单决定答案。** - 出现在**根工程**清单中的未知层名,引擎**必须**报错。 这是作者自己的拼写,而他正看着这次构建。 @@ -194,7 +194,7 @@ mcpp:<层名>[=<实现名>] 清单中其它位置的未知键,引擎**禁止**据此使整份清单加载失败;**应当**警告并忽略。 -⚠️ 第二条来自一次实测。在其落地之前,一个声明了新层名的包 +第二条来自一次实测。在其落地之前,一个声明了新层名的包 **在该层被命名之前发布的每一个引擎上都无法加载**,因此层名词表对已发布的包 永远不可扩展: @@ -202,10 +202,10 @@ mcpp:<层名>[=<实现名>] `provides = ["mcpp:compiler-runtime=compiler-rt"]` names no capability mcpp knows. -⚠️ 这条规定只在**未来**的引擎上生效。一个包若要声明某个层名, +这条规定只在**未来**的引擎上生效。一个包若要声明某个层名, 其使用者的引擎仍须不早于该层名被引入的版本。 -### 5.2 既有拼写 ✅ 已实现 +### 5.2 既有拼写 已实现 - 能力名 `hosted-standard-library` **必须**继续表示 `c++-abi` 层; - 工具链族拼写 `openkal-llvm` **必须**继续解析,归一为 `llvm`; diff --git a/docs/zh/01-examples.md b/docs/zh/01-examples.md index e20aca86..86f37ea8 100644 --- a/docs/zh/01-examples.md +++ b/docs/zh/01-examples.md @@ -24,6 +24,8 @@ mcpp build && mcpp run | 02 | [`examples/02-with-deps`](../../examples/02-with-deps/) | 引入依赖 `mcpplibs.cmdline` 解析命令行参数 | `[dependencies]`、SemVer、`mcpp.lock` | | 03 | [`examples/03-pack-static`](../../examples/03-pack-static/) | 通过 `mcpp pack --mode static` 生成全静态发布包 | `[target.]` 与 `[pack]` 配置 | | 08 | [`examples/08-build-rules`](../../examples/08-build-rules/) | 两个规则包,以及同时用到它们的工程 | `host-module = true`、`[build-dependencies]`、`role = "check"` 的 `mcpp::action` | +| 09 | [`examples/09-cuda-kernel`](../../examples/09-cuda-kernel/) | 接缝模块背后的 CUDA kernel,并带 CPU 回退 | `accel`、带约束的 source glob、`role = "object"` 的 `mcpp::action`、`cfg(accelerator = …)` | +| 10 | [`examples/10-vulkan-compute`](../../examples/10-vulkan-compute/) | 同一个计算写成 Vulkan compute shader,在 GPU 上或在 CPU 上 | 来自 `mcpp:plugins` 的 `mcpp.rules.spirv`、`role = "source"` 的 `mcpp::action`、生成的头文件、作为载荷的软件驱动 | ## 推荐阅读顺序 diff --git a/docs/zh/02-pack-and-release.md b/docs/zh/02-pack-and-release.md index d3f982f6..1993679d 100644 --- a/docs/zh/02-pack-and-release.md +++ b/docs/zh/02-pack-and-release.md @@ -59,10 +59,10 @@ mcpp 之所以针对私有 glibc 构建,正是为了让产物的行为不取决 | Mode | 需要宿主提供能力的程序 | |---|---| -| `system` | ✅ | -| `vendored`(默认) | ✅ **这类程序的正确默认值** | -| `self-contained` | ❌ **打包期拒绝** | -| `static` | ❌ **打包期拒绝** | +| `system` | yes | +| `vendored`(默认) | **这类程序的正确默认值** | +| `self-contained` | **打包期拒绝** | +| `static` | **打包期拒绝** | 两处拒绝出自同一个事实:**自带 libc 的 bundle 无法消费宿主提供的库。** 那个 `.so` 带着它对**目标机 libc** 的要求到达,而该进程没有那份 libc —— 双向实测记录于 diff --git a/docs/zh/03-toolchains.md b/docs/zh/03-toolchains.md index 32cf774a..6c9e67c2 100644 --- a/docs/zh/03-toolchains.md +++ b/docs/zh/03-toolchains.md @@ -105,7 +105,7 @@ $ mcpp build not your gcc@16.1.0 — this project only ``` -⭐ **不写任何东西。** 不写 `~/.mcpp/config.toml`,也不写工程的 `mcpp.toml`。 +**不写任何东西。** 不写 `~/.mcpp/config.toml`,也不写工程的 `mcpp.toml`。 这条要求是**这次构建**的性质,就只作用于这次构建;这台机器的默认值保持原样,对 其他每一个工程都是。版本取自已经装好的那些 —— 与 `mcpp toolchain default <族>` 走的是同一条解析 —— 只有该族一个都没装时,才取生态自己的 pin。 @@ -163,7 +163,7 @@ Available toolchains (run `mcpp toolchain install `): | `via dependency graph` | 编译器在本机,而目标的系统不在,由包供给 | 依赖一个实现该目标内核接口与 C 库的包 | | `planned` | 已登记在词表中,尚未发布 | — | -⚠️ **不在这个块里的 target,在本机根本构建不了**——而这句话现在比以前更窄。 +**不在这个块里的 target,在本机根本构建不了**——而这句话现在比以前更窄。 mcpp 2026.8.25.2 之前,这个块只列载荷能服务的那些,于是「系统来自依赖图」的 target 缺席,而同一台机器能为它产出真实的产物。`x86_64-windows-msvc` 与 `aarch64-macos` 在 Linux 宿主上仍然缺席,这是**对的**:MSVC 与 macOS SDK 是宿主 @@ -499,7 +499,7 @@ $ mcpp build --target riscv64-none-elf # [toolchain] default = "gcc@…" error: target 'riscv64-none-elf' cannot be emitted by 'gcc@16.1.0'. ``` -⭐ **两处拒绝都发生在做出决定的地方**,而不是留给编译器。2026.8.26.1 之前,前者 +**两处拒绝都发生在做出决定的地方**,而不是留给编译器。2026.8.26.1 之前,前者 会跑完整个构建然后死在链接上,报 `crtbeginT.o (bare name)`;后者给出 `g++: error: unrecognized argument in option '-mabi=lp64d'` —— 一条关于选项的 消息,而决定在一百行之前。 diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md index e5a29653..6fa57dd6 100644 --- a/docs/zh/05-mcpp-toml.md +++ b/docs/zh/05-mcpp-toml.md @@ -206,9 +206,10 @@ mcpp 刻意不在一次构建里把同一个共享源编译成两份:一个源 > 进入列表;约束决定它是否适用于某一次构建。它必须至少匹配一个文件(空匹配会被拒绝: > 那会让这个设备无东西可编,而只在链接时才说话)。`--no-accel` 下该 glob 被排除, > 一个工程由此产出它的 CPU-only 变体。`--accel` 未覆盖该约束时构建被拒并给出两侧 -> (`accel-mismatch`)。有效集合匹配到的设备类源文件(`.cu`、`.hip`)引擎从不编译; -> 它们以 `MCPP_DEVICE_SOURCES` 到达构建程序,由工程引入的规则包把每一个变成一条 -> `mcpp::action`。 +> (`accel-mismatch`)。有效集合匹配到的设备类源文件 —— CUDA 与 HIP、GLSL 各 stage、 +> HLSL、OpenCL C 与 Metal,完整清单见 [20 — 异构硬件构建](20-heterogeneous-builds.md) —— 引擎 +> 从不编译;它们以 `MCPP_DEVICE_SOURCES` 到达构建程序,由工程引入的规则包把每一个 +> 变成一条 `mcpp::action`。 ```toml [build] @@ -255,7 +256,7 @@ dependency_linkage = "shared" # 按 profile 覆盖 (ELF)/ `@loader_path`(Mach-O)/ 可执行文件自身目录(PE)保证构建目录 移动后仍能找到它。 -⚠️ **这不是 `[target.].linkage`**(§2.7.1)。那个键回答的是听起来相同、 +**这不是 `[target.].linkage`**(§2.7.1)。那个键回答的是听起来相同、 实则关于 **C 库**的问题(musl 的 `-static`、MSVC 的 `/MT`)。两者并不独立,而且 方向很重要:整链静态的映像没有解释器,根本装不下任何共享对象。因此在 C 库静态 链接的目标上 —— 这是 **musl 的默认** —— `dependency_linkage = "shared"` 会被 @@ -283,7 +284,7 @@ dependency_linkage = "shared" # 按 profile 覆盖 名字,也是 mcpp 构建的那份与第三方携带的同一个库能解析到**同一个文件**的唯一 途径 —— 而如果声明它就意味着这个包不能再作为静态库被消费,包就无法陈述这件事。 -⚠️ 在非 shared 目标上写 `soname` 的描述符,**无法被 2026.8.28.2 之前的 mcpp 读取** +在非 shared 目标上写 `soname` 的描述符,**无法被 2026.8.28.2 之前的 mcpp 读取** —— 失败的是整份 manifest,不只是这个键。因此把它发布进索引要等下限抬上去。 #### 符号提供者检查 @@ -320,7 +321,7 @@ dependency_linkage = "shared" # 按 profile 覆盖 ```toml [build] -# ⚠️ 两类目录的**相对顺序**是承重的:本包自己构建时,内部覆盖层必须排在公共头之前。 +# 两类目录的**相对顺序**是承重的:本包自己构建时,内部覆盖层必须排在公共头之前。 # 这正是它被设计成 `include_dirs` 的**子集**而不是第二个列表的原因 —— # 两个数组表达不了一个顺序。 include_dirs = ["port/include", "musl/src/include", "musl/include"] @@ -576,7 +577,7 @@ Windows 组件(Win10 起),mcpp 从不分发它;而 `vcruntime140.dll` / C++ 编译通道。它覆盖包内每个 TU(含模块接口单元),因此也会进入**编译器自己的** P1689 模块扫描。 -> ⚠️ **但它不会让被宏保护的 `import` 变得可用。** mcpp 在编译器看到文件之前先跑 +> **但它不会让被宏保护的 `import` 变得可用。** mcpp 在编译器看到文件之前先跑 > 自己的词法预扫描,而那个扫描器对**任何** `#if` / `#ifdef` 块内的 `import` > 一律拒绝,不求值条件: > @@ -711,7 +712,7 @@ accel = "cuda12.8+{sm_80,sm_90f} ptx>=90" 也就是在一个同时发布了设备构建的包中选中 CPU-only 变体的方式。 该取值会与构建所消费的任何预建产物的 `accel` 字段比较,而请求为空的构建被任何产物满足。 -见 [20 — 加速器](20-accelerators.md)。 +见 [20 — 异构硬件构建](20-heterogeneous-builds.md)。 ### 2.4 `[lib]` — 库根模块约定 @@ -809,15 +810,15 @@ qux = ">=1.0, <2.0" # 范围组合 不存在有序回退或按短名的全索引模糊搜索: ```toml -# ✅ 正确 —— 点式选择器 +# 正确 —— 点式选择器 [dependencies] chriskohlhoff.asio = "1.38.1" -# ✅ 正确 —— 命名空间子表(同一组织有多个包时更推荐) +# 正确 —— 命名空间子表(同一组织有多个包时更推荐) [dependencies.chriskohlhoff] asio = "1.38.1" -# ❌ 错误 —— 裸名永远到不了 chriskohlhoff 命名空间 +# 错误 —— 裸名永远到不了 chriskohlhoff 命名空间 [dependencies] asio = "1.38.1" ``` @@ -1056,7 +1057,7 @@ sysroot = "xim:newlib-riscv@4.4" # a different C library sysroot = "" # no C library at all ``` -⚠️ **键缺席与键为空是两个不同的答案。** 缺席继承目标表的 C 库。存在且为空是 +**键缺席与键为空是两个不同的答案。** 缺席继承目标表的 C 库。存在且为空是 **零 libc 档**:不解析任何 C 库,不加入头文件与库目录,链接行上只有工程与其依赖 提供的内容,`#include ` 不再解析。内核与 bootloader 要的正是这一档,而把 两种情形合并会让这类工程静默地把目标的 C 库拿回去。 @@ -1068,7 +1069,7 @@ sysroot = "" # no C library at all 名字,`mcpp::target_libc_profile()` 返回目标 ISA 档位对应的子目录。零 libc 档上两者均 为空。 -⚠️ **这与「目标侧解析出的 C 库是哪一个」不是同一个问题。** `target_libc()` 命名的是 +**这与「目标侧解析出的 C 库是哪一个」不是同一个问题。** `target_libc()` 命名的是 mcpp 装上的那个载荷,而这个值是目标侧解析的一项**输入** —— 依赖图里的包可以改为供给 C 库,那时解析出的 `c-abi` 就不是这里返回的东西。要按已解析的层分支,请用层谓词: `[target.'cfg(c-abi = "musl")'.build]`(见[14 —— 目标侧](14-target-side.md))。 @@ -1388,7 +1389,7 @@ error: `toolkitnew` requires cuda.driver >= 13.0, and this machine has 12.4. **没有任何厂商词汇抵达引擎。** 它读到的是一个名字、一个关系和一个版本; `cuda.driver` 是流过的数据,一个 mcpp 从未听说过的后端比较方式完全相同。 -⚠️ **没人回答的下界是沉默的。** 一台从未声明自己有什么的机器,不是「未满足下界」的机器, +**没人回答的下界是沉默的。** 一台从未声明自己有什么的机器,不是「未满足下界」的机器, 而是「没人问过」的机器。把「我们不知道」变成「不行」正是这个机制要避免的失败, 并且有直接判据:`tests/e2e/603_version_floor.sh` 会构建一个下界指向无人提供之物的工程。 @@ -1413,7 +1414,7 @@ backend-openblas = { implies = ["use_blas"] } compat.openblas = "0.3" ``` -⚠️ **写 `"^0.3.0"`,而不是 `"0.3.x"` 或 `"0.3"`。** 以索引中确定存在的包作对照, +**写 `"^0.3.0"`,而不是 `"0.3.x"` 或 `"0.3"`。** 以索引中确定存在的包作对照, 判据取**构建成功**: | 写法 | 结果 | @@ -1423,11 +1424,11 @@ compat.openblas = "0.3" | `cmdline = "0.0"` | 解析通过,随后 `install path missing after fetch` | | `cmdline = "0.0.x"` | `E_NOT_FOUND`,点名的是包 —— 而该包存在 | -⭐ 这三种结果值得分开,因为两个更弱的判据各自会放行一种不可用的写法: +这三种结果值得分开,因为两个更弱的判据各自会放行一种不可用的写法: 「没有 `E_NOT_FOUND`」放行两段前缀,「解析通过」同样放行它。**只有对着真实索引构建 一次**才能定论。 -⚠️ 这一点在此处比在 `[dependencies]` 中更要紧:**实现取不回来的 feature 等于不存在的 +这一点在此处比在 `[dependencies]` 中更要紧:**实现取不回来的 feature 等于不存在的 feature**,而开发期使用 **path** 依赖的工程根本不查索引 —— 该失败只在发布之后才出现, 而且是出现在别人身上。 @@ -1693,7 +1694,7 @@ accelerators = ["cuda", "rocm"] 与产物的 `accel` 字段刻意不同。声明由人手写、可以是期望值;`accel` 是从产生该二进制的 那次构建测量出来的,并且是消费者被拒绝时所依据的东西。见 -[20 — 加速器](20-accelerators.md)。 +[20 — 异构硬件构建](20-heterogeneous-builds.md)。 ### 2.13 `[xlings]` — 工程的环境 @@ -1911,6 +1912,33 @@ mcpp 拒绝这种情形,并点名两个包与各自的 interface 路径。检查 lib 根必须在 `src/.cppm`(或 `[lib] path` 指向的位置);缺失时报 *"host module 'x': no interface unit at …"*。 +**一个包可以提供多条规则,由 feature 选择**(mcpp 2026.9.5.3+)。包解析后的 +`[build] sources` 里 —— 含 feature 加入的源文件 —— 每一个模块接口单元都以它自己声明的 +名字编成一个 host 模块,lib 根排在最前。feature 单元可以 import lib 根;除此之外每个 +单元单独编译,因此只 import `std` 与 `mcpp`。只有写在清单里的源文件参与:未声明 +`sources` 的包所推断出的 `src/**` 不被读取,所以此前发布的规则包暴露的仍是它当时暴露 +的那一个模块。 + +```toml +# 集合包的 manifest +[build] +sources = ["src/plugins.cppm"] # export module mcpp.plugins; + +[features] +rules-cuda = { sources = ["rules/cuda.cppm"] } # export module mcpp.rules.cuda; +rules-spirv = { sources = ["rules/spirv.cppm"] } # export module mcpp.rules.spirv; +``` + +```toml +# 消费者 +[dependencies.mcpp] +plugins = { version = "0.1.0", features = ["rules-spirv"], host-module = true } +``` + +模块集合就是 feature 集合:feature 未激活的单元不编译,import 它会以未知模块失败。 +`mcpp:plugins` 是 mcpp 项目维护的集合(仓库 `mcpp-community/mcpp-plugins`);其成员 +命名为 `mcpp.rules.`(规则包)与 `mcpp.tools.`(构建期工具)。 + *仅构建期:* `host-module = true` 的依赖**不会**被编进、也不会被链进本工程的 target, 它所依赖的东西也不会。它只在 `build.mcpp` 期间运行,别处都不出现。(2026.8.5.2 之前 它还会被当作普通库再编一遍,这正是规则里 `import mcpp;` 失败的原因:在那第二次编译里 diff --git a/docs/zh/07-build-mcpp.md b/docs/zh/07-build-mcpp.md index 50e906c1..61ec3d72 100644 --- a/docs/zh/07-build-mcpp.md +++ b/docs/zh/07-build-mcpp.md @@ -49,7 +49,7 @@ mcpp build # 编译 + 运行 build.mcpp,然后构建工程 | `mcpp:source=` *(0.0.100+)* | 把一份**既有**源文件选入构建(绝对路径,或相对包根)。下游效果与 `generated=` 相同;语义区别在于文件是程序*选中*的(tarball payload / vendored 源树)而非程序写出的——例如对大型源码包做 per-target 源选择 | | `mcpp:include-dir=` *(0.0.100+)* | 为本包自身 TU 增加一个**私有** include 目录(`-I`;绝对路径或相对包根,自动规范化)。取代过去 `cxxflag=-I` + `cflag=-I` 的双重裸发 | | `mcpp:include-dir-after=` *(0.0.100+)* | 同 `include-dir`,但排在系统目录**之后**搜索(`-idirafter`)——用于会遮蔽系统头的 payload 源树 | -| `mcpp:runner=` *(2026.8.19.2+)* | 执行本次构建产物的命令的**一个 argv token**(宿主跑不了它时)。一个 token 一次调用、按顺序;产物路径会被追加(或替换 `{}`)。**到达消费者**。⚠️ 可执行文件要发**绝对路径**,且**只能有一个**依赖提供它 | +| `mcpp:runner=` *(2026.8.19.2+)* | 执行本次构建产物的命令的**一个 argv token**(宿主跑不了它时)。一个 token 一次调用、按顺序;产物路径会被追加(或替换 `{}`)。**到达消费者**。可执行文件要发**绝对路径**,且**只能有一个**依赖提供它 | | `mcpp:link-script=` *(2026.8.19+)* | 用这个**链接脚本**链接(`-T`;相对路径按包根解析,发出的是绝对路径,因为链接是在构建目录里跑的)。与 `include-dir` 不同,它**到达消费者** —— 板子的内存布局恰恰是消费者写不出来的那一项 | | `mcpp:warning=` *(2026.8.21.2+)* | 对用户说一句话并**继续**。唯一一条不改变编译行、链接行与源码集的指令。它**穿过构建缓存** —— 见下 | | `mcpp:fact==` *(2026.9.5.2+)* | 陈述程序**测得的机器事实**(`cuda.driver=12.4`)。在编译任何东西之前与 floor 比较;见下 | @@ -119,7 +119,7 @@ if (const char* dir = mcpp::xpkg_dir("xim", "qemu-riscv"); dir && *dir) { } ``` -⚠️ **它之所以存在,是因为两个替代方案都更差,而且都试过。** 写到 stderr 的提示在成功 +**它之所以存在,是因为两个替代方案都更差,而且都试过。** 写到 stderr 的提示在成功 构建上什么都不打印。非零退出也不对:`mcpp build` 并不需要模拟器,让一个正确的构建失败, 是用一句缺失的话换一条坏掉的命令。 @@ -131,11 +131,11 @@ if (const char* dir = mcpp::xpkg_dir("xim", "qemu-riscv"); dir && *dir) { **它带归属。** 该行显示为 `<包名>: <文字>`,因为一个 workspace 里可能有好几个程序在说话, 而读者需要知道该打开哪一份清单。 -⭐ **它穿过构建缓存。** 构建程序的结果是被缓存的,命中时不再运行 —— 所以一条只活在运行 +**它穿过构建缓存。** 构建程序的结果是被缓存的,命中时不再运行 —— 所以一条只活在运行 路径上的提示,会在工程的第一次构建出现、之后再也不出现,而那读起来像「问题已解决」。 mcpp 在每次命中时重放它。 -⚠️ **全工程 no-op 构建什么都不打印,包括这一条。** 无事可做时,构建根本到不了 +**全工程 no-op 构建什么都不打印,包括这一条。** 无事可做时,构建根本到不了 `build.mcpp` 阶段 —— 它同样不会报告构建了哪个目标、推断了哪些源码。touch 一下源码,提示 就回来了。 @@ -157,12 +157,12 @@ mcpp::floor("cuda.driver >= 12.0"); // 本包需要它至少多新 干净、链接干净,到第一次使用才失败,而消息两边都不点名。解析了该运行时的规则包 在第一次编译之前就知道两个数字。 -⚠️ **两个字符串对引擎都没有意义。** `cuda.driver` 是流过引擎的数据;引擎读到的 +**两个字符串对引擎都没有意义。** `cuda.driver` 是流过引擎的数据;引擎读到的 是一个名字、一个关系、一个版本,第二个后端不需要改引擎。事实的拼法与包在 `[runtime] provides` 里静态声明的一致,下界与 `[[runtime.requirements]]` 的 `kind = "version-floor"` 一致:两条通道落进同一张表。 -⚠️ **事实随程序的其它输出一起进缓存**,命中时被回放。要声明什么会改变它 —— +**事实随程序的其它输出一起进缓存**,命中时被回放。要声明什么会改变它 —— 对读出版本的那个库 `rerun_if_changed` —— 否则事实会比它描述的机器活得更久。 ### `runner` —— 产物的执行方式(2026.8.19.2+) @@ -181,10 +181,10 @@ for (auto a : {"-machine","virt","-nographic","-no-reboot","-kernel"}) 调试时把 `-bios default` 换成 `-bios none -semihosting` 是正当需求 —— 且 mcpp 会说明 它覆盖了哪个依赖。 -⚠️ **可执行文件要发绝对路径。** 裸名会经 `PATH` 解析到一个 shim,而 shim 按**拥有它 +**可执行文件要发绝对路径。** 裸名会经 `PATH` 解析到一个 shim,而 shim 按**拥有它 的 home** 派发,那未必是本次构建用的 home。 -⚠️ **只能有一个依赖提供 runner。** 两个板级支持包都声称知道怎么跑这个产物是配置 +**只能有一个依赖提供 runner。** 两个板级支持包都声称知道怎么跑这个产物是配置 错误;mcpp 会**同时点名两个**并报错,而不是把它们并成一个谁也不是的 argv。 ### 问,而不是声明:`toolchain_dir` / `sysroot_dir`(2026.8.19.4+) @@ -198,7 +198,7 @@ const char* sr = mcpp::sysroot_dir(); // 目标的 C 库根目录,没有则 目标 C 库里的某个**文件**(比如板级支持包要的链接脚本)时,应当**问这个目录在哪**, 而不是去声明一个依赖来把它拽进来。 -⚠️ 这不是写法差异。声明 `xim:llvm` 会把包**钉死在一个标准库实现**上;声明 +这不是写法差异。声明 `xim:llvm` 会把包**钉死在一个标准库实现**上;声明 `xim:picolibc-riscv@1.8.12` 会把它钉死在**一个 C 库、一种架构、一个版本**上。而这些 都不是一个内容全是标准规定的名字的包的属性。**问**则会跟随 `[toolchain]` 与 `--target` 真正解析到的结果。 @@ -227,7 +227,7 @@ crt/host_config.h:218: fatal error: features.h: No such file or directory ——`--sysroot=<值>` 与 `-B<值>`,经外层工具的宿主选项拼法——第二个编译器就看到 第一个看到的东西。 -⚠️ **不是 `sysroot_dir()`。** 那个回答的是目标**档位**的问题,在宿主目标上为空, +**不是 `sysroot_dir()`。** 那个回答的是目标**档位**的问题,在宿主目标上为空, 而宿主目标恰恰是这一对存在的场合。mcpp 不传某个开关时,对应的那个为空串。 ### 找到 `[xlings.workspace]` 的载荷:`xpkg_dir`(2026.8.19+) @@ -252,7 +252,7 @@ const char* same = mcpp::xpkg_dir("picolibc-riscv"); // 裸名 `/data/xpkgs/-x-/` 写进代码,而那是 mcpp 可以随时改的 store 内部结构 —— 与 `dep_dir` 存在的理由相同。 -⚠️ **带版本固定**的引用只解析到那个版本,否则什么都不返回。请求 `1.8.12` 却静默拿 +**带版本固定**的引用只解析到那个版本,否则什么都不返回。请求 `1.8.12` 却静默拿 到 `1.9.0`,是那种要到产物里才被发现的答案。 ### 依赖产出的 host 工具(2026.8.5.1+) @@ -438,7 +438,7 @@ mcpp 会播下一个带着该声明的占位文件,使 prepare 期的扫描与 **包自己处理不了这件事**,而原因值得知道 —— 最直觉的那道防护编译不过: ```cpp -if constexpr (requires { mcpp::runner("qemu"); }) // ✗ 名字不存在时是硬错误 +if constexpr (requires { mcpp::runner("qemu"); }) // 名字不存在时是硬错误 mcpp::runner("qemu"); ``` @@ -512,7 +512,7 @@ PATH=<被声明环境的 bin>: 于是构建程序里的裸名命令,在每一台构建它的机器上都解析到项目点名的那个环境 里面。 -⚠️ **只对声明了的项目生效。** 没有 `[xlings].subos` 的项目拿到的是 mcpp 启动时 +**只对声明了的项目生效。** 没有 `[xlings].subos` 的项目拿到的是 mcpp 启动时 的 `PATH`,逐字节不变。把一个共享目录放到每个项目前面,会让「构建看见什么」取 决于这台机器上还装过什么——同一台机器上的两个项目彼此一致,而同一个项目在两台 机器上不一致。 @@ -520,7 +520,7 @@ PATH=<被声明环境的 bin>: 前置而非替换的理由:构建程序理应会调 `git`、`python3` 或 shell,这些都不在 SubOS 里。前置让被声明的环境成为默认答案;宿主仍在其后可达。 -⚠️ **`command -v` 回答的是这台机器,不是这次构建。** 在此之前,构建程序拿 +**`command -v` 回答的是这台机器,不是这次构建。** 在此之前,构建程序拿 `PATH` 去问一个被声明过的工具,可能问到无关的那个——实测于 `qemu-system-riscv64`:答案是一个执行时报「is not installed in this subos」的 shim,而可用的那份就在项目自己的环境里,根本不在 `PATH` 上。 @@ -539,6 +539,21 @@ shim,而可用的那份就在项目自己的环境里,根本不在 `PATH` 上。 下面这些从第一个规则包 `mcpplibs.grpcgen` 归纳而来,每一条特征都单独判过是必然还是偶然。 它们是指引而非规则,因为其中没有一条能给出引擎可以检查的判据。 +**模块名由规则的源码声明,`mcpp.*` 是保留前缀。** host 模块以其接口单元声明的名字注册, +而不是以包名注册,所以 `export module mcpp.rules.spirv;` 就是消费者 import 的那个名字。 +官方插件集中在一个包里,`mcpp:plugins`(仓库 `mcpp-community/mcpp-plugins`):规则包命名为 +`mcpp.rules.`,构建期工具命名为 `mcpp.tools.`,每个成员由该包的一个 feature 选择 +(见 [`host-module = true`](../05-mcpp-toml.md))。`mcpp.build.*` 是引擎自己的模块族, +不用于插件。引擎判定不了谁是官方,所以检查以包的**命名空间**为键,两者不一致时告警 —— + + warning: build rule 'mcpplibs.plugins' declares the module + 'mcpp.rules.spirv'; the 'mcpp.' prefix is reserved for rules maintained by + the mcpp project. + +什么都不会坏;只是这个名字声称了一个该包并不具有的来源。项目之外的规则自选前缀。 +`examples/09-cuda-kernel` 与 `examples/10-vulkan-compute` 像任何工程一样从 `mcpp:plugins` +消费 `mcpp.rules.cuda` 与 `mcpp.rules.spirv`。 + **分层不得有断崖,且上层必须是下层的组合。** `generate_all(opt)` **就是** `submit(plan_all(opt))`,`.grpc = true` **就是** `.plugins = {cpp()}`。超过两个旋钮之后, 无法继续下降的消费者会手写六十行绕开规则,而那六十行随后就与规则悄悄漂移。 diff --git a/docs/zh/11-machine-output.md b/docs/zh/11-machine-output.md index 06093d08..062eac71 100644 --- a/docs/zh/11-machine-output.md +++ b/docs/zh/11-machine-output.md @@ -94,7 +94,7 @@ $ echo $? | 70 | 内部错误(未捕获异常) | | 127 | 未知命令 | -⚠️ **`1` 可能与 stdout 上的信封同时出现。** `mcpp xpkg parse` 会把一份违反名字形态的 +**`1` 可能与 stdout 上的信封同时出现。** `mcpp xpkg parse` 会把一份违反名字形态的 描述符作为 JSON 报出来**并且**退 1:文档就是答案,退出码说明这个答案是一次拒绝。§1 仍然成立 —— 解析 stdout,不要按退出码分支 —— 但一个把任何非零退出都当作「没有输出」 的客户端,会丢掉它已经拿到的文档。 @@ -254,7 +254,7 @@ mcpp toolchain list --format json `{target, note, toolchain, pin, status, default}`,`status` 取 `installed` / `available` / `via dependency graph` / `planned`。 -⚠️ **`toolchain` 与 `pin` 不是同一个字段写两遍。** `toolchain` 是这一行关联到 +**`toolchain` 与 `pin` 不是同一个字段写两遍。** `toolchain` 是这一行关联到 什么 —— 已装的行是装了的载荷,词表行是那一行的约定。`pin` 只承载目标表的约定, 没有约定的行为空。`x86_64-linux-gnu` 装了 gcc 而根本没有约定,所以要挑「约定是 gcc 的行」必须读 `pin`。 @@ -277,7 +277,7 @@ mcpp why toolchain [--target ] [--toolchain ] --format json | `cLibrary` | `{mode, path, origin, suppliesTarget}`;`mode` 取 `sysroot` / `payload-first` / `none`,`origin` 取 `payload` / `subos` / `host` / `none` | | `layers[]` | 目标侧五层:`{layer, interface, impl, origin, subset}` | -⭐ **`compiler.chosenBy` 回答「为什么是它」。** `{origin, requiredBy, replaced}` +**`compiler.chosenBy` 回答「为什么是它」。** `{origin, requiredBy, replaced}` —— `origin` 与构建状态行用的是同一句话(`[toolchain] in mcpp.toml`、 `your default`、`target default`、`required by the dependency graph`、 `first-run default`)。当某条 `requires = ["mcpp:compiler=…"]` 做了决定时, @@ -293,7 +293,7 @@ mcpp why toolchain [--target ] [--toolchain ] --format json 没有它,要问「为什么」的消费方只能去解析状态行 —— 而那正是这份文档存在的理由所要 消除的字符串匹配。 -⚠️ **`cLibrary` 与 `layers[].c-abi` 回答的是两个问题,`suppliesTarget` 说明哪一个 +**`cLibrary` 与 `layers[].c-abi` 回答的是两个问题,`suppliesTarget` 说明哪一个 管用。** `cLibrary` 描述的是**载荷**的链接模型 —— 一份由载荷供给的 C 库会用到的 搜索路径。`layers[].c-abi` 描述的是**这次构建**。当依赖供给 C 库时两者分叉,而在 `suppliesTarget` 之前,同一份文档同时报出两者却没有任何字段说明该信哪个: @@ -308,7 +308,7 @@ mcpp why toolchain [--target ] [--toolchain ] --format json 是**新增一个字段**而不是给 `cLibrary` 改名或给 `mode` 加取值,因为 §7 承诺字段 只增不删、且一个字段的含义永不改变。 -⚠️ **2026.9.1.1 起,载荷供给的 glibc 让 `layers[].interface` 的**取值**变了** —— +**2026.9.1.1 起,载荷供给的 glibc 让 `layers[].interface` 的**取值**变了** —— 从 `gnu` 变为 `glibc`,Windows 上从 `gnu` 变为 `ucrt`。字段的**含义**没变(它仍然是 「哪个实现」),所以 §7 仍然成立;变的是它不再报三元组的 env 段 —— 那是一次请求而 不是一个实现,也不是任何一个 C 库的名字。现在的取值就是 @@ -316,7 +316,7 @@ mcpp why toolchain [--target ] [--toolchain ] --format json `cfg(c-abi = …)` 谓词里与它们比较。按字面量 `gnu` 取值的客户端需要更新; `musl`、`picolibc`、`libSystem` 不受影响。 -⭐ **`reason` 是一个记号,不是一句话。** 拒绝的消息仍然写给人看,仍然点名目标、 +**`reason` 是一个记号,不是一句话。** 拒绝的消息仍然写给人看,仍然点名目标、 规则与出路;而一个要给结果分类的程序读 `reason`: | `reason` | | @@ -336,11 +336,11 @@ mcpp why toolchain [--target ] [--toolchain ] --format json | `accel-mismatch` | 一条 `[build] sources` 条目被约束到本次构建未覆盖的设备集合 | | `other` | 一处还没有被命名的拒绝分支 | -⚠️ **只要问题被回答了就退 0,包括答案是「拒绝」。** 「它能不能构建,不能的话 +**只要问题被回答了就退 0,包括答案是「拒绝」。** 「它能不能构建,不能的话 为什么」被「不能,因为这一行的 pin 是能力陈述」完整地回答了。非零退出的含义是 这次查询本身没跑起来。 -⚠️ **它声明的 effects 故意偏宽。** `--protocol-version` 为这条命令列出 +**它声明的 effects 故意偏宽。** `--protocol-version` 为这条命令列出 `network`、`write-global-cache` 与 `exec-build-script`:答案来自与构建同一次的 解析,而那可能拉取包、安装载荷、并运行某个依赖的构建程序。客户端是在**运行之前** 读这张表来决定放不放行的,漏报一项就是一句不成立的安全承诺。 @@ -378,7 +378,7 @@ mcpp test [pattern] [--workspace] --message-format json | `not_run_reason` | 它们共同的原因,或 `""` | | `elapsed_ms`、`build_ms`、`run_ms` | 墙钟时间,分段 | -⚠️ **`not_run` 既不是 `pass` 也不是 `run_fail`,退出码也这么说(2026.9.2.1)。** +**`not_run` 既不是 `pass` 也不是 `run_fail`,退出码也这么说(2026.9.2.1)。** 本机无法加载测试产物(交叉目标未声明 runner 时的 `Exec format error`),或声明的 `[target.].runner` 找不到、启动不了时,测试为 `not_run`。这是关于整次调用的 事实:确立一次,其余测试直接报告为 `not_run` 而不再启动,进程以 **2** 退出。退出码 1 diff --git a/docs/zh/12-binary-distribution.md b/docs/zh/12-binary-distribution.md index 2b5cb5a4..7adaf0da 100644 --- a/docs/zh/12-binary-distribution.md +++ b/docs/zh/12-binary-distribution.md @@ -255,7 +255,7 @@ SubOS farm,全都是在真正要运行它的那台机器上算出来的 —— error while loading shared libraries: libstdc++.so.6: cannot open shared object file ``` -⚠️ **`$ORIGIN` 不是解药。** 在真实的包上、把构建机的 store 变成不可达之后实测: +**`$ORIGIN` 不是解药。** 在真实的包上、把构建机的 store 变成不可达之后实测: | 发货 `.so` 上的状态 | 消费方 `DT_RPATH` 被继承? | 结果 | |---|---|---| @@ -287,16 +287,16 @@ Mach-O 上打包器会读出 `LC_RPATH` 并在包会携带它时告警;自动改 | | 状态 | |---|---| -| `kind = "lib"`(静态) | ✅ 所有 target,三平台都测了 | -| `kind = "shared"` on Linux/ELF | ✅ —— 包里同时带链接名与 SONAME,且不含构建机的 loader 路径 | -| `kind = "shared"` on PE / MinGW(`*-windows-gnu`) | ✅ —— 包里同时带 `.dll` **和它的导入库** | -| `kind = "shared"` on Mach-O(`*-macos`) | ✅ —— install name 是 `@rpath/`,`.dylib` 可重定位。`LC_RPATH` 只报告,尚未改写 | -| `kind = "shared"` on PE / MSVC(`*-windows-msvc`) | ✅ —— mcpp 生成 `.def`;见下 | -| `kind = "shared"` on `*-musl` | ❌ musl target 是静态链接的 | -| 一个包同时携带同一 triple 的两套 ABI(gcc **与** clang) | ❌ leg 选择是 `cfg(arch/os/env)`;一个 ABI 发一个包 | -| 发布预编译 BMI | ❌ 未尝试;BMI 与编译器构建逐位绑定 | -| 把依赖打包进去 | ❌ 改为声明依赖(见上) | -| 用**原生 `cl.exe`** 消费这种包 | ✅ —— 经方言中立的链接意图;见下 | +| `kind = "lib"`(静态) | 所有 target,三平台都测了 | +| `kind = "shared"` on Linux/ELF | —— 包里同时带链接名与 SONAME,且不含构建机的 loader 路径 | +| `kind = "shared"` on PE / MinGW(`*-windows-gnu`) | —— 包里同时带 `.dll` **和它的导入库** | +| `kind = "shared"` on Mach-O(`*-macos`) | —— install name 是 `@rpath/`,`.dylib` 可重定位。`LC_RPATH` 只报告,尚未改写 | +| `kind = "shared"` on PE / MSVC(`*-windows-msvc`) | —— mcpp 生成 `.def`;见下 | +| `kind = "shared"` on `*-musl` | musl target 是静态链接的 | +| 一个包同时携带同一 triple 的两套 ABI(gcc **与** clang) | leg 选择是 `cfg(arch/os/env)`;一个 ABI 发一个包 | +| 发布预编译 BMI | 未尝试;BMI 与编译器构建逐位绑定 | +| 把依赖打包进去 | 改为声明依赖(见上) | +| 用**原生 `cl.exe`** 消费这种包 | —— 经方言中立的链接意图;见下 | ### MSVC ABI 上的符号导出 @@ -408,17 +408,17 @@ e2e 套件按宿主能力给每条测试开门,所以「套件是绿的」和「 | 说法 | linux | macOS | windows | |---|---|---|---| -| 布局、两种接口模式、闭包、两道闸门、workspace 根、指名 target、`sources = []`、裸三元组谓词 | ✅ | ✅ | ✅ | -| 多 target 包,两个 target 的产物**同一个**产物名(`gnu` + `musl`) | ✅ | *不可能* | — | -| 多 target 包,两个 target 的产物**两个**产物名(`msvc` + `mingw`) | — | *不可能* | ✅ | -| 跨 OS 边界的多 target 包(含一个 PE target) | ✅ | — | — | -| `lib.exe /REMOVE:` 真的删掉了 | — | — | ✅ | -| PE 共享库:产出、打包、链接、运行 | ✅(wine) | — | — | -| Mach-O 共享库离开构建树仍可加载 | — | ✅ | — | -| MSVC 以「导出」为理由拒绝 `kind = "shared"` | — | — | ✅ | +| 布局、两种接口模式、闭包、两道闸门、workspace 根、指名 target、`sources = []`、裸三元组谓词 | yes | | yes | +| 多 target 包,两个 target 的产物**同一个**产物名(`gnu` + `musl`) | yes | *不可能* | — | +| 多 target 包,两个 target 的产物**两个**产物名(`msvc` + `mingw`) | — | *不可能* | yes | +| 跨 OS 边界的多 target 包(含一个 PE target) | yes | — | — | +| `lib.exe /REMOVE:` 真的删掉了 | — | — | yes | +| PE 共享库:产出、打包、链接、运行 | (wine) | — | — | +| Mach-O 共享库离开构建树仍可加载 | — | yes | — | +| MSVC 以「导出」为理由拒绝 `kind = "shared"` | — | — | yes | | 已发布的 mcpp 消费本版产出的包 | 仅本机 | 仅本机 | 仅本机 | -| 打包出的 `.so` 不含构建机 loader 路径,**且把缺陷放回去时守卫看得见** | ✅ | — | — | -| strip 过的静态归档仍可链接、strip 过的共享库仍可加载,`--no-strip` / `[pack] strip` / `--debug-symbols` 两侧都钉 | ✅ | — | — | +| 打包出的 `.so` 不含构建机 loader 路径,**且把缺陷放回去时守卫看得见** | yes | — | — | +| strip 过的静态归档仍可链接、strip 过的共享库仍可加载,`--no-strip` / `[pack] strip` / `--debug-symbols` 两侧都钉 | yes | — | — | | ELF 编辑器在 ELF32 与大端上的行为 | 单测 | 单测 | 单测 | *不可能* 不是缺口:macOS 宿主只能服务一个 target(`host_can_serve`, diff --git a/docs/zh/13-baremetal.md b/docs/zh/13-baremetal.md index a705a41a..47166623 100644 --- a/docs/zh/13-baremetal.md +++ b/docs/zh/13-baremetal.md @@ -44,7 +44,7 @@ A-profile 有真正的 MMU 与页表走查器,所以它是第一个能被问「* 正是 32 位的 `r1` 装不下「原因」与「状态」两者。把块传给 `0x18`,一切打印都正确,而 **退出状态是错的**。 -⚠️ 这是 **AArch32** 的事实,对 M-profile 与 A-profile 同样成立。实测两次:一个 +这是 **AArch32** 的事实,对 M-profile 与 A-profile 同样成立。实测两次:一个 ARMv7-A 程序退 0 而报回 1;`openarch` 的一个 Cortex-M 示例打印了 `both tasks observed preemption` 然后退 1 —— 对**输出**的每一条断言都通过了。只看 自己打印了什么的板子看不出这个差别,所以 `tests/e2e/332` 与 `336` 都读 `$?`,而且都 @@ -61,7 +61,7 @@ ARMv7-A 程序退 0 而报回 1;`openarch` 的一个 Cortex-M 示例打印了 `eabi`/`eabihf` 后缀即浮点 ABI,clang 直接从 triple 读出它:实测 llvm 22.1.8, `thumbv7em-none-eabi` 得到 `-mfloat-abi soft`,`thumbv7em-none-eabihf` 得到 `hard`。 -⚠️ **浮点 ABI 并不决定 FPU 是否被使用。** 它约束浮点值如何跨越函数边界,不约束 +**浮点 ABI 并不决定 FPU 是否被使用。** 它约束浮点值如何跨越函数边界,不约束 编译器在函数内部可以发什么指令,而 `thumbv7em` 架构蕴含 FPv4-SP。实测:在软浮点 ABI 下,clang 对一次 float 乘法仍然发出 `vmul.f32`。在没有 FPU 的 Cortex-M4 上, 这条指令在运行期触发异常 —— 而编译与链接都是干净的。因此每一个软浮点行都携带 @@ -83,16 +83,16 @@ freestanding 构建以 `-ffunction-sections -fdata-sections` 编译,以 `--gc-se 预编译归档、目标又有若干兆字节时,这不花什么代价;而 Cortex-M 器件只有几十 KB, 没有死代码段消除,每个镜像都会装进整份 C 库。 -⚠️ **链接脚本因此以一种新的方式承重。** 中断向量表不被任何东西引用 —— 硬件按地址 +**链接脚本因此以一种新的方式承重。** 中断向量表不被任何东西引用 —— 硬件按地址 读取它 —— 所以 `--gc-sections` 会把它回收。板级脚本必须写 `KEEP(*(.vectors))`。 实测:有这条 `KEEP` 时,无人调用的函数被丢弃、向量表被保留、镜像能够启动。 -⚠️ 后两行**默认**没有 C 库,这是声明而非遗漏:这两行的第一个消费者 —— 机器机制层 +后两行**默认**没有 C 库,这是声明而非遗漏:这两行的第一个消费者 —— 机器机制层 `openarch` —— 一个 C 库符号都不引用,而**如果四行里没有一行默认在这一层,就没有 任何东西在证明这一层可用**。空列在这里的含义与清单里 `[target.].sysroot = ""` 完全一致。 -⭐ **这两行的 C 库是可声明的,不是不存在的**(mcpp 2026.8.21.3+)。 +**这两行的 C 库是可声明的,不是不存在的**(mcpp 2026.8.21.3+)。 `xim:picolibc-aarch64` 与 `xim:picolibc-x86` 已在索引里;想要它的工程自行声明, 而那与它换用另一份 C 库是同一个动作: @@ -107,7 +107,7 @@ sysroot = "xim:picolibc-aarch64@1.8.12" ### x86_64 这一行不止是四个字符串 -⚠️ **一个目标行通常就是两张表里的两条记录,不含引擎代码。这一行需要引擎代码, +**一个目标行通常就是两张表里的两条记录,不含引擎代码。这一行需要引擎代码, 而原因是 clang 的属性而非指令集的属性。** clang 由 triple 选择工具链。它为 arm、aarch64、riscv 备有 *BareMetal* 工具链, @@ -433,7 +433,7 @@ RAM 从哪开始、哪个模拟器启动它,没有一条是 C 库事实: riscv-virt-rt = { version = "0.5.0", features = ["nolibc"] } ``` -⚠️ `std-freestanding-nolibc` 正是该 feature 解析到的包,而**直接**把它与 C 库并用时 +`std-freestanding-nolibc` 正是该 feature 解析到的包,而**直接**把它与 C 库并用时 是**静默**失败而非响亮失败。C 库以归档形式 发布,归档成员只在符号仍未定义时才被拉入;而依赖包的目标文件无条件进入链接。于是该包 先定义了 `memcpy`,C 库的成员从不被拉入,构建**成功** —— 程序拿到的是逐字节实现而不是 @@ -575,7 +575,7 @@ error: no runner is configured for 'riscv64-none-elf' — a freestanding artifac 并且遵守同样两个开关:在 `--offline` / `MCPP_OFFLINE` 或 `MCPP_NO_AUTO_INSTALL` 下 mcpp 转为拒绝,并列出包名以便手动安装。 -⚠️ **构建程序仍然不得假定目录存在。** 供给发生在**声明**了这些 deps 的那个包上; +**构建程序仍然不得假定目录存在。** 供给发生在**声明**了这些 deps 的那个包上; 构建程序可以从没有发生过供给的路径被走到 —— 一个自己什么都没声明的工程的依赖、 上面两个开关拒绝掉的环境 —— 所以 `xpkg_dir` 仍可能返回空,此时必须说出来,而不是 发出一个坏掉的 runner: diff --git a/docs/zh/14-target-side.md b/docs/zh/14-target-side.md index ece0413e..a8c14a2e 100644 --- a/docs/zh/14-target-side.md +++ b/docs/zh/14-target-side.md @@ -231,13 +231,13 @@ include_dirs = ["config/picolibc"] cxxflags = ["-D_GNU_SOURCE"] ``` -⚠️ **层名的是库,不是三元组的 env 段。** 二者在 `musl` 上重合,在 `gnu` 上分叉: +**层名的是库,不是三元组的 env 段。** 二者在 `musl` 上重合,在 `gnu` 上分叉: 在 Linux 上该段请求的是 glibc,在 Windows 上它命名的是工具链的 MinGW 形态, 而后者的 C 运行时与 MSVC 形态链接的是同一个 UCRT。写法是 `c-abi = "glibc"`, 而非 `c-abi = "gnu"`;与答案相对的那个「请求」是 `env = "gnu"` —— 另一个问题 (`docs/spec/target-side.md` §3.4)。 -⚠️ **`env` 与 `c-abi` 不可互换。** `env` 是三元组**请求**的东西;`c-abi` 是图与载荷 +**`env` 与 `c-abi` 不可互换。** `env` 是三元组**请求**的东西;`c-abi` 是图与载荷 **回答**的东西。依赖图里的 `openkal-musl` 会在 `x86_64-linux-gnu` 三元组下供给 musl, 而只有 `c-abi` 看得见这件事。 diff --git a/docs/zh/15-openkal-cross.md b/docs/zh/15-openkal-cross.md index 7bb08e67..97b77e60 100644 --- a/docs/zh/15-openkal-cross.md +++ b/docs/zh/15-openkal-cross.md @@ -110,7 +110,7 @@ mcpp build --target x86_64-linux `msvc` 是 PE 加微软的 —— 而两者都与不止一种 C 库相容。 因此该项报出被限定在该段命名 C 库的那些平台上。 -⭐ 沉默作为**诊断**是对的,作为**报告**是不够的。读者看到 +沉默作为**诊断**是对的,作为**报告**是不够的。读者看到 ``` Target x86_64-windows-gnu → x86_64-w64-windows-gnu @@ -130,7 +130,7 @@ Target x86_64-windows-gnu → x86_64-w64-windows-gnu 第一行说明 `c-abi musl` 是老实的:MinGW 的 C 运行时一点没链进来。 后两行是 `gnu` 实际选中的东西 —— Itanium C++ ABI,而不是微软那套。 -⭐ 而它**不对应报告里的任何一行**,这正是要点。五层记录的是每一层 +而它**不对应报告里的任何一行**,这正是要点。五层记录的是每一层 **由谁供给**;`gnu` 命名的是这些**对象遵循哪套约定**,是若干层必须一致的 横切事项。把它读成 `c++-abi libc++` 是第二个错误答案:libc++ 是标准库的 一个实现,libstdc++ 是另一个,两者都坐在 Itanium ABI 上。 @@ -196,7 +196,7 @@ C++ 运行时是图供给的那份,没有任何 `#if` 区分目标。它不是 如何表现 —— 名字是否区分大小写、一个时钟的粒度是多少。接口是否存在, 由更早的东西回答:依赖图,以及退而求其次的链接器。 -⚠️ 这个区分容易丢,因为查询是数据对象上的内联函数:一个程序**只是提问** +这个区分容易丢,因为查询是数据对象上的内联函数:一个程序**只是提问** 「有没有文件系统」,就取了 `kal_fs_props` 的地址,于是在整份源码没有任何 文件系统调用的情况下链接失败。在后端把那个字定义为零可以消掉这个错误, 而它恰是该条禁止的唯一补法 —— 程序随后越过了链接器存在的意义。 diff --git a/docs/zh/16-the-target-triple.md b/docs/zh/16-the-target-triple.md index 80f696b6..11741dfc 100644 --- a/docs/zh/16-the-target-triple.md +++ b/docs/zh/16-the-target-triple.md @@ -37,7 +37,7 @@ C 库。选中 `x86_64-linux-musl` 就是选中 musl-gcc 载荷,选中 | `none` | **对象格式** | `elf` | | `macos` | 无;该平台不带这一段 | — | -⭐ 在 Windows 上这一段经常被读错,因为 `gnu` 这个词暗示了一个并不在场的 C 库。 +在 Windows 上这一段经常被读错,因为 `gnu` 这个词暗示了一个并不在场的 C 库。 对一份按构建期体系为 `x86_64-windows-gnu` 构建的产物实测: | 观测 | 值 | @@ -147,7 +147,7 @@ warning: the target name asks for the `gnu` C ABI and the dependency graph suppl Target x86_64-windows-gnu → x86_64-w64-windows-gnu (gnu selects the Itanium C++ ABI, not a C library) ``` -⭐ 而它**不对应报告里的任何一行**,这正是要点。五层记录的是每一层 +而它**不对应报告里的任何一行**,这正是要点。五层记录的是每一层 **由谁供给**;那一段命名的是这些**对象遵循哪套约定**,是若干层必须一致的 横切事项。把它读成 `c++-abi libc++` 是第二个错误答案,因为 libstdc++ 坐在同一套 ABI 上。 @@ -189,7 +189,7 @@ x86_64-w64-mingw32-g++ → x86_64-w64-mingw32 (只能发这个) clang++ → x86_64-unknown-linux-gnu,而 --target= 可改 ``` -⭐ 这就是传统预构建体系**不传 `--target`** 的原因:那份载荷的编译器以它唯一 +这就是传统预构建体系**不传 `--target`** 的原因:那份载荷的编译器以它唯一 能发的目标命名,选目标等于选载荷。也是构建期体系只需要一个编译器的原因。 ### MinGW 按 GCC 的约定给自己命名 @@ -200,7 +200,7 @@ MinGW 自己的三元组是 `x86_64-w64-mingw32`: |---|---|---| | arch | `x86_64` | | | vendor | `w64` | 项目名 `mingw-w64`,用以区别于已停滞的原 `mingw32` 项目 | -| os | **`mingw32`** | ⭐ MinGW 把**自己**放在 OS 位 | +| os | **`mingw32`** | MinGW 把**自己**放在 OS 位 | | env | (无) | 三段就是全名 | 这套约定源自 autoconf 的 `config.guess`,那里 OS 段命名的是目标的运行环境 —— @@ -223,7 +223,7 @@ LLVM x86_64 - unknown - windows - gnu ^vendor ^os ^env ``` -⭐ **`mingw32` 从 OS 位被拆成 `windows` 加 `gnu`。** `gnu` 这个取值之所以 +**`mingw32` 从 OS 位被拆成 `windows` 加 `gnu`。** `gnu` 这个取值之所以 存在,正是因为 LLVM 需要给拆剩下的那一半起个名字。它的含义是 「MinGW/Itanium 这一支 ABI」,在 Windows 上从来不是「C 库是 glibc」—— 同一个词在不同操作系统下承担不同职责,这是 LLVM 词表的既有事实, @@ -245,7 +245,7 @@ Target x86_64-windows-gnu → x86_64-w64-windows-gnu ^ mcpp ^ LLVM ``` -⚠️ 把第三套词表独立出来,正是 mcpp 能命名 LLVM 命名不了的东西的原因。 +把第三套词表独立出来,正是 mcpp 能命名 LLVM 命名不了的东西的原因。 实测 llvm 22.1.8:`windows` 配 `musl` 环境能被三元组解析器接受, 而编译器会崩: @@ -274,7 +274,7 @@ clang++ --target=x86_64-pc-windows-musl -c t.cpp 目标命名的是一台机器。它不指定谁来编译,也不指定它的 C 库从哪来 —— 那是另外两个 选择,而同一个目标字符串在每种选择下都是不同的构建。 -⚠️ **「来自载荷」不是某一份固定载荷**,而是所选编译器带来的那一份;gcc 与 clang +**「来自载荷」不是某一份固定载荷**,而是所选编译器带来的那一份;gcc 与 clang 带法不同:gcc 一个目标一份载荷、驱动带三元组前缀,而一个 `clang++` 打它构建时支持 的每个目标。同一台宿主、同一份源码实测: @@ -304,7 +304,7 @@ error: target 'x86_64-linux-musl' takes its C library from the 'gcc@16.1.0' payload, and 'llvm@22.1.8' has none here. ``` -⚠️ **2026.8.26.1 之前这会把整个构建跑完,然后在链接上失败**,报 +**2026.8.26.1 之前这会把整个构建跑完,然后在链接上失败**,报 `crtbeginT.o (bare name — the linker cannot resolve it)`——对症状准确,对决定沉默。 clang 是可重定向的,自己不带 C 库,于是去够一份 gcc 安装;在恰好装了系统 mingw 的 机器上,同样写法用于 `x86_64-windows-gnu` 够到的是 @@ -322,7 +322,7 @@ default = "llvm@22.1.8" 这就是 [`examples/06-openkal-cross`](../../examples/06-openkal-cross),也是那句 拒绝里为什么点名 openkal。 -⚠️ **裸机行与 `x86_64-windows-musl` 行的工具链不是约定**,根本不可被推翻—— +**裸机行与 `x86_64-windows-musl` 行的工具链不是约定**,根本不可被推翻—— 见[第 03 章](03-toolchains.md)。 ### 而依赖图会整个替换这一轴 @@ -335,7 +335,7 @@ default = "llvm@22.1.8" | `x86_64-windows-gnu` | openkal(openkal-windows,图) | musl(图) | libc++(图) | | `x86_64-windows-musl` | openkal(openkal-windows,图) | musl(图) | libc++(图) | -⚠️ **看两张表里的 `x86_64-windows-gnu`。** 载荷供给时它的 C 库是 `gnu`,即 MinGW +**看两张表里的 `x86_64-windows-gnu`。** 载荷供给时它的 C 库是 `gnu`,即 MinGW CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— 而 mcpp 在 2026.8.24.6 之前无法说清是哪一个:同一条 `--target x86_64-windows-gnu` 产出的东西体积差 16.7 倍、依赖的 DLL 完全不同。 @@ -350,7 +350,7 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— 上面两条轴 —— 用哪个编译器、C 库从哪来 —— 是工程做的选择。第三条不是:它是构建 运行在哪台机器上。 -⭐ **这条轴是 mcpp 自己发布的那一组,而且是 (os, arch) 不是 os。** +**这条轴是 mcpp 自己发布的那一组,而且是 (os, arch) 不是 os。** `release.yml` 发布四份宿主二进制: | 构建机 | 发布资产 | CI runner | @@ -360,7 +360,7 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— | `macos-arm64` | `mcpp--macosx-arm64.tar.gz` | `macos-14` | | `windows-x86_64` | `mcpp--windows-x86_64.zip` | `windows-2022` | -⚠️ **两台 Linux 不是同一台。** `x86_64-linux-gnu` 需要本机架构的 `xim:glibc` 与 +**两台 Linux 不是同一台。** `x86_64-linux-gnu` 需要本机架构的 `xim:glibc` 与 `xim:linux-headers` 载荷,而它们只为宿主自己的架构存在 —— 所以那一行从 `linux-x86_64` 够得着,从 `linux-aarch64` 够不着;`aarch64-linux-gnu` 是镜像的 情形,两台上都是 `planned`。把它们并成 `linux`,一台会把另一台的行覆盖掉。 @@ -369,23 +369,23 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— | target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | |---|---|---|---|---|---|---| -| `x86_64-linux-gnu` | verified | — | ✅ 载荷 | — | — | — | +| `x86_64-linux-gnu` | verified | — | 载荷 | — | — | — | | `aarch64-linux-gnu` | planned | — | planned | planned | planned | planned | -| `x86_64-linux-musl` | verified | `gcc@16.1.0` | ✅ 载荷 | ✅ 载荷 | — | ✅ 载荷 | -| `aarch64-linux-musl` | verified | `gcc@16.1.0` | ✅ 载荷 | ✅ 载荷 | — | — | +| `x86_64-linux-musl` | verified | `gcc@16.1.0` | 载荷 | 载荷 | — | 载荷 | +| `aarch64-linux-musl` | verified | `gcc@16.1.0` | 载荷 | 载荷 | — | — | | `riscv64-linux-musl` | planned | — | planned | planned | planned | planned | -| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | ✅ 载荷 | ✅ 载荷 | — | ✅ 载荷 | -| `x86_64-windows-musl` | preview | `llvm@22.1.8` | ⚙ 图 | ⚙ 图 | ⚙ 图 | ✅ 载荷 | -| `x86_64-windows-msvc` | verified | — | — | — | — | ✅ 系统 | -| `aarch64-macos` | verified | — | — | — | ✅ SDK | — | +| `x86_64-windows-gnu` | verified | `gcc@16.1.0` | 载荷 | 载荷 | — | 载荷 | +| `x86_64-windows-musl` | preview | `llvm@22.1.8` | 图 | 图 | 图 | 载荷 | +| `x86_64-windows-msvc` | verified | — | — | — | — | 系统 | +| `aarch64-macos` | verified | — | — | — | SDK | — | | `x86_64-macos` | planned | — | planned | planned | planned | planned | -| `riscv64-none-elf` | verified | `llvm@22.1.8` | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | -| `riscv32-none-elf` | verified | `llvm@22.1.8` | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | -| `aarch64-none-elf` | preview | `llvm@22.1.8` | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | -| `x86_64-none-elf` | preview | `llvm@22.1.8` | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | ✅ 载荷 | +| `riscv64-none-elf` | verified | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `riscv32-none-elf` | verified | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `aarch64-none-elf` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `x86_64-none-elf` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | -`✅ 载荷` 这里有工具链载荷产出它 · `⚙ 图` 没有载荷,但依赖可以供给系统 · -`✅ 系统` 在机器上被找到,不是 mcpp 装的 · `✅ SDK` 平台自己的 · +`载荷` 这里有工具链载荷产出它 · `图` 没有载荷,但依赖可以供给系统 · +`系统` 在机器上被找到,不是 mcpp 装的 · `SDK` 平台自己的 · `—` 从这台宿主够不着 · `planned` 词表里有,还没有任何东西接线。 ### 列背后的规则 @@ -400,7 +400,7 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— | `aarch64-macos` | macOS | SDK 是那台机器的 | | `*-none-elf` | 每一台 | clang 与 lld 按构造就是交叉编译器 | -⚠️ **一个 `—` 讲的是载荷,不是可能性。** `host_can_serve` 回答的是「这里有没有 +**一个 `—` 讲的是载荷,不是可能性。** `host_can_serve` 回答的是「这里有没有 载荷产出它」,而依赖图可以改为供给系统 —— 这就是 `x86_64-windows-musl` 在 Linux 上显示 `via dependency graph`、并在那里产出真正的 PE32+ 的原因。 @@ -412,7 +412,7 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— [`tests/matrix/expected.tsv`](../../tests/matrix/expected.tsv) 比对,键是 `(mode, host, target, compiler)`。 -⚠️ 每台把什么解析成自己的目标,与它的名字给人的印象并不一致: +每台把什么解析成自己的目标,与它的名字给人的印象并不一致: | runner | 它解析出的宿主目标 | |---|---| @@ -423,7 +423,7 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— 本章三条判据都曾假设了 Linux 上的那个巧合,并在其它宿主上被纠正。 -⚠️ **每台的格数不是常数。** 它取决于那台机器装了什么,而同一台 runner 在相邻两轮 +**每台的格数不是常数。** 它取决于那台机器装了什么,而同一台 runner 在相邻两轮 里被测到工具链不同。所以比对断言的是**扫描真的产出了行**、以及**期望表点名的每一 行都被跑到**,而不是一个总数:一格因为载荷没被恢复而消失,与一格通过了,在退出码 上没有区别。 @@ -442,7 +442,7 @@ runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", ``` `sysroot = ""` 选定零 libc 档:编译行上没有 C 库,链接上也没有。 -⚠️ **缺席 `sysroot` 键是另一个答案** —— 它继承该目标行自己的默认值。 +**缺席 `sysroot` 键是另一个答案** —— 它继承该目标行自己的默认值。 见[第 13 章](13-baremetal.md)。 ## 参考 diff --git a/docs/zh/17-the-project-environment.md b/docs/zh/17-the-project-environment.md index 46c042a8..66359452 100644 --- a/docs/zh/17-the-project-environment.md +++ b/docs/zh/17-the-project-environment.md @@ -47,12 +47,12 @@ PATH=<被声明环境的 bin>: 因此构建程序里把 `qemu-system-riscv64` 写成裸名,拿到的就是那个环境里的副本。 这条通道的契约见第 7 章。 -⚠️ **只对声明了的项目生效。** 没有 `[xlings].subos` 的项目拿到的是 mcpp 启动时 +**只对声明了的项目生效。** 没有 `[xlings].subos` 的项目拿到的是 mcpp 启动时 的 `PATH`,逐字节不变。把一个共享目录放到每个项目前面,会让「构建看见什么」 取决于这台机器上还装过什么——同一台机器上的两个项目彼此一致,而同一个项目在 两台机器上不一致。是声明本身把它放到了前面。 -⚠️ **前置而非替换。** 构建程序理应会调 `git`、`python3` 或 shell,这些都不在 +**前置而非替换。** 构建程序理应会调 `git`、`python3` 或 shell,这些都不在 SubOS 里。前置让被声明的环境成为默认答案;其余的仍在它后面可达。 ### 2.1 哪些版本钉生效(2026.9.3+) diff --git a/docs/zh/18-devices.md b/docs/zh/18-devices.md index 714f9d7e..a4cff365 100644 --- a/docs/zh/18-devices.md +++ b/docs/zh/18-devices.md @@ -30,7 +30,7 @@ runner,于是开发者从模拟器换到真板时,敲的命令不变。 具名 runner 服务剩下的部分:只写不跑、看串口、起调试服务端、擦片、部署但不启动。 -⚠️ **引擎不认识任何 runner 名字。** `flash`、`serve`、`deploy`、`submit`、 +**引擎不认识任何 runner 名字。** `flash`、`serve`、`deploy`、`submit`、 `logcat` 对它一样陌生:它只知道「包可以提供具名 runner」这件事,然后执行它找到的 argv。**引擎里若有一份固定的名字表,就等于由引擎决定哪些领域可被表达。** @@ -47,7 +47,7 @@ mcpp::runner_longlived("monitor"); // 没有自然终点 mcpp::run_exclusive(); // 这个目标的运行不能重叠 ``` -⭐ **写程序名,不要写路径。** mcpp 会定位它:先找**图中任何一个包**在 +**写程序名,不要写路径。** mcpp 会定位它:先找**图中任何一个包**在 `[xlings.workspace]` 里声明的载荷的 `bin/`(消费工程优先,然后是它的依赖),再找 `PATH`。 板级包正是那个知道「哪个模拟器或探针能抵达这台机器」的东西,所以由它声明, **消费者什么都不用声明**。用 `mcpp::xpkg_dir` 拼绝对路径是多余的,而且引入了一个 @@ -129,6 +129,6 @@ cortex-m-rt = { version = "0.1.0", features = ["hardware"] } **消费者的命令不变。** 所选环境不提供的 runner 保持缺席:模拟器没有调试探针, 于是在 emulator 之下 `mcpp run --runner debug` 报告没有这个 runner,并列出有哪些。 -⭐ **这不需要任何引擎机制。** 引擎读 runner,对模拟器与探针一无所知; +**这不需要任何引擎机制。** 引擎读 runner,对模拟器与探针一无所知; `mcpp::has_feature` 本来就在。**一个问题不必新增任何东西就能回答,是分层按规定在 起作用。** diff --git a/docs/zh/19-supported-versions.md b/docs/zh/19-supported-versions.md index 4ce51722..0556fccb 100644 --- a/docs/zh/19-supported-versions.md +++ b/docs/zh/19-supported-versions.md @@ -35,7 +35,7 @@ | 目标表的行 | 只增。一行的档位可以上升;只要还有已发布的包以它为目标,该行不会被移除 | | 构建指纹、缓存布局、`target/` 的内容 | **不是接口。** 它们不经通知即变化,任何东西都不应解析它们 | -⚠️ 一个调用了其引擎自带 `mcpp` 模块中不存在的函数的 `build.mcpp`,失败发生在构建 +一个调用了其引擎自带 `mcpp` 模块中不存在的函数的 `build.mcpp`,失败发生在构建 程序的**编译**期,而不是通过协议错误。协议号管的是线上的指令;类型化 API 由装的是 哪个引擎决定。两者都写在这里,是因为包作者看到的失败取决于他越过了哪一条。 @@ -51,7 +51,7 @@ error: --locked was given and this resolution differs from mcpp.lock: 发布构建、审计或 CI 作业应当传 `--locked`。它会关掉构建快路径,因此断言总会跑到。 -⚠️ 锁**尚未约束**解析 —— 它记录并校验解析。把锁作为解析的输入来钉住它,是对解析器 +锁**尚未约束**解析 —— 它记录并校验解析。把锁作为解析的输入来钉住它,是对解析器 的另一项改动。 ## 物料清单 diff --git a/docs/zh/20-accelerators.md b/docs/zh/20-heterogeneous-builds.md similarity index 89% rename from docs/zh/20-accelerators.md rename to docs/zh/20-heterogeneous-builds.md index e3415dd8..323257bb 100644 --- a/docs/zh/20-accelerators.md +++ b/docs/zh/20-heterogeneous-builds.md @@ -1,6 +1,7 @@ -# 20 — 加速器 +# 20 — 异构硬件构建 -mcpp 如何构建设备代码,以及一个预建产物如何声明它能在哪些设备上运行。 +GPU 与 AI 加速器目标,以及宿主/设备混合编译:mcpp 如何构建设备代码,以及一个预建产物如何 +声明它能在哪些设备上运行。 ## 两种形态,mcpp 当前实现其中一种 @@ -17,9 +18,27 @@ Ascend C 与 Metal 都是这种形态。设备编译器产出一个目标文件( ## 设备编译单元 -扩展名为 `.cu` 或 `.hip` 的源文件是**设备编译单元**。mcpp 据此分类并相应处理: +由另一个编译器消费的语言写成的源文件是**设备编译单元**。mcpp 据此分类并相应处理: 它从不被扫描 import,也从不产出 BMI —— 因为没有任何设备编译器接受 C++20 modules。 +判据是编译器,不是厂商(下表为 2026.9.5.3+;在此之前只有 `.cu` 与 `.hip`): + +| 语言 | 扩展名 | +|---|---| +| CUDA、HIP | `.cu`、`.hip` | +| GLSL(按 stage) | `.comp`、`.vert`、`.frag`、`.geom`、`.tesc`、`.tese`、`.mesh`、`.task`、`.rgen`、`.rint`、`.rahit`、`.rchit`、`.rmiss`、`.rcall` | +| GLSL(无 stage) | `.glsl` | +| HLSL | `.hlsl` | +| OpenCL C | `.cl` | +| Metal Shading Language | `.metal` | + +表外的扩展名若出现在 `[build] sources` 中会被点名拒绝 —— 这正是「这张表是一张表」 +的含义:mcpp 对该文件没有任何规则,它的目标文件不会被任何东西链接,构建下去只会 +在更晚、更不清楚的地方失败。 + +`.glsl` 不携带 stage。glslang 从扩展名推导 stage,因此拒绝一个无 stage 的名字是 +规则包的事 —— 那条消息属于那里,这张表因此不需要知道哪些扩展名指定了 stage。 + `.cuh` 与 `.hiph` 被分类为头文件。它们不被编译,但改动其一可以改变构建图应有的形状, 因此与任何其它头文件一样会使快路径失效。 @@ -54,7 +73,7 @@ tarball 已经发出去了。设备源文件必须被显式点名。 ## 规则包在第一次编译之前报告的事 设备工具包有三件事出错得很晚,而没有一件是关于构建图的事实。它们由驱动这些工具的 -**规则包**读取并报告 —— `examples/09-cuda-kernel/rules-cuda` 逐一演示 —— 引擎一件都 +**规则包**读取并报告 —— `mcpp:plugins` 里的 `mcpp.rules.cuda` 逐一演示 —— 引擎一件都 不拥有(`tests/unit/test_core_vendor_probes.cpp` 守住这条线,于是第二个后端不会在 mcpp 里长出第二份拷贝)。 diff --git a/docs/zh/README.md b/docs/zh/README.md index 00ffad9e..4c248fbe 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -22,7 +22,7 @@ - [17 - 项目环境](17-the-project-environment.md) - [18 - 抵达一台设备](18-devices.md) - [19 - 受支持的版本与兼容性](19-supported-versions.md) -- [20 - 加速器](20-accelerators.md) +- [20 - 异构硬件构建](20-heterogeneous-builds.md) ## 规范文档 diff --git a/examples/06-openkal-cross/README.md b/examples/06-openkal-cross/README.md index d845a84c..fbf1616f 100644 --- a/examples/06-openkal-cross/README.md +++ b/examples/06-openkal-cross/README.md @@ -11,7 +11,7 @@ mcpp build --target aarch64-macos # macOS aarch64, from any host mcpp build --target x86_64-windows # Windows x86_64, from any host ``` -⭐ Every one of them declines the third segment. The `aarch64-linux` line could +Every one of them declines the third segment. The `aarch64-linux` line could not be written before 2026.8.26.2: it completed to `aarch64-linux-gnu`, a row registered but not supported, and refused — while `aarch64-linux-musl` built. A request that names no C library now resolves to a row that exists. @@ -32,7 +32,7 @@ C++ runtime, and depends in turn on a C library, which depends on whichever implementation of the platform interface matches the target being built. One line therefore selects three of the five target-side layers. -⭐⭐ **And the fourth — the compiler — without the manifest naming it.** There +**And the fourth — the compiler — without the manifest naming it.** There used to be a `[toolchain] default = "llvm@22.1.8"` here. It is gone, because `openkal-llvm-runtime` declares `requires = ["mcpp:compiler=llvm"]` — a C++ runtime is configured for one compiler family and records that in the headers it @@ -45,7 +45,7 @@ ships — and since 2026.8.26.2 mcpp reads that and takes it: — this project only ``` -⚠️ **"this project only" is load-bearing.** Nothing is written — not +**"this project only" is load-bearing.** Nothing is written — not `~/.mcpp/config.toml`, not this manifest. A requirement is a property of the package that states it, so it decides this build and no other. A project that wants a different compiler still writes one here, and that statement outranks diff --git a/examples/06-openkal-cross/mcpp.toml b/examples/06-openkal-cross/mcpp.toml index db23cda4..d2c2920f 100644 --- a/examples/06-openkal-cross/mcpp.toml +++ b/examples/06-openkal-cross/mcpp.toml @@ -14,7 +14,7 @@ description = "One source, four hosted targets, over openkal" # openkal matches the target being built. openkal-llvm-runtime = "0.1.3" -# ⭐⭐ AND IT SELECTS THE FOURTH — THE COMPILER — WITHOUT THIS FILE SAYING SO. +# AND IT SELECTS THE FOURTH — THE COMPILER — WITHOUT THIS FILE SAYING SO. # # There used to be a `[toolchain] default = "llvm@22.1.8"` here, with a comment # explaining that it named "a compiler, and nothing else". It is gone, and the @@ -30,14 +30,14 @@ openkal-llvm-runtime = "0.1.3" # (`requires = ["mcpp:compiler=llvm"]`), not your gcc@16.1.0 # — this project only # -# ⚠️ "this project only" is load-bearing. Nothing is written: not +# "this project only" is load-bearing. Nothing is written: not # `~/.mcpp/config.toml`, not this file. A requirement is a property of the # package that states it, so it decides this build and no other. # # A project that wants a different compiler still writes one here, and that # statement outranks the graph — see docs/03. -# ⚠️ NO BARE-METAL TARGET SECTION, AND ITS ABSENCE IS DELIBERATE. +# NO BARE-METAL TARGET SECTION, AND ITS ABSENCE IS DELIBERATE. # # This program asks about a filesystem and about tasks. An implementation that # provides neither is absent as a link-time definition — clause 6.1 of the diff --git a/examples/06-openkal-cross/src/main.cpp b/examples/06-openkal-cross/src/main.cpp index 3dc2017d..04b631ec 100644 --- a/examples/06-openkal-cross/src/main.cpp +++ b/examples/06-openkal-cross/src/main.cpp @@ -6,7 +6,7 @@ // mcpp build --target aarch64-macos macOS aarch64, any host // mcpp build --target x86_64-windows Windows x86_64, any host // -// ⭐ EVERY ONE OF THEM DECLINES THE THIRD SEGMENT, AND THE FOURTH LINE COULD +// EVERY ONE OF THEM DECLINES THE THIRD SEGMENT, AND THE FOURTH LINE COULD // NOT BE WRITTEN UNTIL 2026.8.26.2. `aarch64-linux` used to complete to // `aarch64-linux-gnu` — a row registered but not supported — and refuse, while // `aarch64-linux-musl` built. A request that names no C library now resolves to @@ -26,7 +26,7 @@ // silently reached the host's own libraries instead of the resolved ones would // answer differently. // -// ⚠️ WHAT IS ASKED HERE AND WHAT IS NOT. +// WHAT IS ASKED HERE AND WHAT IS NOT. // // A capability word says how an implementation behaves WITHIN an interface it // provides. Whether it provides the interface at all is a different question, diff --git a/examples/07-project-subos/README.md b/examples/07-project-subos/README.md index 36aeb8bd..fac37ade 100644 --- a/examples/07-project-subos/README.md +++ b/examples/07-project-subos/README.md @@ -72,7 +72,7 @@ subos = "tools" which mcpp resolves to `/.mcpp/.xlings/subos/tools/` — its own `bin`, its own package versions, isolated from every other project on the machine. -⚠️ **mcpp reads such an environment and never creates one.** A name that does +**mcpp reads such an environment and never creates one.** A name that does not resolve is a hard error, not a fallback: ``` diff --git a/examples/07-project-subos/build.mcpp b/examples/07-project-subos/build.mcpp index e7ef2f02..43ed6a61 100644 --- a/examples/07-project-subos/build.mcpp +++ b/examples/07-project-subos/build.mcpp @@ -2,7 +2,7 @@ import std; // A build program that answers one question: whose tools does this build see? // -// ⚠️ IT DOES NOT CONSTRUCT A PATH TO ONE. `MCPP_XPKG_QEMU_RISCV` gives the +// IT DOES NOT CONSTRUCT A PATH TO ONE. `MCPP_XPKG_QEMU_RISCV` gives the // payload directory, and a program can join `/bin/qemu-system-riscv64` onto it // — that works, and it means every build program in the ecosystem repeats the // same joining, each with its own idea of the layout. Asking `PATH` is what a diff --git a/examples/07-project-subos/mcpp.toml b/examples/07-project-subos/mcpp.toml index 91a1a38f..e33b5012 100644 --- a/examples/07-project-subos/mcpp.toml +++ b/examples/07-project-subos/mcpp.toml @@ -3,7 +3,7 @@ name = "project-subos" version = "0.1.0" description = "A build program that finds its tools in an environment the project declared" -# ⭐ THIS SECTION IS WHAT THE DIRECTORY IS FOR. +# THIS SECTION IS WHAT THE DIRECTORY IS FOR. # # `subos` names the environment this project builds in. mcpp already used that # declaration to decide which C library the project links against, and it now @@ -11,11 +11,11 @@ description = "A build program that finds its tools in an environment the projec # `build.mcpp` with — so a bare command name in a build program resolves inside # the environment the project named. # -# ⚠️ A PROJECT THAT DECLARES NO `subos` GETS THE `PATH` mcpp WAS STARTED WITH. +# A PROJECT THAT DECLARES NO `subos` GETS THE `PATH` mcpp WAS STARTED WITH. # There is no shared directory quietly in front of every build; declaring one # is what puts it there. # -# ⚠️ `"default"` SO THIS EXAMPLE BUILDS ON A CLEAN CHECKOUT. The isolated form +# `"default"` SO THIS EXAMPLE BUILDS ON A CLEAN CHECKOUT. The isolated form # is a private name — `subos = "tools"`, resolved to # `/.mcpp/.xlings/subos/tools/` — and mcpp READS such an environment # but never creates one, so a name nobody has bootstrapped is a hard error by diff --git a/examples/08-build-rules/app/tools/check.sh b/examples/08-build-rules/app/tools/check.sh index 4b3eb99f..a5f44d3d 100755 --- a/examples/08-build-rules/app/tools/check.sh +++ b/examples/08-build-rules/app/tools/check.sh @@ -3,7 +3,7 @@ # # A stand-in for clang-tidy: it reads a file and answers with an EXIT CODE. # -# ⚠️ It does not touch the stamp, and that is the point. A check action's output +# It does not touch the stamp, and that is the point. A check action's output # is bookkeeping the build graph needs, not something an analyser knows about — # clang-tidy writes nothing on success either. mcpp creates the stamp when the # command succeeds, which is what makes a check rule writable without a diff --git a/examples/09-cuda-kernel/README.md b/examples/09-cuda-kernel/README.md index 8ef9ec57..4e028dd8 100644 --- a/examples/09-cuda-kernel/README.md +++ b/examples/09-cuda-kernel/README.md @@ -15,8 +15,9 @@ app/ into a C++ one src/main.cpp an ordinary consumer, which imports the seam and never sees the header - build.mcpp hands the device sources to the rule package -rules-cuda/ a build-rule package that knows how to compile them + build.mcpp hands the device sources to `mcpp.rules.cuda`, a + member of `mcpp:plugins` (mcpp-community/mcpp-plugins) + selected by the feature `rules-cuda` ``` Three properties are load-bearing. diff --git a/examples/09-cuda-kernel/app/build.mcpp b/examples/09-cuda-kernel/app/build.mcpp index 7a1b989c..7eaf67d2 100644 --- a/examples/09-cuda-kernel/app/build.mcpp +++ b/examples/09-cuda-kernel/app/build.mcpp @@ -1,6 +1,6 @@ import std; import mcpp; -import mcpplibs.rules.cuda; +import mcpp.rules.cuda; // Everything the rule needs is in the manifest: the architectures in // `[build] accel`, the device sources in the constrained glob, the toolkit @@ -12,10 +12,10 @@ import mcpplibs.rules.cuda; // is how the alternate route is measured without editing the manifest. int main() { mcpp::rerun_if_env_changed("MCPP_EXAMPLE_CUDA_ROUTE"); - mcpplibs::rules::cuda::options opt; + mcpp::rules::cuda::options opt; opt.includes = { "include" }; if (const char* r = std::getenv("MCPP_EXAMPLE_CUDA_ROUTE"); r && *r) - opt.which = std::string_view(r) == "nvcc" ? mcpplibs::rules::cuda::route::nvcc - : mcpplibs::rules::cuda::route::clang; - return mcpplibs::rules::cuda::compile(opt) ? 0 : 1; + opt.which = std::string_view(r) == "nvcc" ? mcpp::rules::cuda::route::nvcc + : mcpp::rules::cuda::route::clang; + return mcpp::rules::cuda::compile(opt) ? 0 : 1; } diff --git a/examples/09-cuda-kernel/app/mcpp.toml b/examples/09-cuda-kernel/app/mcpp.toml index 5118ffab..d1cbff53 100644 --- a/examples/09-cuda-kernel/app/mcpp.toml +++ b/examples/09-cuda-kernel/app/mcpp.toml @@ -17,8 +17,10 @@ import_std = true [toolchain] default = "llvm@22.1.8" -[dependencies] -rules-cuda = { path = "../rules-cuda", host-module = true } +# The rule that compiles the island lives in the official plugin collection, +# selected by its feature; `build.mcpp` imports it as `mcpp.rules.cuda`. +[dependencies.mcpp] +plugins = { version = "0.1.0", features = ["rules-cuda"], host-module = true } # The driver's userspace library, reached through an index package that owns # the one hop mcpp needs: a directory on the artifact's runtime search path. @@ -57,7 +59,7 @@ include_dirs = ["include"] # The CUDA runtime is linked STATICALLY, and only when a device build asks for # it. Linking the redistributable half in leaves exactly one host dependency, # libcuda.so.1, which the driver owns and the sentinel package reaches. -# ⭐ NO ABSOLUTE PATHS: the rule package puts the payload's library directory on +# NO ABSOLUTE PATHS: the rule package puts the payload's library directory on # the link line from `mcpp::xpkg_dir`, so this manifest names libraries only. [target.'cfg(accelerator = "cuda")'.build] ldflags = ["-lcudart_static", "-lrt", "-lpthread", "-ldl"] diff --git a/examples/09-cuda-kernel/rules-cuda/mcpp.toml b/examples/09-cuda-kernel/rules-cuda/mcpp.toml deleted file mode 100644 index eb601205..00000000 --- a/examples/09-cuda-kernel/rules-cuda/mcpp.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "rules-cuda" -namespace = "mcpplibs" -version = "0.1.0" -description = "Compile CUDA device translation units: clang -x cuda by default, nvcc as the alternate route (role = object)" -license = "Apache-2.0" diff --git a/examples/09-cuda-kernel/rules-cuda/src/rules-cuda.cppm b/examples/09-cuda-kernel/rules-cuda/src/rules-cuda.cppm deleted file mode 100644 index 6b537d7c..00000000 --- a/examples/09-cuda-kernel/rules-cuda/src/rules-cuda.cppm +++ /dev/null @@ -1,619 +0,0 @@ -// Compile CUDA device translation units and hand the objects to the link. -// -// WHY A RULE PACKAGE RATHER THAN THE ENGINE -// -// Everything below is knowledge about one vendor's tools: where the toolkit's -// pieces live, how an architecture is spelled, which host compilers nvcc -// tolerates, how a driver states its version. None of it is knowledge about -// the build graph. The engine owns the graph, the artifact's identity and the -// architecture set (`[build] accel`); the spelling of the command that -// produces an object, and every probe of the machine, is this file's business. -// `tests/unit/test_core_vendor_probes.cpp` in mcpp holds that line from the -// other side: the engine names no vendor tool. -// -// TWO ROUTES, ONE PRIMARY -// -// `clang -x cuda` is the primary route: the resolved toolchain's own clang -// compiles the device unit, so there is no second host compiler and no -// host-compiler bound to satisfy. nvcc is the alternate, taken when the -// project's toolchain is GCC or when asked for: it drives a host compiler -// (`-ccbin`) and refuses one newer than the bound its `crt/host_config.h` -// states, which this rule reads and reports. -// -// WHAT THE RULE TELLS THE ENGINE -// -// Objects, through `mcpp::action` (role "object"); library directories, -// through `mcpp::link_search`; and three claims about the machine that the -// engine compares or relays before the first compile: -// - the driver's version, read through the driver's own library and stated -// with `mcpp::fact`, together with the floor the runtime needs -// (`mcpp::floor`); an unmet floor refuses the build with both values; -// - whether nvcc can reach its own back-end stages (`--dryrun`), as an -// advisory naming the first stage that does not resolve; -// - whether the embedded PTX can be JIT-compiled by this driver, as an -// advisory, because the SASS for the named architectures still runs. - -module; -#include -#include -#if !defined(_WIN32) -#include -#endif - -export module mcpplibs.rules.cuda; - -import std; -import mcpp; - -export namespace mcpplibs::rules::cuda { - -enum class route { automatic, clang, nvcc }; - -struct options { - route which = route::automatic; - // Header search paths for the island. Relative entries resolve against the - // package root; an ABSOLUTE entry is passed through unchanged. - // - // ⭐ THE ABSOLUTE FORM IS FOR A DEPENDENCY'S HEADERS. A device compiler is - // a separate driver and inherits nothing from the C++ side's include - // configuration, so a package whose device code includes a dependency's - // header -- ggml's CUDA backend includes `cublas_v2.h` -- has to name that - // dependency's directory here, and it knows it only as the absolute path - // `mcpp::dep_dir` answered with. - std::vector includes; - std::string out_dir = std::string(mcpp::out_dir()); -}; - -// ─── What the engine said ────────────────────────────────────────────────── - -// The `cuda` chunk of `mcpp::accel()`, in this rule's own reading: the engine -// carries the string and compares it as a shape; what `sm_89` means is ours. -struct target { - std::string version; // "12.9" - std::vector archs; // {"sm_89"} - std::string ptx; // "89" when a portable form is embedded - bool present = false; -}; - -// Split on one character. Written out rather than taken from : GCC 16 -// refuses the ranges split view instantiated inside an exported inline function -// when build.mcpp imports this module (`conflicting deduced return type for -// imported declaration ... view_interface::data()`), and clang does not. -inline std::vector split(std::string_view s, char sep) { - std::vector out; - for (std::size_t i = 0; i <= s.size();) { - auto j = s.find(sep, i); - out.push_back(s.substr(i, j == std::string_view::npos ? s.size() - i : j - i)); - if (j == std::string_view::npos) break; - i = j + 1; - } - return out; -} - -inline std::string_view trim(std::string_view s) { - while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.remove_prefix(1); - while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.remove_suffix(1); - return s; -} - -inline target parse_target(std::string_view accel) { - target t; - for (std::size_t i = 0; i <= accel.size();) { - auto comma = accel.find(',', i); - auto open = accel.find('{', i), close = accel.find('}', i); - if (open != std::string_view::npos && close != std::string_view::npos - && comma != std::string_view::npos && comma > open && comma < close) - comma = accel.find(',', close); - auto chunk = trim(comma == std::string_view::npos ? accel.substr(i) - : accel.substr(i, comma - i)); - i = comma == std::string_view::npos ? accel.size() + 1 : comma + 1; - if (!chunk.starts_with("cuda")) continue; - t.present = true; - auto plus = chunk.find('+'); - t.version = std::string(trim(chunk.substr(4, plus == std::string_view::npos - ? chunk.size() - 4 : plus - 4))); - if (plus != std::string_view::npos) { - auto o = chunk.find('{', plus), c = chunk.find('}', plus); - if (o != std::string_view::npos && c != std::string_view::npos) - for (auto part : split(chunk.substr(o + 1, c - o - 1), ',')) - if (auto a = trim(part); !a.empty()) t.archs.emplace_back(a); - auto tail = chunk.substr(c == std::string_view::npos ? chunk.size() : c + 1); - for (auto key : {"ptx>=", "floor>="}) - if (auto p = tail.find(key); p != std::string_view::npos) - t.ptx = std::string(trim(tail.substr(p + std::string_view(key).size()))); - } - } - return t; -} - -inline std::vector device_sources() { - std::vector out; - for (auto part : split(std::string_view(mcpp::device_sources()), '\n')) - if (auto s = trim(part); !s.empty()) out.emplace_back(s); - return out; -} - -// ─── Locating the toolkit ────────────────────────────────────────────────── - -// The toolkit this project declared under `[xlings.workspace]`, by component. -// The 13.x line splits the compiler across `cuda-nvcc`, `cuda-crt` and -// `libnvvm`; the 12.x line keeps them in `cuda-nvcc`. Either way the project -// names the compiler and this rule finds the pieces. -struct toolkit { - std::string nvcc_root, cudart_root, crt_root, driver_dir; - std::string nvcc() const { return nvcc_root + "/bin/nvcc"; } - std::string host_config() const { - for (auto const* r : { &crt_root, &nvcc_root, &cudart_root }) { - if (r->empty()) continue; - auto p = *r + "/include/crt/host_config.h"; - if (std::filesystem::exists(p)) return p; - } - return {}; - } - std::vector include_dirs() const { - std::vector out; - for (auto const* r : { &cudart_root, &crt_root, &nvcc_root }) - if (!r->empty() && std::filesystem::is_directory(*r + "/include")) - out.push_back(*r + "/include"); - return out; - } - std::vector lib_dirs() const { - std::vector out; - for (auto const* r : { &cudart_root, &nvcc_root }) - for (auto const* sub : { "/lib", "/lib64" }) - if (!r->empty() && std::filesystem::is_directory(*r + sub)) - out.push_back(*r + sub); - return out; - } -}; - -inline std::string xpkg(const char* name) { - if (const char* d = mcpp::xpkg_dir("xim", name); d && *d) return d; - return {}; -} - -inline std::optional find_toolkit() { - toolkit t; - t.nvcc_root = xpkg("cuda-nvcc"); - t.cudart_root = xpkg("cuda-cudart"); - t.crt_root = xpkg("cuda-crt"); - t.driver_dir = xpkg("libcuda-host-link"); - if (t.nvcc_root.empty() || t.cudart_root.empty()) { - std::println(std::cerr, - "mcpplibs.rules.cuda: the toolkit is not declared.\n" - " Name it under [xlings.workspace] and mcpp provisions it on first use:\n" - " \"xim:cuda-nvcc\" = \"12.9.86\"\n" - " \"xim:cuda-cudart\" = \"12.9.79\"\n" - " (found nvcc: '{}', cudart: '{}')", t.nvcc_root, t.cudart_root); - return std::nullopt; - } - return t; -} - -// ─── Probes: what the machine has, what the toolkit needs ────────────────── - -// The driver's version through the driver's own library, reached through the -// sentinel package rather than /usr/lib. "" when there is no driver here, -// which is a fact about the machine and not a failure of the build. -inline std::string driver_version(const toolkit& t) { -#if defined(_WIN32) - return {}; -#else - if (t.driver_dir.empty()) return {}; - const auto lib = t.driver_dir + "/lib/libcuda.so.1"; - if (!std::filesystem::exists(lib)) return {}; - // What would change the answer is the library the answer was read from. - mcpp::rerun_if_changed(lib.c_str()); - void* h = ::dlopen(lib.c_str(), RTLD_LAZY | RTLD_LOCAL); - if (!h) { - const char* why = ::dlerror(); - mcpp::warning(std::format("could not open the driver library {}: {}", lib, - why ? why : "(no reason given)").c_str()); - return {}; - } - using fn = int (*)(int*); - auto get = reinterpret_cast(::dlsym(h, "cuDriverGetVersion")); - int v = 0; - std::string out; - if (get && get(&v) == 0 && v > 0) out = std::format("{}.{}", v / 1000, (v % 1000) / 10); - ::dlclose(h); - return out; -#endif -} - -inline int major_of(std::string_view v) { - int m = 0; - for (char c : v) { if (!std::isdigit(static_cast(c))) break; m = m * 10 + (c - '0'); } - return m; -} - -inline bool version_at_least(std::string_view have, std::string_view want) { - auto parse = [](std::string_view s) { - std::vector out; int acc = 0; bool digits = false; - for (char c : s) { - if (c == '.') { out.push_back(acc); acc = 0; digits = false; continue; } - if (!std::isdigit(static_cast(c))) break; - acc = acc * 10 + (c - '0'); digits = true; - } - if (digits) out.push_back(acc); - return out; - }; - auto h = parse(have), w = parse(want); - for (std::size_t i = 0; i < std::max(h.size(), w.size()); ++i) { - int a = i < h.size() ? h[i] : 0, b = i < w.size() ? w[i] : 0; - if (a != b) return a > b; - } - return true; -} - -// State the driver relation. The engine compares the floor against the fact -// and refuses with both values; this rule only knows which numbers matter. -// -// The floor is the toolkit's major: a 12.x runtime runs on any 12.x driver -// (minor-version compatibility), and fails at the first allocation on an 11.x -// one. The embedded PTX is a separate, softer question: PTX emitted by toolkit -// 12.9 is JIT-compiled only by a driver at or above 12.9, but the SASS for the -// named architectures still runs, so a driver below the toolkit costs reach on -// newer hardware rather than correctness here -- reported, not enforced. -inline void state_driver_relation(const toolkit& t, const target& tg) { - const auto driver = driver_version(t); - if (!driver.empty()) mcpp::fact("cuda.driver", driver.c_str()); - else mcpp::warning("no driver library reachable through xim:libcuda-host-link; " - "the build proceeds and the artifact will find no device at run time"); - const int major = major_of(tg.version); - if (major > 0) mcpp::floor(std::format("cuda.driver >= {}.0", major).c_str()); - if (!driver.empty() && !tg.ptx.empty() && !version_at_least(driver, tg.version)) - mcpp::warning(std::format( - "the PTX embedded for compute_{} was emitted by toolkit {} and this driver " - "serves {}; hardware newer than {{{}}} will not be able to JIT it. The named " - "architectures run. Build with a toolkit at or below the driver, or add the " - "newer hardware's SASS to [build] accel.", - tg.ptx, tg.version, driver, [&] { - std::string s; for (auto& a : tg.archs) { if (!s.empty()) s += ','; s += a; } - return s; }()).c_str()); -} - -// The greatest gcc major and the greatest clang major the toolkit accepts. -// Zero means the header said nothing, which is not a refusal. -struct bounds { int gcc = 0, clang = 0; }; - -inline bounds read_bounds(std::string_view headerPath) { - bounds b; - if (headerPath.empty()) return b; - std::ifstream in{std::string(headerPath)}; - std::string text{std::istreambuf_iterator(in), std::istreambuf_iterator()}; - auto number_after = [&](std::size_t pos) { - int v = 0, n = 0; - while (pos < text.size() && !std::isdigit(static_cast(text[pos]))) { - if (text[pos] == '\n') return 0; - ++pos; - } - while (pos < text.size() && std::isdigit(static_cast(text[pos]))) { - v = v * 10 + (text[pos] - '0'); ++pos; ++n; - } - return n ? v : 0; - }; - if (auto p = text.find("__GNUC__ > "); p != std::string::npos) b.gcc = number_after(p + 10); - if (auto p = text.find("clang version must be less than "); p != std::string::npos) - if (int excl = number_after(p + 31); excl > 0) b.clang = excl - 1; - return b; -} - -// Does the C library this build compiles against declare the C23 functions -// `cospi`, `sinpi` and `rsqrt`? -// -// ⚠️ Measured 2026-09-05 against glibc 2.44. Toolkit 12.9's -// `crt/math_functions.h` declares those same names for the host WITHOUT -// `noexcept`; glibc declares them WITH it, and since C++17 that is part of the -// function type. nvcc's front end stops with six `exception specification is -// incompatible` errors that name a glibc header and a CUDA header and leave -// the reader to work out that neither is at fault alone. The 13.x line does -// not redeclare them and compiles cleanly against the same C library. -// -// Read, not probed. The answer is one substring of one header the sysroot -// already contains; a probe compile would spend a second nvcc invocation to -// learn the same thing, and would report it as a compile failure rather than -// as a pairing that cannot work. -inline bool libc_declares_c23_pi_math(std::string_view sysroot) { - if (sysroot.empty()) return false; - for (auto const* rel : { "/usr/include/bits/mathcalls.h", "/include/bits/mathcalls.h" }) { - std::ifstream in{std::string(sysroot) + rel}; - if (!in) continue; - std::string text{std::istreambuf_iterator(in), std::istreambuf_iterator()}; - return text.find("(cospi,") != std::string::npos - && text.find("(rsqrt,") != std::string::npos; - } - return false; -} - -// ` -dumpversion` → major. The host compiler is the toolchain mcpp -// resolved for this build, so its version is a fact of the build, not a guess. -inline int compiler_major(const std::string& cc) { -#if defined(_WIN32) - (void)cc; return 0; -#else - std::string cmd = cc + " -dumpversion 2>/dev/null"; - if (FILE* p = ::popen(cmd.c_str(), "r")) { - char buf[64] = {}; - std::string s; - if (std::fgets(buf, sizeof buf, p)) s = buf; - ::pclose(p); - return major_of(s); - } - return 0; -#endif -} - -// The first back-end stage nvcc names but cannot resolve, from its own plan. -// nvcc invokes cicc, cudafe++, ptxas and fatbinary by bare name on a PATH it -// states in the plan; a stage that does not resolve there fails the compile -// with `sh: 1: cicc: not found`, naming nothing that helps. -inline std::optional unreachable_stage(const toolkit& t, const std::string& ccbin) { -#if defined(_WIN32) - (void)t; (void)ccbin; return std::nullopt; -#else - const auto probe = std::filesystem::temp_directory_path() / "mcpp-rules-cuda-dryrun.cu"; - { std::ofstream(probe) << "__global__ void k() {}\n"; } - std::string cmd = std::format("{} --dryrun -ccbin {} -c {} -o /dev/null 2>&1", - t.nvcc(), ccbin, probe.string()); - std::string text; - if (FILE* p = ::popen(cmd.c_str(), "r")) { - char buf[4096]; - while (std::fgets(buf, sizeof buf, p)) text += buf; - ::pclose(p); - } - std::filesystem::remove(probe); - std::string path; - std::vector stages; - for (auto l : split(text, '\n')) { - if (!l.starts_with("#$ ")) continue; - l.remove_prefix(3); - if (l.starts_with("PATH=")) { path = std::string(l.substr(5)); continue; } - for (auto const* stage : { "cicc", "cudafe++", "ptxas", "fatbinary", "nvlink" }) { - auto pos = l.find(stage); - if (pos == 0 || (pos != std::string_view::npos && (l[pos - 1] == ' ' || l[pos - 1] == '"'))) - if (std::ranges::find(stages, stage) == stages.end()) stages.emplace_back(stage); - } - } - if (stages.empty()) return std::nullopt; // no plan, no finding - for (auto const& stage : stages) { - bool found = false; - for (auto dir : split(path, ':')) { - std::string d(dir); - if (!d.empty() && std::filesystem::exists(d + "/" + stage)) { found = true; break; } - } - if (!found) return stage; - } - return std::nullopt; -#endif -} - -// ─── Planning ────────────────────────────────────────────────────────────── - -struct edge { - std::string id, description; - std::vector command, inputs, outputs; -}; - -inline route decide(route asked) { - if (asked != route::automatic) return asked; - return std::string_view(mcpp::compiler()) == "clang" ? route::clang : route::nvcc; -} - -inline std::vector plan(std::span sources, options opt = {}) { - std::vector out; - const std::string root = mcpp::manifest_dir(); - if (root.empty()) { - std::println(std::cerr, "mcpplibs.rules.cuda: no mcpp build context -- this runs from build.mcpp"); - return out; - } - const auto tg = parse_target(mcpp::accel()); - if (!tg.present || tg.archs.empty()) { - // C19: a device build that names no device is refused HERE, not at - // run time as `no kernel image is available for execution`. - std::println(std::cerr, - "mcpplibs.rules.cuda: [build] accel names no CUDA architecture (accel = \"{}\").\n" - " Write e.g. accel = \"cuda12.9+{{sm_89}} ptx>=89\" -- the set a build compiles\n" - " for is a decision, and the machine's own hardware is a poor default for it.", - mcpp::accel()); - return out; - } - auto tk = find_toolkit(); - if (!tk) return out; - state_driver_relation(*tk, tg); - - const route r = decide(opt.which); - const std::string tcdir = mcpp::toolchain_dir(); - std::string driver_cc; // the compiler that runs the device unit - std::vector front; // the command up to the input file - if (r == route::clang) { - driver_cc = tcdir + "/bin/clang++"; - if (!std::filesystem::exists(driver_cc)) { - std::println(std::cerr, "mcpplibs.rules.cuda: the clang route needs the toolchain's clang++ at {}", driver_cc); - return out; - } - front = { driver_cc, "-x", "cuda", "-std=c++17", "-O2", "-fPIC", - "--cuda-path=" + tk->nvcc_root, "-Wno-unknown-cuda-version", - // ⚠️ NVIDIA'S HEADER REFUSES libc++, AND THE REFUSAL IS - // ABOUT nvcc RATHER THAN ABOUT THIS COMPILER. - // - // crt/host_defines.h:67: error: "libc++ is not supported - // on x86 system" - // - // The guard is `#if defined(__CUDACC__) && … && - // defined(_LIBCPP_VERSION)`, and clang defines `__CUDACC__` - // when it compiles CUDA itself — so a device unit that - // includes stops here on any LLVM - // toolchain, which is the toolchain this route exists for. - // Measured on ggml's CUDA backend; the CUDA example's own - // kernel never showed it because a bare kernel includes no - // toolkit header at all. - // - // The escape hatch is upstream's own, and it is passed only - // on this route: nvcc's host pass really does break against - // libc++, and nothing here weakens that. - "-D_ALLOW_UNSUPPORTED_LIBCPP" }; - for (auto const& inc : tk->include_dirs()) front.push_back("-I" + inc); - for (auto const& a : tg.archs) front.push_back("--cuda-gpu-arch=" + a); - // clang checks ptxas and fatbinary itself; say so before it does. - for (auto const* tool : { "ptxas", "fatbinary" }) - if (!std::filesystem::exists(tk->nvcc_root + "/bin/" + tool)) - mcpp::warning(std::format("the toolkit payload has no {}; clang invokes it " - "after generating PTX", tool).c_str()); - std::println("mcpplibs.rules.cuda: clang route -- {} (toolkit {})", driver_cc, tk->nvcc_root); - } else { - // nvcc drives the toolchain's own compiler, and refuses one newer than - // the bound its header states. Read the bound; if exceeded, pass the - // escape hatch and say so -- an unexplained flag is worse than a note. - const bool clangHost = std::string_view(mcpp::compiler()) == "clang"; - if (clangHost) { - // Measured: nvcc's own crt/host_defines.h stops the compile with - // `libc++ is not supported on x86 system`, and libc++ is what an - // LLVM toolchain's clang uses. The pairing that works is nvcc with - // a GCC toolchain; with an LLVM toolchain the clang route is the - // one to take, and it is the default. - std::println(std::cerr, - "mcpplibs.rules.cuda: the nvcc route needs a GCC host compiler; this project's " - "toolchain is LLVM, whose clang uses libc++ and nvcc refuses it. Use the clang " - "route (the default for an LLVM toolchain) or set [toolchain] to a gcc payload."); - return out; - } - // The other pairing this route cannot have: an old toolkit and a C - // library new enough to have the C23 `pi` functions. Stated before the - // compile, because the compile's own report names two headers and no - // decision. - if (major_of(tg.version) < 13 - && libc_declares_c23_pi_math(mcpp::toolchain_sysroot())) { - std::println(std::cerr, - "mcpplibs.rules.cuda: toolkit {} redeclares the C23 functions cospi, sinpi and " - "rsqrt for the host without `noexcept`, and the C library this build compiles " - "against declares them with it; nvcc's front end refuses the pair.\n" - " Name a 13.x toolkit, whose headers leave them to the C library:\n" - " [xlings.workspace]\n" - " \"xim:cuda-nvcc\" = \"13.3.33\"\n" - " \"xim:cuda-crt\" = \"13.3.33\"\n" - " \"xim:cuda-cudart\" = \"13.3.29\"\n" - " or take the clang route, which does not include that header at all.", - tg.version); - return out; - } - // The host compiler nvcc drives, chosen within the bound the toolkit - // states. Measured: gcc 16 under nvcc 12.9 (bound gcc <= 14) fails inside - // nvcc's front end on GCC 16's own even with - // -allow-unsupported-compiler -- the escape hatch admits a compiler one - // step past the bound, not a standard library two majors newer. So the - // rule does not guess: the toolchain's g++ when it is within the bound, - // otherwise a gcc payload the project declared for this purpose, and - // otherwise a refusal that says which declaration to add. - const auto b = read_bounds(tk->host_config()); - const std::string tcGcc = tcdir + "/bin/g++"; - const int tcMajor = compiler_major(tcGcc); - if (b.gcc == 0 || tcMajor <= b.gcc) { - driver_cc = tcGcc; - } else if (auto payload = xpkg("gcc"); !payload.empty() - && compiler_major(payload + "/bin/g++") <= b.gcc) { - driver_cc = payload + "/bin/g++"; - mcpp::warning(std::format( - "nvcc {} states gcc <= {} in {}; the toolchain's gcc {} exceeds it, so the " - "device unit is compiled with the declared xim:gcc payload ({}). The clang " - "route has no such bound.", tg.version, b.gcc, tk->host_config(), tcMajor, - driver_cc).c_str()); - } else { - std::println(std::cerr, - "mcpplibs.rules.cuda: nvcc {} accepts gcc <= {} ({}), and this project's " - "toolchain is gcc {}.\n" - " Declare a gcc payload within the bound and the rule drives that one:\n" - " [xlings.workspace]\n" - " \"xim:gcc\" = \"13.3.0\"\n" - " or take the clang route with [toolchain] default = \"llvm@22.1.8\".", - tg.version, b.gcc, tk->host_config(), tcMajor); - return out; - } - front = { tk->nvcc(), "-ccbin", driver_cc, "-std=c++17", "-O2", - "--compiler-options", "-fPIC" }; - // The host compiler nvcc drives is not one mcpp resolved, so nothing - // has told it where the C library or the assembler are. Measured: with - // neither of these, NVIDIA's own crt/host_config.h stops at - // `features.h: No such file or directory`. Both are the flags mcpp - // passes to its own compiler for this target. - if (const char* sr = mcpp::toolchain_sysroot(); sr && *sr) { - front.push_back("--compiler-options"); - front.push_back(std::string("--sysroot=") + sr); - } - if (const char* bu = mcpp::toolchain_binutils_dir(); bu && *bu) { - front.push_back("--compiler-options"); - front.push_back(std::string("-B") + bu); - } - for (auto const& inc : tk->include_dirs()) front.push_back("-I" + inc); - for (auto const& a : tg.archs) { - std::string digits; - for (char c : a) if (std::isdigit(static_cast(c))) digits += c; - front.push_back("-gencode"); - front.push_back(std::format("arch=compute_{},code={}", digits, a)); - } - if (!tg.ptx.empty()) { - front.push_back("-gencode"); - front.push_back(std::format("arch=compute_{0},code=compute_{0}", tg.ptx)); - } - if (auto missing = unreachable_stage(*tk, driver_cc)) - mcpp::warning(std::format( - "nvcc cannot reach its own back-end: it invokes '{}' by name and that name " - "does not resolve on the search path it states. On the 13.x line install " - "xim:libnvvm beside xim:cuda-nvcc.", *missing).c_str()); - std::println("mcpplibs.rules.cuda: nvcc route -- {} with -ccbin {}", tk->nvcc(), driver_cc); - } - - // 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. - for (auto const& d : tk->lib_dirs()) mcpp::link_search(d.c_str()); - - for (auto const& src : sources) { - const auto stem = std::filesystem::path(src).stem().string(); - const auto obj = opt.out_dir + "/" + stem + ".cu.o"; - edge e; - e.id = "cuda:" + stem; - e.description = (r == route::clang ? "clang -x cuda " : "nvcc ") + src; - e.command = front; - for (auto const& inc : opt.includes) - e.command.push_back("-I" + (std::filesystem::path(inc).is_absolute() - ? inc : root + "/" + inc)); - e.command.insert(e.command.end(), { "-c", root + "/" + src, "-o", obj }); - e.inputs = { root + "/" + src }; - e.outputs = { obj }; - out.push_back(std::move(e)); - } - return out; -} - -inline bool submit(std::span edges) { - for (auto const& e : edges) { - mcpp::action a; - a.id = e.id.c_str(); - a.role = "object"; // the linkable artifact itself - a.description = e.description.c_str(); - for (auto const& c : e.command) a.arg(c.c_str()); - for (auto const& i : e.inputs) a.input(i.c_str()); - for (auto const& o : e.outputs) a.output(o.c_str()); - a.submit(); - } - return true; -} - -// Everything from the manifest: the architectures from `[build] accel`, the -// sources from the constrained glob in `[build] sources`. A build that asks -// for no accelerator has no device sources and nothing to do here -- that is -// the CPU-only variant, and the seam's fallback carries it. -inline bool compile(options opt = {}) { - if (!*mcpp::accel()) return true; - const auto sources = device_sources(); - if (sources.empty()) { - mcpp::warning("[build] accel names a device but no constrained glob matched a device " - "source; nothing was compiled for it"); - return true; - } - auto edges = plan(sources, std::move(opt)); - if (edges.empty()) return false; - return submit(edges); -} - -} // namespace mcpplibs::rules::cuda diff --git a/examples/10-vulkan-compute/README.md b/examples/10-vulkan-compute/README.md new file mode 100644 index 00000000..49ba6989 --- /dev/null +++ b/examples/10-vulkan-compute/README.md @@ -0,0 +1,88 @@ +# 10 — A Vulkan compute shader behind the same seam + +The example next door compiles CUDA. This one compiles GLSL, dispatches it +through Vulkan, and prints the same four numbers — and the point is what the +two have in common, not what differs. + +## The shape + +``` +app/ + shaders/scale.comp the island: GLSL, compiled by glslang, never by + the C++ toolchain and never in the module graph + src/vulkan/saxpy.cpp the host side of the island: Vulkan calls, and the + SPIR-V compiled into the binary as a C array + src/cpu/saxpy.cpp the same interface implemented for the host, + compiled instead when the build names no device + include/saxpy/saxpy.h the island's interface: extern "C", no std types + src/app.cppm the seam: a module that turns the C interface back + into a C++ one + src/main.cpp an ordinary consumer, which imports the seam + build.mcpp hands the shaders to `mcpp.rules.spirv`, a member + of `mcpp:plugins` (mcpp-community/mcpp-plugins) + selected by the feature `rules-spirv` +``` + +## What is shared with `examples/09-cuda-kernel` + +Everything structural. The device axis is declared once in the manifest, the +device sources are a constrained glob, the rule package receives them through +`MCPP_DEVICE_SOURCES`, and the engine never learns a vendor's name: + +```toml +accel = "vulkan1.2" +sources = [ + "src/*.cppm", + "src/*.cpp", + { glob = "shaders/*.comp", accel = "vulkan1.2" }, +] +``` + +`vulkan1.2` is the SPIR-V target environment and reaches `--target-env` the +same way `sm_89` reaches `-gencode`. It carries no architecture set, and that +is not an omission: SPIR-V is the portable form, and which device executes it +is decided by the driver when the program runs. A rule that demanded an +architecture would be inventing a requirement its device API does not have. + +## What is different, and what it demonstrates + +**The output is a header, not an object.** A SPIR-V module is data the program +hands to `vkCreateShaderModule`. `mcpp.rules.spirv` submits its actions with +`role = "source"`, the one role the engine orders BEFORE compilation, and adds +the directory it writes to the include path. `#include "scale_comp.h"` then +resolves to a `const uint32_t scale_comp_spv[]`, and the artifact carries its +shaders — `mcpp pack` has nothing further to collect and the program does not +read a file at run time. + +An `artifact` role would not do: its outputs are ordered against the LINK, +which is after the translation unit that includes the header is compiled. + +**One artifact runs on every device.** The same binary was measured on three: + +| where | how | output | +|---|---|---| +| NVIDIA RTX 4080 | the host's own ICD, through `compat.vulkan-runtime` | `12 24 36 48` | +| CPU (`llvmpipe`) | `xim:mesa-lavapipe`, a payload, with `VK_DRIVER_FILES` naming only it | `12 24 36 48` | +| no device at all | `mcpp build --no-accel`, the CPU implementation behind the seam | `12 24 36 48` | + +The middle row is why the payload exists: a machine with no GPU — every CI +runner in this ecosystem — still has a Vulkan device, so the Vulkan lane is +something a test can assert on rather than something that only runs on a +developer's desk. + +## Running it + +```bash +cd examples/10-vulkan-compute/app +mcpp run # whatever device the loader finds +mcpp run --no-accel # the CPU implementation behind the same seam +``` + +The `[xlings.workspace]` block names the two payloads this build needs — the +shader compiler and the software driver — so the first build installs them. + +## What this example does not show + +Graphics. There is no swapchain, no window and no surface: the device API is +here for compute, which is the half a build system has to carry. A shader that +draws is compiled by exactly the same rule. diff --git a/examples/10-vulkan-compute/app/build.mcpp b/examples/10-vulkan-compute/app/build.mcpp new file mode 100644 index 00000000..3ae0fdf8 --- /dev/null +++ b/examples/10-vulkan-compute/app/build.mcpp @@ -0,0 +1,13 @@ +import std; +import mcpp; +import mcpp.rules.spirv; + +int main() { + mcpp::rerun_if_changed_glob("shaders/**/*.comp"); + mcpp::rerun_if_changed_glob("shaders/**/*.glsl"); + + mcpp::rules::spirv::options opt; + // So a shader can `#include` another from the same directory. + opt.includes = { "shaders" }; + return mcpp::rules::spirv::compile(opt) ? 0 : 1; +} diff --git a/examples/10-vulkan-compute/app/include/saxpy/saxpy.h b/examples/10-vulkan-compute/app/include/saxpy/saxpy.h new file mode 100644 index 00000000..4a017861 --- /dev/null +++ b/examples/10-vulkan-compute/app/include/saxpy/saxpy.h @@ -0,0 +1,16 @@ +#pragma once + +// The device interface, in the one shape every device API agrees on: raw +// pointers and a count. The seam module above it turns that back into C++. +// +// Exactly one definition of this symbol is linked: the Vulkan island when the +// build names an accelerator, the CPU file when it does not. +#ifdef __cplusplus +extern "C" { +#endif + +int saxpy_device(float a, const float* x, const float* y, float* out, unsigned n); + +#ifdef __cplusplus +} +#endif diff --git a/examples/10-vulkan-compute/app/mcpp.toml b/examples/10-vulkan-compute/app/mcpp.toml new file mode 100644 index 00000000..2dd09a9a --- /dev/null +++ b/examples/10-vulkan-compute/app/mcpp.toml @@ -0,0 +1,62 @@ +[package] +name = "vulkan-saxpy" +namespace = "example" +version = "0.1.0" +description = "A Vulkan compute shader behind a seam module, with a CPU fallback" +accelerators = ["vulkan"] + +[language] +standard = "c++23" +modules = true +import_std = true + +# The rule that compiles the shaders lives in the official plugin collection, +# selected by its feature; `build.mcpp` imports it as `mcpp.rules.spirv`. +[dependencies.mcpp] +plugins = { version = "0.1.0", features = ["rules-spirv"], host-module = true } + +# The Khronos loader, built by the index rather than taken from the host, and +# the adapter that makes the host's own ICDs reachable from a binary running +# under mcpp's private loader. Neither one is a driver: a driver has to match +# the kernel module on the machine it runs on, which is why the software one +# below is a payload and the hardware ones are the host's. +[dependencies.compat] +vulkan = "1.4.357.0" +vulkan-runtime = "2026.09.05" + +# The two payloads this build names rather than discovers: the shader compiler, +# and a Vulkan driver that is always present because it is the CPU. With +# which is what every CI runner in this ecosystem is. +[xlings.workspace] +"xim:glslang" = "15.1.0" + +[build] +# What this build compiles device code FOR. `vulkan1.2` is the SPIR-V target +# environment, and the rule package derives `--target-env` from it: the same +# route by which `sm_89` reaches nvcc in examples/09. Unlike CUDA there is no +# architecture set, because SPIR-V is the portable form and which device +# executes it is decided by the driver at run time. +accel = "vulkan1.2" +sources = [ + "src/*.cppm", + "src/*.cpp", + # The shaders carry the accel they are for. They are not C++ and the engine + # never offers them to the C++ compiler; the constrained glob is what routes + # them to the build program instead. + { glob = "shaders/*.comp", accel = "vulkan1.2" }, +] +include_dirs = ["include"] + +# The Vulkan island, compiled only when the build names the accelerator. +[target.'cfg(accelerator = "vulkan")'.build] +sources = ["src/vulkan/*.cpp"] + +# The CPU-only variant: the same seam, a plain loop behind it. This is what +# `mcpp build --no-accel` produces, and the two files define the same symbol +# and are never in one link. +[target.'cfg(not(accelerator = "vulkan"))'.build] +sources = ["src/cpu/*.cpp"] + +[targets.vulkan-saxpy] +kind = "bin" +main = "src/main.cpp" diff --git a/examples/10-vulkan-compute/app/shaders/scale.comp b/examples/10-vulkan-compute/app/shaders/scale.comp new file mode 100644 index 00000000..360eb264 --- /dev/null +++ b/examples/10-vulkan-compute/app/shaders/scale.comp @@ -0,0 +1,15 @@ +#version 450 + +// The device side of the same computation `examples/09-cuda-kernel` runs on +// CUDA: out = a*x + y. One storage buffer holds all three vectors so the host +// side needs one allocation and one descriptor. +layout(local_size_x = 64) in; + +layout(std430, binding = 0) buffer Data { float v[]; }; +layout(push_constant) uniform Push { float a; uint n; } push; + +void main() { + const uint i = gl_GlobalInvocationID.x; + if (i >= push.n) return; + v[2u * push.n + i] = push.a * v[i] + v[push.n + i]; +} diff --git a/examples/10-vulkan-compute/app/src/app.cppm b/examples/10-vulkan-compute/app/src/app.cppm new file mode 100644 index 00000000..266d6d66 --- /dev/null +++ b/examples/10-vulkan-compute/app/src/app.cppm @@ -0,0 +1,22 @@ +// The seam, in the shape `examples/09-cuda-kernel` uses, for the same reason: +// it is the one place a backend can be exchanged. Underneath is a Vulkan +// compute island on one build and a plain loop on another, and no importer of +// this module can tell. +module; +#include "saxpy/saxpy.h" +export module app.saxpy; +import std; + +export namespace app { + +std::optional> +saxpy(float a, std::span x, std::span y) { + if (x.size() != y.size()) return std::nullopt; + std::vector out(x.size()); + if (saxpy_device(a, x.data(), y.data(), out.data(), + static_cast(x.size())) != 0) + return std::nullopt; + return out; +} + +} // namespace app diff --git a/examples/10-vulkan-compute/app/src/cpu/saxpy.cpp b/examples/10-vulkan-compute/app/src/cpu/saxpy.cpp new file mode 100644 index 00000000..e6ec01b9 --- /dev/null +++ b/examples/10-vulkan-compute/app/src/cpu/saxpy.cpp @@ -0,0 +1,11 @@ +// The CPU implementation behind the same seam. Compiled only when the build +// asks for no accelerator (`mcpp build --no-accel`), through the +// `cfg(not(accelerator = "vulkan"))` section of the manifest; the Vulkan +// island and this file define the same symbol and are never in one link. +#include "saxpy/saxpy.h" + +extern "C" int saxpy_device(float a, const float* x, const float* y, + float* out, unsigned n) { + for (unsigned i = 0; i < n; ++i) out[i] = a * x[i] + y[i]; + return 0; +} diff --git a/examples/10-vulkan-compute/app/src/main.cpp b/examples/10-vulkan-compute/app/src/main.cpp new file mode 100644 index 00000000..6efc1938 --- /dev/null +++ b/examples/10-vulkan-compute/app/src/main.cpp @@ -0,0 +1,12 @@ +import std; +import app.saxpy; + +int main() { + const std::vector x{1, 2, 3, 4}, y{10, 20, 30, 40}; + auto out = app::saxpy(2.0f, x, y); + if (!out) { std::println("device unavailable"); return 1; } + for (auto v : *out) std::print("{} ", v); + std::println(""); + const std::vector want{12, 24, 36, 48}; + return *out == want ? 0 : 1; +} diff --git a/examples/10-vulkan-compute/app/src/vulkan/saxpy.cpp b/examples/10-vulkan-compute/app/src/vulkan/saxpy.cpp new file mode 100644 index 00000000..80c8010c --- /dev/null +++ b/examples/10-vulkan-compute/app/src/vulkan/saxpy.cpp @@ -0,0 +1,277 @@ +// The Vulkan compute island. +// +// It is an island for the same reason the CUDA one is: the API underneath is +// C, the memory it operates on is not the program's, and the seam above turns +// both back into C++. What is different is where the device code came from — +// nothing here was compiled by the C++ toolchain. `scale_comp.h` is a +// `const uint32_t[]` that `mcpp.rules.spirv` produced from `shaders/scale.comp`, +// and the driver compiles it for whatever device is present when the program +// runs. That is the property this example exists to show: one artifact, no +// per-GPU build, and a CPU rasteriser is a legitimate device. +#include "saxpy/saxpy.h" + +#include + +#include +#include +#include + +// Generated by mcpp.rules.spirv into the build directory, which the rule puts on +// the include path. The symbol name is derived from the file name: +// `shaders/scale.comp` -> `scale_comp_spv`. +#include "scale_comp.h" + +namespace { + +struct push_constants { float a; std::uint32_t n; }; + +// A single allocation holds x, y and out back to back, so the shader needs one +// binding and this file one memory mapping. +constexpr std::uint32_t kVectorsInBuffer = 3; + +// Any memory that is host-visible and coherent will do. A discrete GPU offers +// a non-device-local heap with those properties and a software rasteriser has +// nothing else, so this is the one requirement both satisfy; a real workload +// would stage through device-local memory and this one would gain nothing from +// it. +int find_memory_type(VkPhysicalDevice phys, std::uint32_t bits, + VkMemoryPropertyFlags want) { + VkPhysicalDeviceMemoryProperties props{}; + vkGetPhysicalDeviceMemoryProperties(phys, &props); + for (std::uint32_t i = 0; i < props.memoryTypeCount; ++i) + if ((bits & (1u << i)) && (props.memoryTypes[i].propertyFlags & want) == want) + return static_cast(i); + return -1; +} + +// The first device with a compute queue. Not "the fastest": what this program +// demonstrates is that it runs wherever a driver exists, and a machine whose +// only driver is lavapipe is the case the example is written for. +bool pick_device(VkInstance inst, VkPhysicalDevice& out, std::uint32_t& family) { + std::uint32_t n = 0; + vkEnumeratePhysicalDevices(inst, &n, nullptr); + if (n == 0) return false; + std::vector devices(n); + vkEnumeratePhysicalDevices(inst, &n, devices.data()); + for (auto d : devices) { + std::uint32_t qn = 0; + vkGetPhysicalDeviceQueueFamilyProperties(d, &qn, nullptr); + std::vector qs(qn); + vkGetPhysicalDeviceQueueFamilyProperties(d, &qn, qs.data()); + for (std::uint32_t i = 0; i < qn; ++i) + if (qs[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { out = d; family = i; return true; } + } + return false; +} + +} // namespace + +extern "C" int saxpy_device(float a, const float* x, const float* y, + float* out, unsigned n) { + if (n == 0) return 0; + + VkApplicationInfo app{}; + app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + app.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo ici{}; + ici.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + ici.pApplicationInfo = &app; + + VkInstance inst{}; + if (vkCreateInstance(&ici, nullptr, &inst) != VK_SUCCESS) return 1; + + VkPhysicalDevice phys{}; + std::uint32_t family = 0; + if (!pick_device(inst, phys, family)) { vkDestroyInstance(inst, nullptr); return 1; } + + const float priority = 1.0f; + VkDeviceQueueCreateInfo qci{}; + qci.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + qci.queueFamilyIndex = family; + qci.queueCount = 1; + qci.pQueuePriorities = &priority; + VkDeviceCreateInfo dci{}; + dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + dci.queueCreateInfoCount = 1; + dci.pQueueCreateInfos = &qci; + + VkDevice dev{}; + if (vkCreateDevice(phys, &dci, nullptr, &dev) != VK_SUCCESS) { + vkDestroyInstance(inst, nullptr); + return 1; + } + + int rc = 1; + VkQueue queue{}; + vkGetDeviceQueue(dev, family, 0, &queue); + + const VkDeviceSize bytes = VkDeviceSize(n) * kVectorsInBuffer * sizeof(float); + VkBufferCreateInfo bci{}; + bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bci.size = bytes; + bci.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + VkBuffer buffer{}; + VkDeviceMemory memory{}; + VkShaderModule shader{}; + VkDescriptorSetLayout setLayout{}; + VkPipelineLayout pipeLayout{}; + VkPipeline pipeline{}; + VkDescriptorPool pool{}; + VkCommandPool cmdPool{}; + VkFence fence{}; + + if (vkCreateBuffer(dev, &bci, nullptr, &buffer) != VK_SUCCESS) goto done; + + { + VkMemoryRequirements req{}; + vkGetBufferMemoryRequirements(dev, buffer, &req); + const int type = find_memory_type(phys, req.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT + | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + if (type < 0) goto done; + VkMemoryAllocateInfo mai{}; + mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + mai.allocationSize = req.size; + mai.memoryTypeIndex = static_cast(type); + if (vkAllocateMemory(dev, &mai, nullptr, &memory) != VK_SUCCESS) goto done; + if (vkBindBufferMemory(dev, buffer, memory, 0) != VK_SUCCESS) goto done; + + void* mapped = nullptr; + if (vkMapMemory(dev, memory, 0, bytes, 0, &mapped) != VK_SUCCESS) goto done; + auto* v = static_cast(mapped); + std::memcpy(v, x, n * sizeof(float)); + std::memcpy(v + n, y, n * sizeof(float)); + std::memset(v + 2 * n, 0, n * sizeof(float)); + vkUnmapMemory(dev, memory); + } + + { + VkShaderModuleCreateInfo smci{}; + smci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + smci.codeSize = sizeof scale_comp_spv; + smci.pCode = scale_comp_spv; + if (vkCreateShaderModule(dev, &smci, nullptr, &shader) != VK_SUCCESS) goto done; + + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + VkDescriptorSetLayoutCreateInfo dslci{}; + dslci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + dslci.bindingCount = 1; + dslci.pBindings = &binding; + if (vkCreateDescriptorSetLayout(dev, &dslci, nullptr, &setLayout) != VK_SUCCESS) + goto done; + + VkPushConstantRange range{}; + range.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + range.size = sizeof(push_constants); + VkPipelineLayoutCreateInfo plci{}; + plci.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + plci.setLayoutCount = 1; + plci.pSetLayouts = &setLayout; + plci.pushConstantRangeCount = 1; + plci.pPushConstantRanges = ⦥ + if (vkCreatePipelineLayout(dev, &plci, nullptr, &pipeLayout) != VK_SUCCESS) goto done; + + VkComputePipelineCreateInfo cpci{}; + cpci.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + cpci.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + cpci.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + cpci.stage.module = shader; + cpci.stage.pName = "main"; + cpci.layout = pipeLayout; + if (vkCreateComputePipelines(dev, VK_NULL_HANDLE, 1, &cpci, nullptr, &pipeline) + != VK_SUCCESS) + goto done; + } + + { + VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}; + VkDescriptorPoolCreateInfo dpci{}; + dpci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + dpci.maxSets = 1; + dpci.poolSizeCount = 1; + dpci.pPoolSizes = &size; + if (vkCreateDescriptorPool(dev, &dpci, nullptr, &pool) != VK_SUCCESS) goto done; + + VkDescriptorSetAllocateInfo dsai{}; + dsai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + dsai.descriptorPool = pool; + dsai.descriptorSetCount = 1; + dsai.pSetLayouts = &setLayout; + VkDescriptorSet set{}; + if (vkAllocateDescriptorSets(dev, &dsai, &set) != VK_SUCCESS) goto done; + + VkDescriptorBufferInfo info{buffer, 0, bytes}; + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = set; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &info; + vkUpdateDescriptorSets(dev, 1, &write, 0, nullptr); + + VkCommandPoolCreateInfo cpi{}; + cpi.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + cpi.queueFamilyIndex = family; + if (vkCreateCommandPool(dev, &cpi, nullptr, &cmdPool) != VK_SUCCESS) goto done; + + VkCommandBufferAllocateInfo cbai{}; + cbai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + cbai.commandPool = cmdPool; + cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + cbai.commandBufferCount = 1; + VkCommandBuffer cmd{}; + if (vkAllocateCommandBuffers(dev, &cbai, &cmd) != VK_SUCCESS) goto done; + + VkCommandBufferBeginInfo begin{}; + begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &begin); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeLayout, + 0, 1, &set, 0, nullptr); + const push_constants pc{a, n}; + vkCmdPushConstants(cmd, pipeLayout, VK_SHADER_STAGE_COMPUTE_BIT, + 0, sizeof pc, &pc); + // The workgroup size is in the shader (`local_size_x = 64`); the count + // here has to agree with it, which is why the shader also bounds-checks. + vkCmdDispatch(cmd, (n + 63) / 64, 1, 1); + vkEndCommandBuffer(cmd); + + VkFenceCreateInfo fci{}; + fci.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + if (vkCreateFence(dev, &fci, nullptr, &fence) != VK_SUCCESS) goto done; + + VkSubmitInfo submit{}; + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &cmd; + if (vkQueueSubmit(queue, 1, &submit, fence) != VK_SUCCESS) goto done; + if (vkWaitForFences(dev, 1, &fence, VK_TRUE, ~0ull) != VK_SUCCESS) goto done; + + void* mapped = nullptr; + if (vkMapMemory(dev, memory, 0, bytes, 0, &mapped) != VK_SUCCESS) goto done; + std::memcpy(out, static_cast(mapped) + 2 * n, n * sizeof(float)); + vkUnmapMemory(dev, memory); + rc = 0; + } + +done: + if (fence) vkDestroyFence(dev, fence, nullptr); + if (cmdPool) vkDestroyCommandPool(dev, cmdPool, nullptr); + if (pool) vkDestroyDescriptorPool(dev, pool, nullptr); + if (pipeline) vkDestroyPipeline(dev, pipeline, nullptr); + if (pipeLayout) vkDestroyPipelineLayout(dev, pipeLayout, nullptr); + if (setLayout) vkDestroyDescriptorSetLayout(dev, setLayout, nullptr); + if (shader) vkDestroyShaderModule(dev, shader, nullptr); + if (memory) vkFreeMemory(dev, memory, nullptr); + if (buffer) vkDestroyBuffer(dev, buffer, nullptr); + vkDestroyDevice(dev, nullptr); + vkDestroyInstance(inst, nullptr); + return rc; +} diff --git a/mcpp.toml b/mcpp.toml index 2d6ed50c..40ce7513 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.5.2" +version = "2026.9.5.3" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/buildmcpp/src/directives.cppm b/modules/buildmcpp/src/directives.cppm index 69df9735..b6cc87dd 100644 --- a/modules/buildmcpp/src/directives.cppm +++ b/modules/buildmcpp/src/directives.cppm @@ -76,14 +76,14 @@ enum class Slot : std::size_t { // is neither a compile input nor a link input, and putting it in LdFlags // would put an emulator's argv on the linker command line. Runner, - // ⭐⭐ A NAMED WAY OF REACHING THE ARTEFACT. ONE SLOT, ANY NUMBER OF NAMES. + // A NAMED WAY OF REACHING THE ARTEFACT. ONE SLOT, ANY NUMBER OF NAMES. // // `Runner` above is the default — how the artefact is EXECUTED. Writing it // to a device, watching what it prints, starting a debug server, deploying // it, serving it: all the same shape, an argv the PACKAGE supplies, and the // only thing that distinguishes them is a name. // - // ⚠️ THE NAME IS DATA. An earlier version gave `flash`, `monitor` and + // THE NAME IS DATA. An earlier version gave `flash`, `monitor` and // `debug` their own slots — which put EMBEDDED vocabulary in the engine, // so a web package could not add `serve` nor a cluster package `submit` // without an engine release. The value here is `:`, one token @@ -122,7 +122,7 @@ enum class Slot : std::size_t { // A sentence for the USER. Not a build input at all — see Scope::Advisory // for why this could not be folded into any existing slot. Warnings, - // ⭐ A CLAIM ABOUT THE MACHINE, OR ABOUT WHAT THIS PACKAGE NEEDS OF IT. + // A CLAIM ABOUT THE MACHINE, OR ABOUT WHAT THIS PACKAGE NEEDS OF IT. // // `fact` carries `=`: something the program established // about the machine, by whatever means the package owns (a driver's @@ -139,7 +139,7 @@ enum class Slot : std::size_t { }; inline constexpr std::size_t kSlotCount = static_cast(Slot::Count); -// ⚠️ THERE IS DELIBERATELY NO LIST OF ACTION NAMES HERE. +// THERE IS DELIBERATELY NO LIST OF ACTION NAMES HERE. // // An earlier version carried `kDeviceSlots`, `device_slot_name()` and a // `Semantics` function switching on four hardcoded values. Every one of them @@ -161,7 +161,7 @@ enum class Scope { SourceSet, // joins the compile set RerunKey, // not a build input at all; only feeds the re-run key GraphNode, // declares an edge in the build graph; see manifest::BuildAction - // ⚠️ REACHES THE USER RATHER THAN THE BUILD, AND THAT IS WHY IT IS A + // REACHES THE USER RATHER THAN THE BUILD, AND THAT IS WHY IT IS A // SEVENTH VALUE RATHER THAN A REUSED ONE. // // Every other scope answers "which part of the build sees this". An @@ -226,7 +226,7 @@ inline constexpr std::array kTable{{ {"cfg", "define", Slot::Defines, Scope::PackagePrivate, Transform::DefinePrefix, false, "", "", 1}, {"generated", "generated", Slot::Generated, Scope::SourceSet, Transform::Verbatim, true, "declared generated source", "but it does not exist after the run", 1}, {"source", "source", Slot::Sources, Scope::SourceSet, Transform::Verbatim, true, "selected source", "(mcpp:source=) but no such file exists", 1}, - // ⚠️ LinkGlobal, and that is the whole reason this row exists. + // LinkGlobal, and that is the whole reason this row exists. // // A board-support package is the one thing that knows a board's memory // layout, and a linker script is how that layout is expressed. Every other @@ -243,14 +243,14 @@ inline constexpr std::array kTable{{ // Single-valued in practice — two scripts on one line is an lld error, and // that error names both, which is a better diagnostic than anything a // conflict check here would produce. - // ⚠️ `mustExistAfterRun` is FALSE, and not by oversight. That contract + // `mustExistAfterRun` is FALSE, and not by oversight. That contract // assumes the directive's value IS a path (`generated=`, `source=`), and // this one's transformed value is `-T ` — so the check would test // the wrong string and reject a script that is right there. Special-casing // the contract for one row would cost more than it buys: lld's own error // is already exact ("cannot find linker script "), which is the // condition the contract exists to make legible. - // ⚠️ One argv TOKEN per line, in emission order. + // One argv TOKEN per line, in emission order. // // argv is an ordered list and a directive is one line = one value, so the // list is built by repetition. The alternative — a JSON array, as `action` @@ -272,7 +272,7 @@ inline constexpr std::array kTable{{ {"rerun-if-changed", "", Slot::RerunFiles, Scope::RerunKey, Transform::Verbatim, false, "", "", 1}, {"rerun-if-env-changed","", Slot::RerunEnv, Scope::RerunKey, Transform::Verbatim, false, "", "", 1}, {"rerun-if-changed-glob","", Slot::RerunGlobs, Scope::RerunKey, Transform::Verbatim, false, "", "", 2}, - // ⚠️ THE ONE THING A BUILD PROGRAM COULD NOT DO BEFORE: SUCCEED AND STILL + // THE ONE THING A BUILD PROGRAM COULD NOT DO BEFORE: SUCCEED AND STILL // SAY SOMETHING. // // mcpp captures a build program and prints what it captured only on a @@ -287,14 +287,14 @@ inline constexpr std::array kTable{{ // `xpkg_dir` returns empty and the program configures no runner. Correct, // silent, and indistinguishable to the user from a package that forgot. // - // ⚠️ `tag` IS NON-EMPTY, AND THAT IS LOAD-BEARING. A build program's + // `tag` IS NON-EMPTY, AND THAT IS LOAD-BEARING. A build program's // result is cached and a hit does not re-run it, so an advisory that lived // only on the run path would appear once and never again — the same // failure shape as the note that was deleted, arrived at from the other // side. A non-empty tag puts it in the cache record, and `serialize` / // `accept_cache_record` are table-driven, so the replay costs nothing. // - // ⚠️ kCacheEpoch is deliberately NOT bumped for this row. Entries written + // kCacheEpoch is deliberately NOT bumped for this row. Entries written // before it carry no `d warning` line, and the programs that wrote them // could not emit one — so replaying them yields exactly what the program // said, and the entry is still correct. Bumping would re-run every build @@ -365,7 +365,7 @@ std::optional protocol_error(const Directives& d); // The `mcpp:warning=` lines a program emitted, each already prefixed with the // package it came from. // -// ⚠️ THE FORMATTING LIVES HERE AND THE PRINTING DOES NOT, for two reasons that +// THE FORMATTING LIVES HERE AND THE PRINTING DOES NOT, for two reasons that // pull the same way. This module deliberately imports no UI — a directive // table that knew how to draw would be a different kind of thing. And there // are TWO call sites, a run and a cache hit, which is exactly the shape that @@ -446,7 +446,7 @@ std::string action_error(const Directives& d); // there, and rewriting it would make ninja think the input changed on every // prepare. // -// ⚠️ ONLY FOR OUTPUTS THAT ARE TRANSLATION UNITS, which is why this needs the +// ONLY FOR OUTPUTS THAT ARE TRANSLATION UNITS, which is why this needs the // table. A placeholder exists so the SCAN has something to read, and the scan // never reads a header — but writing one anyway turned "the generator did not // run" into "the header is empty", and mcpp#534 was diagnosed as a race for @@ -620,7 +620,7 @@ std::optional protocol_error(const Directives& d) { std::string list; for (auto const& k : d.unknownKeys) list += (list.empty() ? "" : ", ") + ("mcpp:" + k); - // ⚠️ NOT "so it must be a typo". + // NOT "so it must be a typo". // // That is what this said, and adding `link-script` in protocol 3 // proved it wrong: a package written against a newer mcpp reaches an @@ -733,7 +733,7 @@ void apply(mcpp::manifest::Manifest& m, const Directives& d) { bc.ldflags.insert(bc.ldflags.end(), ld.begin(), ld.end()); // Appended in emission order — the tokens ARE the argv. bc.runner.insert(bc.runner.end(), runner.begin(), runner.end()); - // ⭐ `:`, split on the FIRST colon. A token may contain colons + // `:`, split on the FIRST colon. A token may contain colons // (a Windows path, a URL); a name may not, which is what makes the first // one unambiguous. for (auto const& entry : d.at(Slot::NamedRunner)) { @@ -743,7 +743,7 @@ void apply(mcpp::manifest::Manifest& m, const Directives& d) { } for (auto const& name : d.at(Slot::RunnerLongLived)) bc.namedRunners[name].longLived = true; - // ⚠️ ANY non-empty value sets it, and nothing can unset it. Exclusivity is a + // ANY non-empty value sets it, and nothing can unset it. Exclusivity is a // claim about the DEVICE: if one package knows the target is a mutex, it is // one, and a later package saying nothing must not relax that. if (!d.at(Slot::RunExclusive).empty()) bc.runExclusive = true; diff --git a/modules/buildmcpp/src/program_protocol.cppm b/modules/buildmcpp/src/program_protocol.cppm index 8cbc98ac..88b92656 100644 --- a/modules/buildmcpp/src/program_protocol.cppm +++ b/modules/buildmcpp/src/program_protocol.cppm @@ -46,7 +46,7 @@ export namespace mcpp::build::program_protocol { // and "warn and ignore" would turn that into a silently different build. // v2 (#359): adds `rerun-if-changed-glob`. // v5: adds `warning` — the channel a program uses to succeed and still say -// something. ⚠️ A package that calls `mcpp::warning()` does not fail on an +// something. A package that calls `mcpp::warning()` does not fail on an // older engine with a PROTOCOL error; it fails earlier, at the build.mcpp // COMPILE, because the bundled module that engine ships has no such function. // That is the same cost `link-script` carried into v3 and is stated here so diff --git a/modules/buildmcpp/src/provisions.cppm b/modules/buildmcpp/src/provisions.cppm index e5001804..8027cc4c 100644 --- a/modules/buildmcpp/src/provisions.cppm +++ b/modules/buildmcpp/src/provisions.cppm @@ -309,6 +309,54 @@ inline std::string host_module_name(const std::filesystem::path& interfacePath, return declared.front(); } +// The name a PRIMARY module interface unit declares, or an empty string. +// +// `declared_module_roots` answers a different question -- every module a file +// names, implementation units included -- and is the right reader for the lib +// root, which `build_host_module` compiles whatever kind of unit it is. The +// question here is which OTHER files of a package are interface units at all. +// A package that exposes several rules through features lists their sources +// (mcpp 2026.9.5.3+), and an implementation unit or a partition among those +// cannot be compiled alone under a name of its own. Only `export module +// ;` qualifies: `module ;`, `export module :;` and the +// global module fragment `module;` do not, and a line that is a comment does +// not either -- a rule's own documentation may quote the declaration. +inline std::string declared_interface_name(std::string_view source) +{ + auto is_name = [](char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') || c == '_' || c == '.'; + }; + auto is_ws = [](char c) { return c == ' ' || c == '\t' || c == '\r'; }; + std::size_t lineStart = 0; + while (lineStart < source.size()) { + auto eol = source.find('\n', lineStart); + if (eol == std::string_view::npos) eol = source.size(); + auto line = source.substr(lineStart, eol - lineStart); + lineStart = eol + 1; + + std::size_t i = 0; + while (i < line.size() && is_ws(line[i])) ++i; + if (line.substr(i).starts_with("//")) continue; + if (!line.substr(i).starts_with("export")) continue; + i += 6; + if (i >= line.size() || !is_ws(line[i])) continue; + while (i < line.size() && is_ws(line[i])) ++i; + if (!line.substr(i).starts_with("module")) continue; + i += 6; + if (i >= line.size() || !is_ws(line[i])) continue; + while (i < line.size() && is_ws(line[i])) ++i; + std::size_t start = i; + while (i < line.size() && is_name(line[i])) ++i; + if (i == start) continue; // `export module;` or `:part` + std::string name(line.substr(start, i - start)); + while (i < line.size() && is_ws(line[i])) ++i; + if (i >= line.size() || line[i] != ';') continue; // a partition, or noise + return name; + } + return {}; +} + // One host module as registered for one consumer's build program. struct HostModule { std::string module; // what `import` in build.mcpp addresses diff --git a/modules/manifest/src/dep_spec.cppm b/modules/manifest/src/dep_spec.cppm index 9dae4b80..37131f7c 100644 --- a/modules/manifest/src/dep_spec.cppm +++ b/modules/manifest/src/dep_spec.cppm @@ -42,7 +42,7 @@ struct DependencySpec { // #519 — "static" | "shared" | "" (no request). How the consumer wants // this dependency to arrive in its images. // - // ⚠️ HONOURED ONLY ON THE ROOT MANIFEST'S EDGES, and that is a + // HONOURED ONLY ON THE ROOT MANIFEST'S EDGES, and that is a // supply-chain property rather than a simplification. A form is a // whole-image decision: if a dependency four levels down could impose one, // any package at any depth could change how the final program is laid out diff --git a/modules/manifest/src/targetside_model.cppm b/modules/manifest/src/targetside_model.cppm index 034a6502..403fd5b2 100644 --- a/modules/manifest/src/targetside_model.cppm +++ b/modules/manifest/src/targetside_model.cppm @@ -81,7 +81,7 @@ enum class EnvAxis { Unknown, CLibrary, ObjectAbi, ObjectFormat }; // The noun for a segment, as it appears in the report. Empty for `Unknown`, // because a report that cannot name the axis says nothing rather than guessing. // -// ⚠️ THE VALUE, NOT ONLY THE AXIS. `gnu` and `msvc` sit on the same axis and +// THE VALUE, NOT ONLY THE AXIS. `gnu` and `msvc` sit on the same axis and // select opposite ABIs, so a noun fixed per axis would print "the Itanium C++ // ABI" for an MSVC build. Naming the ABI rather than the axis is deliberate: // `Itanium` and `MSVC` appear in no row of the report, so neither can be @@ -139,7 +139,7 @@ struct Layer { // cxx the C++ library and its libc++ / libstdc++ / MSVC STL // ABI runtime // -// ⚠️ `compilerRuntime` IS NOT PART OF `cxx`, AND THE DISTINCTION WAS MEASURED +// `compilerRuntime` IS NOT PART OF `cxx`, AND THE DISTINCTION WAS MEASURED // BEFORE IT WAS NAMED. The builtins (`__udivti3` and its relatives) are what a // PURE C PROGRAM needs. Counting them as part of the C++ runtime is the same // error as the one recorded at the head of this file: a C program crossed to @@ -147,7 +147,7 @@ struct Layer { // link line kept the payload's own libc++ and handed a Linux shared object to a // Mach-O linker. A layer that only some programs need is still a layer. // -// ⚠️ `kernelAbi` HAS NO NAME ON A TRADITIONAL STACK. A C library issues system +// `kernelAbi` HAS NO NAME ON A TRADITIONAL STACK. A C library issues system // calls or calls the platform's own entry points directly, and nothing names the // seam. Naming it is what lets one C library sit above four platforms, which is // why this field reads `—` for a picolibc bare-metal build and `openkal` for an @@ -195,7 +195,7 @@ struct TargetSide { return kernelAbi.fromGraph() || cAbi.fromGraph(); } - // ⚠️ AND THE C LIBRARY IS A SEPARATE QUESTION, WHICH THE ONE ABOVE WAS + // AND THE C LIBRARY IS A SEPARATE QUESTION, WHICH THE ONE ABOVE WAS // ANSWERING FOR IT AND GETTING WRONG. // // `system_from_graph` is an OR over two layers, and the link line's @@ -216,11 +216,11 @@ struct TargetSide { // error: hermetic link check failed // crt1.o (bare name — the linker cannot resolve it) // - // ⚠️ THIS SHIPPED, in 2026.8.24.1, and reached every backend in the + // THIS SHIPPED, in 2026.8.24.1, and reached every backend in the // ecosystem — that shape is how a backend is tested. Their CI was pinned // to an older mcpp and kept passing. // - // ⭐ THERE IS NO PREDICATE HERE, BECAUSE `Layer::prebuilt()` ALREADY IS + // THERE IS NO PREDICATE HERE, BECAUSE `Layer::prebuilt()` ALREADY IS // ONE. The question the link line asks is whether the C library came from // a DIRECTORY that existed before resolution (a payload, an xpkg sysroot) // or from packages that had to be resolved first — which is the very @@ -337,7 +337,7 @@ parse_capability(std::string_view entry) { // and this engine checks a relation it can state generically: the layer named // must resolve to the interface named. // -// ⚠️ It is also how `compiler-runtime` stays honest. libgcc is configured for +// It is also how `compiler-runtime` stays honest. libgcc is configured for // gcc and compiler-rt for clang; a build whose compiler is one and whose // runtime is the other resolves `__udivti3` differently from every other link in // the same program. mcpp does not know which runtime belongs to which family — @@ -384,7 +384,7 @@ struct Inputs { std::string compilerFamily; // "llvm" / "gcc" / "msvc" std::string compilerVersion; - // ⚠️ THE C LIBRARY THE TRIPLE ASKED FOR, WHICH IS NOT THE SAME QUESTION AS + // THE C LIBRARY THE TRIPLE ASKED FOR, WHICH IS NOT THE SAME QUESTION AS // WHICH ONE RESOLVED. // // `x86_64-linux-musl` states a request; `x86_64-linux` declines to. The @@ -395,7 +395,7 @@ struct Inputs { std::string requestedCAbi; // The same target spelled without that segment, for the suggestion. std::string requestFreeTarget; - // ⚠️ WHAT THE ENV SEGMENT NAMES ON THIS PLATFORM. It is a different axis + // WHAT THE ENV SEGMENT NAMES ON THIS PLATFORM. It is a different axis // per OS, and a boolean here was a lossy encoding of that. // // On Linux the segment names the C library — `gnu` is glibc, `musl` is musl @@ -429,7 +429,7 @@ struct Inputs { // The name of the C library a compiler payload carries for a target. // -// ⚠️ THE TRIPLE'S ENV FIELD ANSWERS THIS ONLY WHERE THE TRIPLE HAS ONE, AND +// THE TRIPLE'S ENV FIELD ANSWERS THIS ONLY WHERE THE TRIPLE HAS ONE, AND // FALLING BACK TO `glibc` NAMED A LIBRARY THAT DOES NOT EXIST ON THE PLATFORM. // Measured on macOS, where the canonical triple carries no env segment: // @@ -439,11 +439,11 @@ struct Inputs { // build that had something to say; showing the whole stack made a wrong label // into a wrong statement. // -// ⚠️ These names are PAYLOAD facts, which is why they may be written here at +// These names are PAYLOAD facts, which is why they may be written here at // all: mcpp ships those payloads and knows what is inside them. What must never // be written here is what a PACKAGE supplies — that is the difference the // reserved-capability grammar exists to keep. -// ⚠️ THE TRIPLE'S ENV SEGMENT IS A TRIPLE SPELLING, NOT A C LIBRARY'S NAME, AND +// THE TRIPLE'S ENV SEGMENT IS A TRIPLE SPELLING, NOT A C LIBRARY'S NAME, AND // THE TWO COINCIDE ONLY SOMETIMES. `musl` is both. `gnu` is neither: on Linux it // means glibc, and on Windows it names the MinGW flavour of the toolchain, whose // C runtime is the same UCRT the MSVC flavour links. @@ -503,7 +503,7 @@ inline TargetSide resolve(const Inputs& in) { // compiler-runtime — the builtins and the unwinder. // - // ⚠️ ABSENT FROM THE GRAPH DOES NOT MEAN ABSENT. Every compiler payload + // ABSENT FROM THE GRAPH DOES NOT MEAN ABSENT. Every compiler payload // ships one; a package supplies it only when the payload's own is the wrong // one for this target, which is the same shape as every other layer here. // The payload's is reported under the compiler's own name because that is @@ -601,7 +601,7 @@ inline std::optional check_layering(const TargetSide& ts) { // ── Rule two, part two: declared requirements ──────────────────────────────── // -// ⚠️ A REQUIREMENT IS CHECKED AGAINST THE RESOLVED LAYER, NOT AGAINST THE +// A REQUIREMENT IS CHECKED AGAINST THE RESOLVED LAYER, NOT AGAINST THE // REQUEST. `requires = ["mcpp:compiler=llvm"]` is satisfied by whatever the // compiler layer actually resolved to, which is the only value that will be on // the command line. @@ -611,7 +611,7 @@ inline std::optional check_layering(const TargetSide& ts) { // reported by naming both — which is what a reader needs and what an engine // hardcoding a table of families could not produce for a family it had not // heard of. -// ⚠️ `compilerStatedBy` NAMES WHERE THE COMPILER CAME FROM, AND THE ADVICE IS +// `compilerStatedBy` NAMES WHERE THE COMPILER CAME FROM, AND THE ADVICE IS // WRONG WITHOUT IT. // // Until 2026.8.26.2 a compiler requirement mcpp could satisfy by itself still @@ -697,7 +697,7 @@ check_requirements(const TargetSide& ts, std::span reqs, // ── The triple is a request; the target side is the fact ──────────────────── // -// ⚠️ REPORTED RATHER THAN REFUSED, AND THE SEVERITY WAS DECIDED BY A +// REPORTED RATHER THAN REFUSED, AND THE SEVERITY WAS DECIDED BY A // MEASUREMENT RATHER THAN BY THE PRINCIPLE. // // The first version refused. It is the semantically clean answer — the name @@ -713,11 +713,11 @@ check_requirements(const TargetSide& ts, std::span reqs, // it is a build whose name misdescribes it, and saying so is the whole // remedy. // -// ⚠️ The remedy has to be actionable, which is why `x86_64-linux` had to work +// The remedy has to be actionable, which is why `x86_64-linux` had to work // first. Telling someone their target name is wrong is only useful once there // is a right one to give them. inline std::optional check_request(const TargetSide& ts) { - // ⚠️ TWO AXES REACH HERE, AND EXEMPTING THE SECOND WAS THE DEFECT. + // TWO AXES REACH HERE, AND EXEMPTING THE SECOND WAS THE DEFECT. // // `CLibrary` is the obvious one: on Linux the segment names the C library // outright. `ObjectAbi` was exempted on the grounds that `gnu` on Windows @@ -732,7 +732,7 @@ inline std::optional check_request(const TargetSide& ts) { // --target x86_64-linux-gnu c-abi musl (graph) warned // --target x86_64-windows-gnu c-abi musl (graph) silent ← the defect // - // ⚠️ `ObjectFormat` STAYS EXEMPT, and not for symmetry. `elf` never names a + // `ObjectFormat` STAYS EXEMPT, and not for symmetry. `elf` never names a // C library on any platform, so "the target name asks for the `elf` C ABI" // would be nonsense rather than merely noisy. That axis is glossed in the // report instead. @@ -810,7 +810,7 @@ inline std::string format_conflict(const Conflict& c) { // intention that goes stale when the packages beneath it change; this states // the outcome and cannot. // -// ⚠️ BY DEFAULT IT PRINTS ONLY THE LAYERS THE COMPILER PAYLOAD DID NOT SUPPLY. +// BY DEFAULT IT PRINTS ONLY THE LAYERS THE COMPILER PAYLOAD DID NOT SUPPLY. // A zero-configuration build resolves all five from one payload, and five lines // reading `(payload)` carry no information — they are the answer to a question // nobody asked. What earns a line is a layer that came from somewhere else. @@ -830,7 +830,7 @@ inline std::string format_layers(const TargetSide& ts, bool verbose) { { "c++-abi", ts.cxx }, }; - // ⚠️ WHETHER THE STACK IS SHOWN AT ALL IS DECIDED BEFORE ANY ROW IS + // WHETHER THE STACK IS SHOWN AT ALL IS DECIDED BEFORE ANY ROW IS // WRITTEN, AND THE FIRST VERSION DECIDED IT PER ROW. // // An absent layer is a statement rather than a gap, so it belongs in a @@ -875,7 +875,7 @@ inline std::string format_report(const TargetSide& ts, std::string_view targetNa ? std::format("{}", targetName) : std::format("{} → {}", targetName, ts.llvmTriple); - // ⚠️ WHEN THE SEGMENT IS NOT A C LIBRARY, SAY WHAT IT IS — HERE, WHERE THE + // WHEN THE SEGMENT IS NOT A C LIBRARY, SAY WHAT IT IS — HERE, WHERE THE // READER IS LOOKING AT IT. // // `x86_64-windows-gnu` above a line reading `c-abi musl` is not a @@ -889,7 +889,7 @@ inline std::string format_report(const TargetSide& ts, std::string_view targetNa // is linked. The other two are what `gnu` actually selected — the Itanium // C++ ABI rather than Microsoft's. // - // ⭐ AND IT CORRESPONDS TO NO ROW OF THIS REPORT, WHICH IS THE POINT. The + // AND IT CORRESPONDS TO NO ROW OF THIS REPORT, WHICH IS THE POINT. The // five layers record who SUPPLIES each layer; `gnu` names a convention the // OBJECTS FOLLOW, and several layers must agree on it. Pointing the reader // at `c++-abi libc++` would be a second wrong answer: libc++ is one @@ -906,7 +906,7 @@ inline std::string format_report(const TargetSide& ts, std::string_view targetNa // present, it does not name a C library here, and the C library came from // somewhere the segment did not choose. A payload C library IS selected by // the triple, so `gnu → ucrt` follows visibly and needs no gloss. - // ⚠️ `ObjectFormat` ONLY. The object-ABI axis used to be glossed here and is + // `ObjectFormat` ONLY. The object-ABI axis used to be glossed here and is // now WARNED about instead — see `check_request`. Leaving both in place // would say the same thing twice, once as an aside and once as a warning, // which reads as two different findings. diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index 0333df42..f16c49c1 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -14,7 +14,7 @@ import mcpp.pm.index_spec; import mcpp.platform; import mcpp.platform.axis; // the one macos/macosx spelling rule -// ⚠️ ANONYMOUS NAMESPACE, AND THIS COST TWO WINDOWS JOBS TO LEARN. +// ANONYMOUS NAMESPACE, AND THIS COST TWO WINDOWS JOBS TO LEARN. // // The first version of this helper sat at namespace scope in the module // purview, which makes its declaration part of what this module's interface @@ -37,7 +37,7 @@ namespace { // A dependency's version requirement, checked with the parser that will later // be asked to match it. // -// ⚠️ THE PARSER EXISTED AND THIS PATH DID NOT USE IT. +// THE PARSER EXISTED AND THIS PATH DID NOT USE IT. // // `version_req::parse_req` is what decides which published version satisfies a // requirement. The dependency reader handed its string straight to the @@ -55,7 +55,7 @@ namespace { // message that names the actual problem, at the point where the text was // written. // -// ⚠️ A WARNING AND NOT AN ERROR, AND THE FIRST VERSION GOT THIS WRONG. +// A WARNING AND NOT AN ERROR, AND THE FIRST VERSION GOT THIS WRONG. // // Rejecting the manifest breaks every consumer of a PUBLISHED package that // carries such a string — including one where the offending entry belongs to a @@ -67,7 +67,7 @@ namespace { // not invalidate a running program; equally, a new program must not invalidate // published data. A manifest check has no standing to do so over an entry that // may never be reached. -// ⚠️ RETURNS A PLAIN STRING, EMPTY MEANING "NO PROBLEM", AND NOT AN +// RETURNS A PLAIN STRING, EMPTY MEANING "NO PROBLEM", AND NOT AN // `std::optional`. The optional was the obvious spelling and cost // two rounds of Windows CI: see the note on `TargetEntry::sysroot` for what // that specialisation does to importers under clang with the MSVC standard @@ -121,7 +121,7 @@ struct LoadContext { // `XlingsConfig::workspaceByPlatform`, because the descriptor emitter needs // every platform at once and cannot re-derive what was already collapsed. // -// ⚠️ `macos` AND `macosx` ARE ONE PLATFORM, AND THE RULE IS NOT WRITTEN HERE. +// `macos` AND `macosx` ARE ONE PLATFORM, AND THE RULE IS NOT WRITTEN HERE. // mcpp's triple vocabulary says `macos`; a descriptor and xlings' project file // say `macosx`. `mcpp::platform::xpkg_platform_key_for` is the one place that // knows, and `xpkg_platform` is the host in the same vocabulary — an earlier @@ -243,7 +243,7 @@ inline XlingsEntry parse_address(std::string_view address) { // An entry's value, split into the part that names a version and the tier that // part belongs to. // -// ⭐ `{ version = "…", when = "…" }` IS THE SAME SHAPE A DEPENDENCY ALREADY +// `{ version = "…", when = "…" }` IS THE SAME SHAPE A DEPENDENCY ALREADY // HAS. mcpp writes `dep = "1.0"` or `dep = { version = "1.0", features = [] }`, // and a tool entry now writes `"xim:probe-rs" = "0.24.0"` or // `{ version = "0.24.0", when = "run" }`. One table, richer entries — rather @@ -278,7 +278,7 @@ split_when(const mcpp::libs::toml::Value& v) { auto itWhen = t.find("when"); if (itVer == t.end() && itWhen == t.end()) return WhenSplit{ &v, ToolWhen::Always }; // a platform table - // ⚠️ A SCOPED ENTRY MUST NAME ITS VERSION KEY EVEN TO LEAVE IT EMPTY. + // A SCOPED ENTRY MUST NAME ITS VERSION KEY EVEN TO LEAVE IT EMPTY. // `{ when = "run" }` alone reads as "present, unconstrained, run tier", // which is a meaningful thing to say — but so is a typo of `version`, and // the two would be indistinguishable. The key is required, `""` says @@ -805,7 +805,7 @@ std::expected parse_string(std::string_view content, // "unsupported" would deny a documented plan; saying nothing // is what let it look implemented. if (fkey == "deps") { - // ⚠️ THE SPELLING NAMED HERE HAS TO EXIST. The first + // THE SPELLING NAMED HERE HAS TO EXIST. The first // draft of this message offered `optional = true`, // which mcpp has never had — a diagnostic that sends // its reader to a key the parser does not know is the @@ -888,7 +888,7 @@ std::expected parse_string(std::string_view content, } // std-module / std-compat-module / std-module-flags. // - // ⚠️ THEY BELONG UNDER `[build]`, AND `[package]` IS THE OLDER SPELLING. + // THEY BELONG UNDER `[build]`, AND `[package]` IS THE OLDER SPELLING. // The module source is one of this package's translation units in every way // that matters: it is compiled with the package's include directories and // its definitions, and it is a `.cppm` file like any other. Keeping it in @@ -1694,14 +1694,14 @@ std::expected parse_string(std::string_view content, // `[feature-xlings.]` — the same table, gated on a feature of the // package that declared it. // - // ⭐ THE SPELLING IS `[feature-deps.]`'s, DELIBERATELY. A feature + // THE SPELLING IS `[feature-deps.]`'s, DELIBERATELY. A feature // that pulls in a package and a feature that pulls in a tool are the same // statement about the same feature, and inventing a second syntax for the // second would make an author learn a rule that buys nothing. The keys // differ because the things named differ: a package name there, an xim // address here. // - // ⚠️ A FEATURE NAME THAT NO `[features]` DECLARES IS REPORTED. A + // A FEATURE NAME THAT NO `[features]` DECLARES IS REPORTED. A // `[feature-xlings.hardwear]` activates for nobody and installs nothing, // and the tool it names is the one whose absence is hardest to diagnose: // the build succeeds and the device is simply never reachable. @@ -1844,7 +1844,7 @@ std::expected parse_string(std::string_view content, bool known = false; for (auto k : kKnownBuildKeys) if (key == k) { known = true; break; } if (!known) { - // ⚠️ THE LIST IN THE MESSAGE IS THE SAME LIST. It used to be a + // THE LIST IN THE MESSAGE IS THE SAME LIST. It used to be a // THIRD hand-written copy and had already drifted from both // others: it named neither `jobs` nor `bmi_schedule`, while // `kKnownBuildKeys` carried a `schedule` that nothing reads and @@ -2215,7 +2215,7 @@ std::expected parse_string(std::string_view content, *out = it->second.as_bool(); } - // ⚠️ THE EXPERIMENTAL GATE, AND THE WHOLE OF IT. + // THE EXPERIMENTAL GATE, AND THE WHOLE OF IT. // // `[hooks]` is experimental, so it may not decide whether a build // succeeded. Asking for `side_effect = true` is refused rather than @@ -2378,7 +2378,7 @@ std::expected parse_string(std::string_view content, } } - // ⭐ `[target..runners]` — the NAMED ways of reaching this + // `[target..runners]` — the NAMED ways of reaching this // target's artefact, one key per name. // // The engine knows none of these names. `flash`, `monitor`, @@ -2422,7 +2422,7 @@ std::expected parse_string(std::string_view content, // so a key that looks plausible — `cxx_runtime_tests` was the real // one — was accepted in silence and had no effect (#418). // - // ⚠️ NO SUB-TABLES, AND THAT IS THE POINT. The sub-TABLES here are the + // NO SUB-TABLES, AND THAT IS THE POINT. The sub-TABLES here are the // conditional channel (`[target..build]`, `.dependencies`, // `.dev-dependencies`, `.build-dependencies`, `.feature-deps`) and // TOML presents each as a key of this table. A hand-written list of @@ -2561,7 +2561,7 @@ std::expected parse_string(std::string_view content, for (auto k : kKnownConditionalBuildKeys) if (key == k) { known = true; break; } if (!known) { - // ⚠️ THE LIST IN THE MESSAGE IS THE SAME LIST, for the + // THE LIST IN THE MESSAGE IS THE SAME LIST, for the // reason spelled out at kKnownBuildKeys: a hand-written // second copy of a vocabulary drifts, and the drift // surfaces as a message that names the wrong set. This @@ -3213,7 +3213,7 @@ upsert_dependency_text(std::string_view source, // ── the lib root that actually exists ──────────────────────────────────── // -// ⚠️ LIVES HERE, NOT IN mcpp.manifest.types, AND THE REASON IS MEASURED. +// LIVES HERE, NOT IN mcpp.manifest.types, AND THE REASON IS MEASURED. // Probing needs the extension table (`mcpp.source_kind`), which this module // already imports and `types` does not. Adding that import to `types` — a // module nearly everything depends on — made GCC 16.1 ICE while compiling diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index 7cda3366..7726a2a8 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -227,7 +227,7 @@ struct BuildInputs { std::vector includeDirs; // relative to package root // #249: emitted as -idirafter (searched after the toolchain's system dirs) std::vector includeDirsAfter; - // ⭐⭐ WHICH OF `includeDirs` A CONSUMER MUST NOT RECEIVE. + // WHICH OF `includeDirs` A CONSUMER MUST NOT RECEIVE. // // `publicUsage` has always taken `privateBuild`'s include directories // ENTIRE, so a package is built from exactly the set it publishes. For @@ -249,7 +249,7 @@ struct BuildInputs { // was discovered one at a time: a C++ one on `restrict`, then on linkage; // a C one (compiler-rt) on `weak`, which it writes itself. // - // ⚠️ A SUBSET OF `includeDirs`, NOT A SECOND LIST, AND THE REASON IS ORDER. + // A SUBSET OF `includeDirs`, NOT A SECOND LIST, AND THE REASON IS ORDER. // The relative order of the two kinds is load-bearing: moving musl's // internal directories after the public ones makes musl's OWN build find // the public `` first and fail with `unknown type name hidden` @@ -449,7 +449,7 @@ struct Resources { // inputs plus the selection axis and resolved policy scalars. // One named way of reaching the artefact. // -// ⚠️ `longLived` IS DECLARED, NOT DERIVED FROM THE NAME. Whether the process +// `longLived` IS DECLARED, NOT DERIVED FROM THE NAME. Whether the process // ends is the one thing the engine must act on and no argv can express: // `openocd -c "program … exit"` terminates and `openocd -c "init"` does not, // spelled alike up to the argument the package chose. Deriving it from a name @@ -475,13 +475,13 @@ struct BuildConfig : BuildInputs { // board-support package emitting `mcpp:runner=` is the intended producer; // the manifest key remains the consumer's override. // - // ⚠️ EXACTLY ONE provider among the dependencies. Two board-support + // EXACTLY ONE provider among the dependencies. Two board-support // packages both claiming to know how to run the artifact is a // configuration error, not something to merge: appending would produce an // argv that is neither one's, and it would fail at exec time with no // indication of which package contributed which token. std::vector runner; - // ⭐⭐ NAMED WAYS OF REACHING THE ARTEFACT, AND THE ENGINE KNOWS NONE OF + // NAMED WAYS OF REACHING THE ARTEFACT, AND THE ENGINE KNOWS NONE OF // THEIR NAMES. // // `runner` above is how the artefact is EXECUTED. Writing it to a device, @@ -490,7 +490,7 @@ struct BuildConfig : BuildInputs { // a tool performs, with the artefact appended or substituted for `{}`. The // only thing distinguishing them is a name. // - // ⚠️ SO THE NAME IS DATA, NOT VOCABULARY. An earlier version of this gave + // SO THE NAME IS DATA, NOT VOCABULARY. An earlier version of this gave // `flash`, `monitor` and `debug` their own members, enum values, TOML keys // and subcommands — four instances of one idea, where a fifth would have // touched nine places. Worse, it put EMBEDDED vocabulary into the engine: @@ -853,11 +853,11 @@ struct RuntimeConfig { // have had since the beginning (`[dependencies]` / `[build-dependencies]` / // `[dev-dependencies]`) and tools did not. // -// ⭐ `always` IS WHAT AN ENTRY WITHOUT `when` GETS, AND IT IS TODAY'S +// `always` IS WHAT AN ENTRY WITHOUT `when` GETS, AND IT IS TODAY'S // BEHAVIOUR EXACTLY. Narrowing is an optional action, never a question an // author has to answer, so no manifest needs to change. // -// ⚠️ `dev` IS THE ONLY TIER THAT DOES NOT PROPAGATE. It means "when the package +// `dev` IS THE ONLY TIER THAT DOES NOT PROPAGATE. It means "when the package // that declared this is itself being developed", so a dependency's `dev` entry // is never installed for a consumer. The other three reach a consumer, because // a board-support package that knows which emulator runs its machine is @@ -894,7 +894,7 @@ struct XlingsConfig { // emitter reads it, because `xpm..deps` needs all three at once // and the host resolution above has already discarded two. // - // ⚠️ A VECTOR, NOT A NESTED MAP, AND NOT A STYLE CHOICE. Spelling this + // A VECTOR, NOT A NESTED MAP, AND NOT A STYLE CHOICE. Spelling this // `map>` compiles the module and then produces // a TRUNCATED BMI under GCC 16: consumers fail with // `failed to read compiled module cluster N: Bad file data` and @@ -905,7 +905,7 @@ struct XlingsConfig { // address → tier, for every address in `deps`. An entry that wrote no // `when` is absent here and reads as `Always` through `when_of` below. // - // ⚠️ BESIDE `deps` RATHER THAN INSIDE IT. `deps` is materialised into + // BESIDE `deps` RATHER THAN INSIDE IT. `deps` is materialised into // `.xlings.json` verbatim, and that file has no such axis — folding the // tier into the address would put a word xlings does not parse into a // field xlings reads. @@ -967,7 +967,7 @@ struct TargetEntry { // names the compiler the target resolves, the other names the C library, // and both were engine-only until a project had a reason to disagree. // - // ⚠️ TWO MEMBERS AND NOT AN `std::optional`, AND THE REASON IS + // TWO MEMBERS AND NOT AN `std::optional`, AND THE REASON IS // NOT STYLE. // // ABSENT and EMPTY are different answers — absent inherits the target row, @@ -991,7 +991,7 @@ struct TargetEntry { // Two plain members carry the same information and instantiate nothing. std::string sysroot; bool sysrootDeclared = false; - // ⚠️ NO per-role field here. There used to be a `cxxRuntimeTests`, and it was + // NO per-role field here. There used to be a `cxxRuntimeTests`, and it was // parsed nowhere and applied nowhere — a configuration key that looked // available and did nothing (#418). The per-target channel carries the // SCALAR contract only; `[build].cxx_runtime`'s table form already covers @@ -1087,7 +1087,7 @@ struct LibConfig { // "bundle-all" — bundle every dynamic dep including libc / libstdc++ struct PackConfig { std::string defaultMode; // empty → "bundle-project" - // ⚠️ THERE IS DELIBERATELY NO `[pack] profile`. Which profile `mcpp pack` + // THERE IS DELIBERATELY NO `[pack] profile`. Which profile `mcpp pack` // builds with is `--profile` > `[build] default-profile` > "release" — // packaging only changes the LAST step (from "dev"), because a fourth // precedence level would have to be resolved before `prepare_build` runs @@ -1194,7 +1194,7 @@ struct WorkspaceConfig { // The commands are host-shell strings written by the project author, run by // `mcpp build` around the build it performs. See docs/05-mcpp-toml.md §2.16. // -// ⚠️ ONLY THE ROOT PROJECT'S HOOKS ARE EVER RUN. Every manifest mcpp parses +// ONLY THE ROOT PROJECT'S HOOKS ARE EVER RUN. Every manifest mcpp parses // carries this field, including a DEPENDENCY's — and `mcpp build` reaches the // invoker (mcpp.hooks) with the root project's manifest alone. A dependency // that declares hooks is inert by construction, which is the only reason @@ -1242,7 +1242,7 @@ struct Hooks { int timeoutSeconds = 10; // default for one run of a hook command bool enabled = true; // whole table - // ⚠️ EXPERIMENTAL: FALSE, AND CURRENTLY THE ONLY VALUE. + // EXPERIMENTAL: FALSE, AND CURRENTLY THE ONLY VALUE. // // The key means "a hook failure fails the build". While `[hooks]` is // experimental it does not get to decide that: a hook that fails is @@ -1299,7 +1299,7 @@ struct Manifest { // schema evolution is loud in lint instead of invisible. std::vector xpkgUnknownKeys; - // ⚠️ CAPABILITY NAMES INSIDE THE RESERVED `mcpp:` PREFIX THAT THIS ENGINE + // CAPABILITY NAMES INSIDE THE RESERVED `mcpp:` PREFIX THAT THIS ENGINE // DOES NOT KNOW, AND WHY THEY ARE RECORDED RATHER THAN REFUSED HERE. // // The reserved prefix is a closed set so that a misspelled layer name is an @@ -1363,9 +1363,9 @@ struct Manifest { // by naming both, which a hardcoded table could not do for a family it had // never heard of. // - // ⚠️ Names outside the `mcpp:` prefix are the feature system's and pass + // Names outside the `mcpp:` prefix are the feature system's and pass // through untouched, exactly as they do in `provides`. - // ⚠️ The spelling is `requires_` because `requires` is a keyword. + // The spelling is `requires_` because `requires` is a keyword. std::vector requires_; // [package] exclusive — the capabilities this package claims it is the ONLY // provider of. @@ -1394,7 +1394,7 @@ struct Manifest { // both — instead of at link time naming a symbol, or at run time naming // nothing at all. // - // ⚠️ An entry that is not also in `provides` (or a feature's `provides`) is + // An entry that is not also in `provides` (or a feature's `provides`) is // a typo and is reported: claiming exclusivity over something you do not // provide cannot be acted on. std::vector exclusive; @@ -1574,7 +1574,7 @@ std::optional validate_target_soname(const Target& t, // the name down while still being consumed as a static library — which is // the normal case (mcpp-index: 84 `kind = "lib"` against 12 `"shared"`). // - // ⚠️ RELAXED RATHER THAN MOVED: the old spelling made the whole manifest + // RELAXED RATHER THAN MOVED: the old spelling made the whole manifest // FAIL TO LOAD, in both parsers. Any descriptor that starts writing this // key is therefore unreadable by every mcpp released before this change, // so the ecosystem-side rollout is gated on the index's floor moving — diff --git a/modules/manifest/src/xpkg.cppm b/modules/manifest/src/xpkg.cppm index c61a1573..5b77b1da 100644 --- a/modules/manifest/src/xpkg.cppm +++ b/modules/manifest/src/xpkg.cppm @@ -109,9 +109,9 @@ bool xpkg_lua_identity_matches(std::string_view luaContent, // INV-NAME (SPEC-001 §3.2) — `package.name` is a SINGLE ATOMIC SEGMENT. All // hierarchy belongs in `package.namespace`, which is the dotted path. // -// ✅ namespace = "chriskohlhoff", name = "asio" -// ✅ namespace = "mcpplibs.capi", name = "lua" -// ❌ namespace = "mcpplibs", name = "capi.lua" (short name has a dot) +// accepted namespace = "chriskohlhoff", name = "asio" +// accepted namespace = "mcpplibs.capi", name = "lua" +// refused namespace = "mcpplibs", name = "capi.lua" (short name has a dot) // // Why it matters: identity is the pair (namespace, name), and xlings addresses // packages as `:` with the LITERAL name. A @@ -240,7 +240,7 @@ inline constexpr std::string_view kKnownXpkgKeys[] = { inline constexpr std::pair kXpkgKeyAliases[] = { { "dependencies", "deps" }, { "dependency", "deps" }, - // ⚠️ `requires` USED TO REDIRECT HERE and no longer does: it is a key in its + // `requires` USED TO REDIRECT HERE and no longer does: it is a key in its // own right now, the symmetric half of `provides`. A redirect for a key that // exists is unreachable — `closest_known_xpkg_key` is consulted only for // UNKNOWN keys — and leaving it would be a claim this file no longer makes. diff --git a/modules/platform/src/platform.cppm b/modules/platform/src/platform.cppm index 1d9f32d3..168d210b 100644 --- a/modules/platform/src/platform.cppm +++ b/modules/platform/src/platform.cppm @@ -43,7 +43,7 @@ // modules (`fs`, `env`, `scaffold_fs`); migrating those is incremental // work, not a precondition for adding new code the right way. // -// ⚠️ A module under a platform directory MUST NOT name a `std` type in its +// A module under a platform directory MUST NOT name a `std` type in its // EXPORTED interface if a widely-imported module will import it: under GCC // 16.1 that corrupts every BMI downstream, and the error points at an // unrelated module. Both bounded_process modules document the measurements. diff --git a/modules/platform/src/process.cppm b/modules/platform/src/process.cppm index 18ca8c0a..ce2145ff 100644 --- a/modules/platform/src/process.cppm +++ b/modules/platform/src/process.cppm @@ -83,7 +83,7 @@ RunResult capture_with_env( // (LD_LIBRARY_PATH) cannot poison mcpp itself or any sibling host process. // Returns a platform-normalized exit code, or 127 if exec fails. // -// ⚠️ A SPAWN FAILURE IS REPORTED EXACTLY ONCE AND NEVER DROPPED (#544). Every +// A SPAWN FAILURE IS REPORTED EXACTLY ONCE AND NEVER DROPPED (#544). Every // launcher below follows one rule: when the child could not be started it // returns 127 and either (a) stores the errno in `*spawn_error` and prints // nothing, because a caller that asked for the errno owns the report, or @@ -244,7 +244,7 @@ std::string windows_wrap_for_cmd_c(std::string_view cmd); // The command line that runs a USER-AUTHORED shell command through cmd.exe. // -// ⚠️ NOT `windows_command_from_argv({"cmd.exe", "/d", "/s", "/c", command})`. +// NOT `windows_command_from_argv({"cmd.exe", "/d", "/s", "/c", command})`. // That shape is for a program plus its argv, where CreateProcess's parsing is // what has to be satisfied. cmd.exe is not parsed that way: its switches must // arrive BARE (quoted, they are no longer switches), and the command tail is diff --git a/modules/platform/src/runtime_search.cppm b/modules/platform/src/runtime_search.cppm index b02c8e7d..2ae81cbc 100644 --- a/modules/platform/src/runtime_search.cppm +++ b/modules/platform/src/runtime_search.cppm @@ -54,7 +54,7 @@ enum class Origin { // The artifact's own directory — what the linker writes as `$ORIGIN` on // ELF and `@loader_path` on Mach-O. // - // ⚠️ IT WAS MISSING, AND THAT MADE THIS ORDERING DECORATIVE. The module + // IT WAS MISSING, AND THAT MADE THIS ORDERING DECORATIVE. The module // claims below to be the one place the search order is decided, but // `$ORIGIN` was emitted by `shared_library_link_flags` on a completely // separate per-unit channel and never entered the closure — so the ONE diff --git a/modules/platform/src/unix/bounded_process.cppm b/modules/platform/src/unix/bounded_process.cppm index 8c1c000b..bb017f83 100644 --- a/modules/platform/src/unix/bounded_process.cppm +++ b/modules/platform/src/unix/bounded_process.cppm @@ -11,7 +11,7 @@ // `mcpp.platform.process` dispatch once, with `if constexpr`, instead of // carrying the platform question through twenty-five separate `#if` blocks. // -// ⚠️ THE INTERFACE NAMES NO `std` TYPE — SAME HARD CONSTRAINT AS THE WINDOWS +// THE INTERFACE NAMES NO `std` TYPE — SAME HARD CONSTRAINT AS THE WINDOWS // SIDE. See mcpp.platform.windows.bounded_process for the measurements: a new // module imported by mcpp.platform.process whose EXPORTS mention std types // corrupts every BMI downstream of it under GCC 16.1. `std` inside the module @@ -103,7 +103,7 @@ DeadlineRun capture_with_deadline(const char* const* argvEntries, // caller needs a handle it can poll and stop later. Every member is a builtin, // same constraint as DeadlineRun. // -// ⚠️ `group`, NOT a pid. The child is placed in a process group of its own +// `group`, NOT a pid. The child is placed in a process group of its own // (posix_spawnattr_setpgroup) and stopped with killpg, because the thing being // started is a user-authored SHELL command: `sh -c 'player & wait'` makes the // writer a grandchild, and `kill(pid)` reaches only the shell. A background @@ -376,7 +376,7 @@ BackgroundChild spawn_background(const char* const* argvEntries, return out; } -// ⚠️ DOES NOT REAP, and that is the whole point. +// DOES NOT REAP, and that is the whole point. // // A zombie still holds its pid, so an unreaped leader is what keeps the GROUP // id from being recycled — and `background_stop` signals that group. Reaping diff --git a/modules/platform/src/windows/bounded_process.cppm b/modules/platform/src/windows/bounded_process.cppm index 7f525f5a..5d832105 100644 --- a/modules/platform/src/windows/bounded_process.cppm +++ b/modules/platform/src/windows/bounded_process.cppm @@ -19,7 +19,7 @@ // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE takes the whole tree down at once, which // closes every inherited handle and lets the drain finish. // -// ⚠️ WHY THE INTERFACE HAS NO `std` TYPES IN IT +// WHY THE INTERFACE HAS NO `std` TYPES IN IT // // This is a hard constraint, not a style preference. Measured on GCC 16.1.0 // while adding this module: a NEW module that (a) is imported by @@ -503,7 +503,7 @@ void background_stop(unsigned long long job, unsigned long long process, static_cast(job)) : nullptr; - // ⚠️ NO POLITE ASK HERE, AND `graceMs` IS DELIBERATELY UNSPENT. + // NO POLITE ASK HERE, AND `graceMs` IS DELIBERATELY UNSPENT. // // The obvious "ask first" is // diff --git a/modules/source-kind/src/source_kind.cppm b/modules/source-kind/src/source_kind.cppm index 27bd629b..085b5691 100644 --- a/modules/source-kind/src/source_kind.cppm +++ b/modules/source-kind/src/source_kind.cppm @@ -231,7 +231,34 @@ constexpr std::string_view kHeaderExtensions[] = { ".h", ".hpp", ".hh", ".hxx" } // Device sources and the headers they include. The headers are classified as // `Header` rather than `Device` because their role is the header role: they // are not compiled, and editing one can change what the graph should be. -constexpr std::string_view kDeviceExtensions[] = { ".cu", ".hip" }; +// +// THE CRITERION IS THE COMPILER, NOT THE VENDOR. `SourceKind::Device` says a +// file is compiled by a device compiler mcpp does not drive, and its comment +// says so in order to avoid "the classification table growing a row per +// vendor". This list nonetheless held exactly two rows, both NVIDIA's, and the +// gap that revealed is not hypothetical: a shader listed in a constrained glob +// was refused with "mcpp has no role for the extension '.comp'" — the file +// never reached `MCPP_DEVICE_SOURCES`, so the rule package that exists to +// compile it was told there was nothing to compile, and warned about it. +// +// So the list is the set of languages a separate compiler consumes: CUDA and +// HIP, the GLSL stages and HLSL (glslang, dxc), OpenCL C (clang, and every +// vendor's own), and Metal Shading Language. Adding to it cannot change a +// build that works today, for two reasons that hold jointly: device extensions +// are deliberately absent from `default_source_globs`, so no glob widens; and +// a file with one of these extensions named in `sources` is TODAY a hard +// error, so nothing silently switches role. +// +// `.glsl` is here and carries no stage. glslang derives the stage from the +// extension, so a rule package refuses a stage-less name — which is the right +// place for that message, and the reason this table does not need to know +// which of these extensions name a stage. +constexpr std::string_view kDeviceExtensions[] = { + ".cu", ".hip", + ".comp", ".vert", ".frag", ".geom", ".tesc", ".tese", ".mesh", ".task", + ".rgen", ".rint", ".rahit", ".rchit", ".rmiss", ".rcall", + ".glsl", ".hlsl", ".cl", ".metal", +}; constexpr std::string_view kDeviceHeaderExtensions[] = { ".cuh", ".hiph" }; bool contains(std::span set, std::string_view ext) { diff --git a/modules/toolchain-model/src/dialect.cppm b/modules/toolchain-model/src/dialect.cppm index e7b5167b..b721d388 100644 --- a/modules/toolchain-model/src/dialect.cppm +++ b/modules/toolchain-model/src/dialect.cppm @@ -107,7 +107,7 @@ struct CommandDialect { std::string_view archiveRemoveArg; // "d" needs no {} | "/REMOVE:{}" bool archiveRemoveTakesArchiveFirst = true; - // ⚠️ THE IMPORT-LIBRARY AND `.def` SPELLINGS ARE NOT HERE, on purpose. + // THE IMPORT-LIBRARY AND `.def` SPELLINGS ARE NOT HERE, on purpose. // // They were, keyed on the dialect, and that is wrong in a way Windows CI // demonstrated: clang targeting the MSVC ABI speaks the GNU DIALECT while @@ -122,7 +122,7 @@ const CommandDialect& dialect_for(const Toolchain& tc); // The MSVC CRT model for a given linkage, in ONE place. // -// ⚠️ cl bakes `_MSVC_MT` / `_MSVC_MD` into every module it produces, so the std +// cl bakes `_MSVC_MT` / `_MSVC_MD` into every module it produces, so the std // module and the TUs importing it must agree. They were derived in two places: // the project's TUs from `flags.cppm` and the std module from cl's own default // (`/MT`, because the std build passed no flag at all). A project on the default diff --git a/modules/toolchain-model/src/linkmodel.cppm b/modules/toolchain-model/src/linkmodel.cppm index d3eb7f29..f625c9d1 100644 --- a/modules/toolchain-model/src/linkmodel.cppm +++ b/modules/toolchain-model/src/linkmodel.cppm @@ -306,7 +306,7 @@ ClangDriverModel resolve_clang_driver(const Toolchain& tc) { auto libcxxInclude = dm.llvmRoot / "include" / "c++" / "v1"; dm.cxxIncludes.push_back(libcxxInclude); - // ⭐⭐ THE PAYLOAD'S DIRECTORIES ARE NAMED IN LLVM'S VOCABULARY, AND THIS + // THE PAYLOAD'S DIRECTORIES ARE NAMED IN LLVM'S VOCABULARY, AND THIS // LOOKUP USED mcpp'S. // // `include//c++/v1` and `lib/` are written by the LLVM @@ -316,7 +316,7 @@ ClangDriverModel resolve_clang_driver(const Toolchain& tc) { // they come apart the moment a target is named: prepare rewrites // `targetTriple` to mcpp's canonical spelling for a retargetable driver. // - // ⚠️ AND THE MISS IS SILENT. Both lookups are `if (exists) push_back`, so + // AND THE MISS IS SILENT. Both lookups are `if (exists) push_back`, so // a directory that is not found simply does not appear. What is in the one // that goes missing is a single file — `__config_site` — which is why the // failure this produces reads `'__config_site' file not found` from inside @@ -325,10 +325,10 @@ ClangDriverModel resolve_clang_driver(const Toolchain& tc) { // // Measured 2026-08-27, same machine, same compiler, same target: // - // mcpp build include/x86_64-unknown-linux-gnu/c++/v1 ✔ + // mcpp build include/x86_64-unknown-linux-gnu/c++/v1 // mcpp build --target x86_64-linux-gnu (absent) // - // ⭐ BOTH SPELLINGS ARE TRIED, and that is not a heuristic: they are two + // BOTH SPELLINGS ARE TRIED, and that is not a heuristic: they are two // vocabularies for one fact, and which one a given payload used is a // property of how it was built rather than of anything mcpp decides. The // LLVM spelling is tried first because it is the one an LLVM payload diff --git a/modules/toolchain-model/src/model.cppm b/modules/toolchain-model/src/model.cppm index 99f59248..a583c560 100644 --- a/modules/toolchain-model/src/model.cppm +++ b/modules/toolchain-model/src/model.cppm @@ -93,7 +93,7 @@ struct Toolchain { // They reach the cache key without anything further being done: the key is // derived from the build COMMANDS, and these are part of them. std::string stdModuleFlags; - // ⭐⭐ THE PART OF THE ABOVE THAT SAYS WHICH MACHINE, SEPARATED FROM THE + // THE PART OF THE ABOVE THAT SAYS WHICH MACHINE, SEPARATED FROM THE // PART THAT SAYS WHERE THE HEADERS ARE. // // `stdModuleFlags` is one string carrying two different facts: the target @@ -102,7 +102,7 @@ struct Toolchain { // the second compiles a BMI, which already contains everything the headers // contributed. // - // ⚠️ Passing the whole string to the second step is not wrong, it is noisy, + // Passing the whole string to the second step is not wrong, it is noisy, // and the noise is the kind that hides things: // // clang++: warning: argument unused during compilation: '-nostdinc++' @@ -110,7 +110,7 @@ struct Toolchain { // (× 17, once per include directory) // // Seventeen warnings that are correct and mean nothing, in front of any - // warning that would mean something. ⚠️ They were present on every platform + // warning that would mean something. They were present on every platform // and visible on none: the non-Windows command ends in `2>&1` and mcpp // discards a successful command's output, so the Windows leg — which has no // redirection — is where they first appeared. @@ -121,11 +121,11 @@ struct Toolchain { // throw, and wrong when something can. bool targetCxxRuntime = false; - // ⭐⭐ DOES THE TARGET'S C LIBRARY COME FROM A DIRECTORY THAT EXISTED + // DOES THE TARGET'S C LIBRARY COME FROM A DIRECTORY THAT EXISTED // BEFORE DEPENDENCY RESOLUTION? — `TargetSide::cAbi.prebuilt()`, recorded // here so the three producers of a compile line read one value. // - // ⚠️ THIS IS NOT `targetCxxRuntime` AND THE DIFFERENCE IS THE ONE + // THIS IS NOT `targetCxxRuntime` AND THE DIFFERENCE IS THE ONE // 2026.8.25.1 WAS ABOUT. That field says a package supplies a C++ RUNTIME; // this one says where the C LIBRARY comes from. A pure C program over // openkal has no C++ runtime and its C library still comes from the graph, @@ -133,7 +133,7 @@ struct Toolchain { // interface from the graph while its C library stays the payload's. The // two come apart in both directions. // - // ⚠️ RECORDED, NOT DERIVED. `mcpp.targetside` answers it once, after the + // RECORDED, NOT DERIVED. `mcpp.targetside` answers it once, after the // dependency graph exists; every consumer reads this. The predicate it // replaced on the compile side was `!crossTargetFlag.empty()` — "is there a // `--target=` on the command line" — which is true for a project that names @@ -144,14 +144,14 @@ struct Toolchain { // every host-targeting toolchain, means exactly that. bool cAbiPrebuilt = true; - // ⭐⭐ THE `--target=` A RETARGETABLE DRIVER HAS TO BE GIVEN, OR EMPTY. + // THE `--target=` A RETARGETABLE DRIVER HAS TO BE GIVEN, OR EMPTY. // // Non-empty only when the user asked for a cross AND the resolved compiler // is one binary that emits many targets (clang). For a native build, and // for a cross served by a driver that has exactly one target of its own // (`x86_64-w64-mingw32-g++`), this stays empty and nothing is added. // - // ⚠️ IT CANNOT BE DERIVED FROM `targetTriple` BEING NON-EMPTY. A native + // IT CANNOT BE DERIVED FROM `targetTriple` BEING NON-EMPTY. A native // build has a `targetTriple` too — the probed one — so a consumer that // tested for non-empty would add `--target=` to every compile in // every project. Measured: it does, and what it produces is not a @@ -234,7 +234,7 @@ struct Toolchain { } } - // ⚠️ THE FAMILY NAME, WHICH IS NOT THE DRIVER'S NAME, AND THEY DIFFER FOR + // THE FAMILY NAME, WHICH IS NOT THE DRIVER'S NAME, AND THEY DIFFER FOR // EXACTLY ONE FAMILY — THE COMMON ONE. // // The driver is `clang`; the family is `llvm`. Everything a user or a @@ -260,7 +260,7 @@ bool is_musl_target(const Toolchain& tc); bool is_msvc_target(const Toolchain& tc); bool is_mingw_target(const Toolchain& tc); -// ⭐⭐ THE FLAGS A WHOLE GRAPH HAS TO AGREE ON WHEN THE RUNTIME COMES FROM IT. +// THE FLAGS A WHOLE GRAPH HAS TO AGREE ON WHEN THE RUNTIME COMES FROM IT. // // An ordinary flag is a package's business. These two are not: they change what // a translation unit EMITS for constructs the language guarantees work across a @@ -291,7 +291,7 @@ bool is_mingw_target(const Toolchain& tc); // and it is machinery a self-contained image has no use // for. // -// ⚠️⚠️ AND THE THIRD ONE WAS FOUND BY A PROGRAM THAT LINKED, WAS SIGNED, AND +// AND THE THIRD ONE WAS FOUND BY A PROGRAM THAT LINKED, WAS SIGNED, AND // CRASHED ON THE REAL MACHINE — which is the whole argument for running the // artefact rather than inspecting it. On an arm64 Mac: // @@ -314,13 +314,13 @@ bool is_mingw_target(const Toolchain& tc); // 0x29b8 → 0x58. Nine stubs and eleven slots, which is the size a program with // three imports should have. // -// ⚠️ ELF IS DELIBERATELY ABSENT FROM THE SECOND, and it is not an oversight: +// ELF IS DELIBERATELY ABSENT FROM THE SECOND, and it is not an oversight: // there a `thread_local` is a fixed offset from the thread pointer, which the C // library establishes itself. Adding the flag would work and cost an indirection // on every access — but it would also make ELF the only target whose thread // locals are laid out differently from every OTHER build of the same target. // -// ⚠️ AND THE REASON THIS IS A FUNCTION RATHER THAN TWO `if`s: the compile +// AND THE REASON THIS IS A FUNCTION RATHER THAN TWO `if`s: the compile // command is assembled in two places (`hostflags.cppm` for every ordinary unit, // `prepare.cppm` for the `std` module), and a `std.pcm` built with SEH imported // by units built with DWARF is a defect that neither file can see. "One fact, @@ -450,7 +450,7 @@ std::vector graph_runtime_compile_flags(const Toolchain& tc) { if (!tc.targetCxxRuntime) return out; auto t = triple::parse(tc.targetTriple); if (!t) return out; - // ⚠️⚠️ AND aarch64's RUNTIME LIBRARY CHOICE, WHICH IS A CODEGEN FACT THERE. + // AND aarch64's RUNTIME LIBRARY CHOICE, WHICH IS A CODEGEN FACT THERE. // // On aarch64 `--rtlib=compiler-rt` is not a link-time preference: it moves // the target-feature set. Measured by `openkal-llvm-runtime`, whose own @@ -469,17 +469,17 @@ std::vector graph_runtime_compile_flags(const Toolchain& tc) { // error: current translation unit is compiled with the target feature // '-fmv' but the precompiled file 'std.pcm' was not // - // ⭐ SAME DEFECT AS `-fdwarf-exceptions`, ONE FLAG LATER — see the note in + // SAME DEFECT AS `-fdwarf-exceptions`, ONE FLAG LATER — see the note in // hostflags.cppm, which describes that one in these words: "its objects // agreed with each other and nothing else did". A property of the graph // cannot be declared by one package for one command. // - // ⚠️ x86_64 HAS NO SUCH FEATURE, so both sides listed nothing there and the + // x86_64 HAS NO SUCH FEATURE, so both sides listed nothing there and the // defect was invisible until a second architecture was built. if (t->arch == "aarch64") out.emplace_back("--rtlib=compiler-rt"); if (t->is_pe()) out.emplace_back("-fdwarf-exceptions"); if (t->is_pe() || t->os == "macos") out.emplace_back("-femulated-tls"); - // ⭐⭐ MACH-O ONLY, AND THE REASON IS THAT WEAK-DEF IS A RUN-TIME MECHANISM + // MACH-O ONLY, AND THE REASON IS THAT WEAK-DEF IS A RUN-TIME MECHANISM // THERE. See the note on this function for the measurement. if (t->os == "macos") { out.emplace_back("-fvisibility=hidden"); diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index ca0e8f0f..cf098f54 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -34,7 +34,7 @@ struct Triple { std::string os; // "linux" | "macos" | "windows" std::string env; // "gnu" | "musl" | "msvc" | "" (always empty on macos) - // ⚠️ WHETHER THE ENV SEGMENT WAS WRITTEN, AS OPPOSED TO SUPPLIED BY THIS + // WHETHER THE ENV SEGMENT WAS WRITTEN, AS OPPOSED TO SUPPLIED BY THIS // PARSER — AND THE TRIPLE HAS TO CARRY BOTH BECAUSE IT SERVES TWO ROLES. // // A triple is an IDENTITY — the output directory's name, part of a cache @@ -70,13 +70,13 @@ struct Triple { return s; } - // ⭐⭐ THE SPELLING A COMPILER TAKES, WHICH IS NOT THE SPELLING mcpp USES. + // THE SPELLING A COMPILER TAKES, WHICH IS NOT THE SPELLING mcpp USES. // // `str()` is mcpp's vocabulary: short, unambiguous, and the thing a user // types. LLVM's is a four-field form with a vendor, and on Apple platforms // the architecture has a different name and the OS carries a version. // - // ⚠️ THIS EXISTS BECAUSE CROSS-COMPILING USED TO MEAN SOMETHING NARROWER. + // THIS EXISTS BECAUSE CROSS-COMPILING USED TO MEAN SOMETHING NARROWER. // Every hosted cross mcpp could do was served by a payload whose DRIVER was // already specialised — `x86_64-w64-mingw32-g++` needs no `--target`, // because it has only one. So nothing ever needed this function, and @@ -89,7 +89,7 @@ struct Triple { // emits every format it was built with from one binary. There is no payload // to specialise, so the triple has to be said out loud. // - // ⚠️ Measured 2026-08-23, before this existed: a build for `aarch64-macos` + // Measured 2026-08-23, before this existed: a build for `aarch64-macos` // resolved the whole graph, took musl's aarch64 headers, and compiled with // NO `--target` at all — so the host's x86_64 code generation met aarch64 // declarations. What caught it was the port's own assertion, which exists @@ -131,7 +131,7 @@ struct Triple { // re-derives it from `os == "none"` and drifts. bool is_freestanding() const { return os == "none"; } - // ⭐⭐ WHETHER THIS ROW'S TOOLCHAIN PIN IS A CAPABILITY RATHER THAN A + // WHETHER THIS ROW'S TOOLCHAIN PIN IS A CAPABILITY RATHER THAN A // CONVENTION — the distinction that decides whether an author may override // it. // @@ -146,7 +146,7 @@ struct Triple { // mingw payload emits PE with the MinGW CRT, which is the // separate `-gnu` row; there is no third gcc. // - // ⚠️ SPELLED HERE RATHER THAN AT EACH DECISION, because the first version + // SPELLED HERE RATHER THAN AT EACH DECISION, because the first version // said `is_freestanding()` at two of them and `x86_64-windows-musl` — a row // added later — was a convention at both. Measured: declaring gcc for it // resolved the host's Linux musl payload and reported a missing C++ @@ -174,7 +174,7 @@ struct Triple { return std::nullopt; } - // ⚠️ IDENTITY IS THE THREE SEGMENTS, AND `envExplicit` IS DELIBERATELY NOT + // IDENTITY IS THE THREE SEGMENTS, AND `envExplicit` IS DELIBERATELY NOT // AMONG THEM — WHICH IS WHY THIS IS NOT `= default`. // // The flag records where the env segment came from, not what the target is. @@ -210,7 +210,7 @@ struct TargetInfo { // The TARGET's C library, resolved at compile time exactly the way `pin` // resolves the compiler. Empty = none applies. // - // ⚠️ This axis exists because bare metal was the one target class without + // This axis exists because bare metal was the one target class without // it, and the gap leaked into every package. A hosted target gets its libc // automatically — `x86_64-linux-musl` carries musl inside its gcc payload, // and glibc arrives through PayloadPaths — so nobody writes `xim:glibc` in @@ -232,7 +232,7 @@ inline constexpr TargetInfo kKnownTargets[] = { { "x86_64-linux-musl", "verified", "", "gcc@16.1.0", "", true }, { "aarch64-linux-musl", "verified", "", "gcc@16.1.0", "", true }, { "x86_64-windows-gnu", "verified", "PE", "gcc@16.1.0", "", true }, - // ⚠️ musl ON WINDOWS. IT EXISTS, AND UNTIL THIS ROW mcpp HAD NO NAME FOR IT. + // musl ON WINDOWS. IT EXISTS, AND UNTIL THIS ROW mcpp HAD NO NAME FOR IT. // // LLVM's triple vocabulary offers `gnu` and `msvc` for Windows and both // name an ABI, so a reader concludes there is no third possibility and @@ -246,7 +246,7 @@ inline constexpr TargetInfo kKnownTargets[] = { // `gnu` put the one thing the C library is not into its identity, its // output directory, its `cfg(env = …)` and its packed ABI tag. // - // ⭐ THE FIX IS A NAME, NOT A MECHANISM, AND THE REASON THE MISTAKE HELD SO + // THE FIX IS A NAME, NOT A MECHANISM, AND THE REASON THE MISTAKE HELD SO // LONG IS WORTH RECORDING. "LLVM cannot spell x86_64-windows-musl" is true // and is about the string handed to CLANG. mcpp's canonical form is a // different string — the build report prints both, either side of an arrow: @@ -269,7 +269,7 @@ inline constexpr TargetInfo kKnownTargets[] = { // The last two rows differ in the first column and agree in the second, // which is the whole point. // - // ⚠️ THE PIN IS `llvm`, AND IT IS NOT A PREFERENCE. The column names the + // THE PIN IS `llvm`, AND IT IS NOT A PREFERENCE. The column names the // payload that supplies this target's C library everywhere else in this // table; here nothing supplies it, and what the column has to prevent is // the OPPOSITE — a global default of gcc being carried onto a target no gcc @@ -288,7 +288,7 @@ inline constexpr TargetInfo kKnownTargets[] = { // is known (see the long note at prepare.cppm's `unservedTargetDiagnosis`), // so a project whose C library comes from a dependency is not turned away. // - // ⚠️ TIER IS `preview`, NOT `verified`. `verified` in this table means an + // TIER IS `preview`, NOT `verified`. `verified` in this table means an // artefact was built AND RUN. Running a PE on a Linux host needs wine, and // openkal's CI has that step — so this is measurable, and the tier moves // when it has been measured rather than when it seems likely. @@ -308,7 +308,7 @@ inline constexpr TargetInfo kKnownTargets[] = { // name a libc: the C library is the target's, like the compiler. { "riscv64-none-elf", "verified", "bare","llvm@22.1.8","xim:picolibc-riscv@1.8.12", true }, { "riscv32-none-elf", "verified", "bare","llvm@22.1.8","xim:picolibc-riscv@1.8.12", true }, - // ⚠️ AN EMPTY SYSROOT COLUMN, AND IT IS A STATEMENT RATHER THAN AN OMISSION. + // AN EMPTY SYSROOT COLUMN, AND IT IS A STATEMENT RATHER THAN AN OMISSION. // // The two rows above name a C library because a project targeting them // ordinarily wants one. This row does not, because there is no aarch64 @@ -324,13 +324,13 @@ inline constexpr TargetInfo kKnownTargets[] = { // resolve. A project that wants a C library on this target says so in its // own manifest, which is also how it would choose a different one. // - // ⚠️ The tier is `preview` and not `verified`: `verified` in this table + // The tier is `preview` and not `verified`: `verified` in this table // means an image has been built AND RUN for the row, and running one needs // an emulator. `xim:qemu-arm` provides `qemu-system-aarch64`; until a probe // has actually booted under it, claiming `verified` would be claiming the // measurement rather than reporting it. { "aarch64-none-elf", "preview", "bare","llvm@22.1.8","", true }, - // ⚠️ THIS ROW EXISTS SO THAT A THIRD MACHINE CAN DISAGREE WITH THE FIRST + // THIS ROW EXISTS SO THAT A THIRD MACHINE CAN DISAGREE WITH THE FIRST // TWO, WHICH IS THE ONLY THING THAT TELLS AN ABSTRACTION FROM A HABIT. // // riscv64 and aarch64 are both load/store RISC machines with a weak memory @@ -341,7 +341,7 @@ inline constexpr TargetInfo kKnownTargets[] = { // an interrupt mechanism that is a table of gates rather than a base // register. What survives all three is an abstraction. // - // ⚠️ The tier is `preview` for the same reason aarch64's is, and the reason + // The tier is `preview` for the same reason aarch64's is, and the reason // is stricter than it sounds: `verified` here means an image was built AND // RUN. `xim:qemu-x86` does not exist yet — the index carries no // `qemu-system-x86_64` — so nothing on this row has booted. Claiming @@ -353,7 +353,7 @@ inline constexpr TargetInfo kKnownTargets[] = { { "x86_64-none-elf", "preview", "bare","llvm@22.1.8","", true }, // ── Cortex-M ──────────────────────────────────────────────────────────── // - // ⚠️ SEVEN ROWS AND NOT ONE, BECAUSE "Cortex-M" IS NOT AN INSTRUCTION SET. + // SEVEN ROWS AND NOT ONE, BECAUSE "Cortex-M" IS NOT AN INSTRUCTION SET. // // Every other bare-metal family here is one row per architecture. M-profile // is not: an object built for `thumbv7em` uses instructions a Cortex-M0 @@ -366,13 +366,13 @@ inline constexpr TargetInfo kKnownTargets[] = { // single `arm-none-eabi` row plus an `-mcpu` the project remembers would // move a correctness decision out of the table and into every manifest. // - // ⭐ THE `eabi`/`eabihf` SUFFIX IS THE FLOAT ABI, AND CLANG ALREADY READS + // THE `eabi`/`eabihf` SUFFIX IS THE FLOAT ABI, AND CLANG ALREADY READS // IT. Measured on llvm 22.1.8 (`-###`, `-cc1` line): `thumbv7em-none-eabi` // gives `-mfloat-abi soft` and `-none-eabihf` gives `hard`, with no flag // from us. So the ABI needs no entry in the ISA table's `extra` column — // only the FPU does, and only on the soft rows. See `kThumbSoftExtra`. // - // ⚠️ `sysroot` IS EMPTY ON EVERY ROW, AND THAT IS THE POINT RATHER THAN A + // `sysroot` IS EMPTY ON EVERY ROW, AND THAT IS THE POINT RATHER THAN A // GAP. The three older bare-metal families name an `xim:` payload here; a C // library for these targets arrives from the DEPENDENCY GRAPH instead // (`mcpp:c-abi=picolibc`, docs/14). A prebuilt payload would have to ship @@ -381,7 +381,7 @@ inline constexpr TargetInfo kKnownTargets[] = { // package is compiled with the consuming target's own flags, so the ABI // agreement holds by construction and there is no multilib at all. // - // ⚠️ THE TIER COLUMN RECORDS WHAT WAS RUN, NOT WHAT WAS REASONED. Measured + // THE TIER COLUMN RECORDS WHAT WAS RUN, NOT WHAT WAS REASONED. Measured // 2026-09-04 under `xim:qemu-arm@9.2.4-1`: each `verified` row below built // an image that BOOTED on the named machine and printed over semihosting — // thumbv6m on `microbit`, thumbv7m on `mps2-an385`, thumbv7em-eabihf on @@ -396,13 +396,13 @@ inline constexpr TargetInfo kKnownTargets[] = { { "thumbv8m.main-none-eabihf","preview","bare","llvm@22.1.8","", true }, // ── ARMv7-A (Cortex-A, 32-bit) ────────────────────────────────────────── // - // ⭐ NOT A SECOND SPELLING OF THE M ROWS. A-profile has a memory management + // NOT A SECOND SPELLING OF THE M ROWS. A-profile has a memory management // unit and a page-table walker; M-profile has an MPU and no page-table // entry at all. It is the first 32-bit machine in this table on which an // address space can be described, which is precisely the question the // openarch layer has never been able to ask of a 32-bit target. // - // ⚠️ `verified` records what was RUN. Measured 2026-09-04 under + // `verified` records what was RUN. Measured 2026-09-04 under // `xim:qemu-arm@9.2.4-1`: both rows booted on `-M virt -cpu cortex-a15` and // printed over semihosting. The soft row carries `-mfpu=none` for the // reason `kThumbSoftExtra` gives, measured again on this architecture @@ -427,7 +427,7 @@ inline bool is_known_target(const Triple& t) { return find_known_target(t) != nu // ── Completing a request that declined to name a C library ────────────────── // -// ⚠️⚠️ `parse` FILLS THE ENV SEGMENT LEXICALLY, AND THE TIER GATE USED TO ASK +// `parse` FILLS THE ENV SEGMENT LEXICALLY, AND THE TIER GATE USED TO ASK // ABOUT THE FILLED VALUE RATHER THAN ABOUT THE REQUEST. // // The fill is an IDENTITY operation and has to stay exactly as it is: total, @@ -453,7 +453,7 @@ inline bool is_known_target(const Triple& t) { return find_known_target(t) != nu // — compile-time data, therefore the same on every host, so target identity // still does not depend on where the build ran. // -// ⭐ RULE ONE MAKES THIS RETIRE ITSELF. When `aarch64-linux-gnu` graduates from +// RULE ONE MAKES THIS RETIRE ITSELF. When `aarch64-linux-gnu` graduates from // `planned`, rule one matches first and the completion goes back to the lexical // answer with nobody editing this function. struct RequestResolution { @@ -500,7 +500,7 @@ inline RequestResolution resolve_request(const Triple& parsed) { r.triple.env = only.size() > prefix.size() ? std::string(only.substr(prefix.size() + 1)) : std::string{}; - // ⚠️ STILL NOT EXPLICIT. `envExplicit` records what the PROJECT asked + // STILL NOT EXPLICIT. `envExplicit` records what the PROJECT asked // for and feeds the C-library-request check; mcpp choosing a row is not // the project naming a C library. Setting it here would make // `check_request` compare mcpp's own answer against itself, and would @@ -537,7 +537,7 @@ inline RequestResolution resolve_request(const Triple& parsed) { // "add no target sysroot paths". The tier therefore needs no new branch // anywhere downstream — it reuses the answer the engine already knew how to // handle. -// ⚠️ A POINTER AND NOT AN `std::optional`. The tri-state is the +// A POINTER AND NOT AN `std::optional`. The tri-state is the // same — null means "the project said nothing" — and a pointer parameter // instantiates nothing in this module's interface. See the note on // `TargetEntry::sysroot` for what the optional cost when it reached one. @@ -786,13 +786,13 @@ std::optional parse(std::string_view s) { if (!sawOs) return std::nullopt; // macOS carries no env segment at all, so nothing was declined there. if (t.os == "macos") { t.env.clear(); t.envExplicit = false; } - // ⚠️ THE FILL STAYS, AND THE FACT THAT IT WAS A FILL IS NOW RECORDED. + // THE FILL STAYS, AND THE FACT THAT IT WAS A FILL IS NOW RECORDED. // `x86_64-linux` is the canonical identity `x86_64-linux-gnu` — every // directory name and cache key downstream depends on that — but it is NOT // the request `x86_64-linux-gnu`, which names a C library. See // `Triple::envExplicit`. if (t.os == "linux" && t.env.empty()) t.env = "gnu"; - // ⚠️ AND THE SAME ON WINDOWS AND ON BARE METAL, WHICH WERE MISSING AND MADE + // AND THE SAME ON WINDOWS AND ON BARE METAL, WHICH WERE MISSING AND MADE // THE RULE A LIE ON TWO PLATFORMS OUT OF FOUR. // // `x86_64-linux` parsed and `x86_64-windows` did not — `unknown target` — @@ -804,7 +804,7 @@ std::optional parse(std::string_view s) { // openkal. It is LLVM's label for the non-MSVC ABI, inherited from MinGW, // and mcpp cannot rename it — but it can stop requiring it to be typed. // - // ⚠️ `gnu` AND NOT THE HOST'S OWN ENV. `host_triple()` answers `msvc` on a + // `gnu` AND NOT THE HOST'S OWN ENV. `host_triple()` answers `msvc` on a // Windows machine, and filling from it would give one command a different // identity — a different output directory and cache key — on each host. A // target's identity may not depend on where it was built. `gnu` is the row diff --git a/modules/versioning/mcpp.toml b/modules/versioning/mcpp.toml index ac71e293..1c39d016 100644 --- a/modules/versioning/mcpp.toml +++ b/modules/versioning/mcpp.toml @@ -5,7 +5,7 @@ # version is read with. They ship together because a release moves both and # because separating them would put one constant in a package of its own. # -# ⚠️ `MCPP_VERSION` here is one of two version sites checked against each other +# `MCPP_VERSION` here is one of two version sites checked against each other # by `.github/tools/check_version_pins.sh`; the other is the root `mcpp.toml`. # Moving this file means moving that check's path, which is why the check reads # it by path rather than by glob. @@ -16,7 +16,7 @@ version = "0.1.0" description = "This binary's version, and SemVer requirement parsing" license = "Apache-2.0" -# ⚠️ NAMED `versioning`, NOT `version`. `version` is a reserved key inside a +# NAMED `versioning`, NOT `version`. `version` is a reserved key inside a # dependency specification, so `[dependencies.mcpp] version = { path = … }` # parses as "the mcpp dependency's version field" and fails with # `must specify 'path', 'version', or 'git'` -- an error about the wrong table. diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index c035f79e..a09cf03c 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.9.5.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.5.3"; } // namespace mcpp diff --git a/modules/versioning/tests/test_version_grammar.cpp b/modules/versioning/tests/test_version_grammar.cpp index fbea6a3a..8b4e2a39 100644 --- a/modules/versioning/tests/test_version_grammar.cpp +++ b/modules/versioning/tests/test_version_grammar.cpp @@ -10,7 +10,7 @@ import mcpp.version_req; // with. Nothing outside this package is involved. TEST(Versioning, ThisBinaryDeclaresAFourSegmentDateVersion) { - // ⚠️ The same string lives in the root mcpp.toml, and + // The same string lives in the root mcpp.toml, and // .github/tools/check_version_pins.sh keeps the two equal. This states the // SHAPE, which that checker cannot: a version that lost a segment would // still match the manifest and still be wrong. diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index f4274f00..5c8ead7d 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -43,7 +43,7 @@ struct BuildProgramEnv { // hostprogram::toolchain_dir / sysroot_dir for why declaring was wrong. std::string toolchainDir; std::string targetSysroot; - // ⭐⭐ THE TWO ANSWERS A SECOND COMPILER NEEDS AND CANNOT DERIVE. + // THE TWO ANSWERS A SECOND COMPILER NEEDS AND CANNOT DERIVE. // // `toolchainSysroot` is the `--sysroot` mcpp passes to its own compiler and // `toolchainBinutilsDir` the directory it names with `-B`; either is empty @@ -53,7 +53,7 @@ struct BuildProgramEnv { // facts, and on a hosted subos both are non-empty precisely because the C // library is not at `/usr/include` and the assembler is not at `/usr/bin`. // - // ⚠️ Measured 2026-09-05 on the CUDA example. `nvcc` refuses a libc++ host + // Measured 2026-09-05 on the CUDA example. `nvcc` refuses a libc++ host // compiler and fails on GCC 16's ``, so its rule package // resolves a second host compiler from a declared payload. That compiler // is not one mcpp resolved, so nothing tells it where anything is, and the @@ -66,7 +66,7 @@ struct BuildProgramEnv { // emits — so the answer belongs to the engine and is stated once here. std::string toolchainSysroot; std::string toolchainBinutilsDir; - // ⭐⭐ WHICH COMPILER RESOLVED — "gcc" | "clang" | "msvc" | "". + // WHICH COMPILER RESOLVED — "gcc" | "clang" | "msvc" | "". // // A package should never have to guess this, and until this field existed // the only way to was to look at `toolchainDir` and recognise a directory @@ -75,7 +75,7 @@ struct BuildProgramEnv { // compiler-rt under another, and the tool that turns a `.def` into an // import library is `dlltool` under one and `llvm-dlltool` under another. // - // ⚠️ Measured 2026-08-22, both on the same day and both from the same + // Measured 2026-08-22, both on the same day and both from the same // missing answer: `openkal-musl` naming `-lgcc` on a link whose compiler was // clang (`unable to find library -lgcc`), and `openkal-windows` running // `llvm-dlltool` under a GCC toolchain (`sh: 1: llvm-dlltool: not found`). @@ -83,7 +83,7 @@ struct BuildProgramEnv { std::string compilerId; // Three more answers a board-support package would otherwise hardcode. // - // ⚠️ THE COUPLING THESE REMOVE IS INVISIBLE IN A MANIFEST. `riscv-virt-rt` + // THE COUPLING THESE REMOVE IS INVISIBLE IN A MANIFEST. `riscv-virt-rt` // declares no dependency on LLVM or on picolibc — #459 removed those — and // yet it named `clang_rt.builtins-riscv64` (a compiler-rt fact, `libgcc` // under GCC) and `rv64gc/lp64d` (picolibc's multilib convention). A @@ -142,7 +142,7 @@ struct BuildProgramEnv { // the same re-run key: a rebuilt tool re-runs the program that uses it, // with no `rerun-if-changed` needed from the author. std::vector> toolPaths; - // ⭐⭐ THE `bin` OF THIS PROJECT'S OWN SubOS, AT THE FRONT OF THE CHILD'S + // THE `bin` OF THIS PROJECT'S OWN SubOS, AT THE FRONT OF THE CHILD'S // `PATH`. Empty for a project that has not declared one, and an empty // value means the child's `PATH` is left exactly as mcpp received it. // @@ -157,21 +157,21 @@ struct BuildProgramEnv { // sat in its own payload directory, reachable only by a path the program // would have had to construct itself. // - // ⚠️ THE PROJECT'S SubOS, NEVER A GLOBAL ONE. An earlier draft put this + // THE PROJECT'S SubOS, NEVER A GLOBAL ONE. An earlier draft put this // build system's shared `subos/default/bin` in front, which makes what a // build sees depend on what else has been installed on the machine — two // projects on one machine would agree with each other, and the same // project on two machines would not. A declared `[xlings].subos` is a // directory that belongs to the project and travels with it. // - // ⚠️ WHO DECIDES IS NOT DECIDED HERE. `mcpp::xlings::runtime` is the sole + // WHO DECIDES IS NOT DECIDED HERE. `mcpp::xlings::runtime` is the sole // project runtime-selection policy and `RuntimeBinding::subosDir` is its // resolved answer; this field carries that answer to the child. Deriving // it a second time — from the manifest, from `[xlings] deps`, from the // config — is how a build ends up with two subos and no way to say which // one it used. // - // ⚠️ PREPENDED, NOT SUBSTITUTED. A build program legitimately calls `git`, + // PREPENDED, NOT SUBSTITUTED. A build program legitimately calls `git`, // `python3` or a shell, none of which arrive this way. std::string toolsBin; // #355 step 5: dependency-provided modules to compile FOR THE HOST and make @@ -221,7 +221,7 @@ inline std::string xpkg_env_var(std::string_view ns, std::string_view name) { // Does a compiler's output say the program asked for something the bundled // `mcpp` module does not have? // -// ⚠️ This is the ONLY place an engine-too-old situation can be caught for the +// This is the ONLY place an engine-too-old situation can be caught for the // TYPED api, and it exists because the in-language probe does not: // // if constexpr (requires { mcpp::runner("x"); }) // ← hard error, @@ -345,12 +345,12 @@ std::vector host_base_flags(const mcpp::toolchain::Toolchain& tc, // undefined __cxa_* / __gxx_personality_v0. opt.cfgBypass = mcpp::toolchain::HostFlagOptions::CfgBypass::LinuxOnly; opt.clangStdlibSelect = true; - // ⭐ `cAbiPrebuilt` is left at its default (true), and that is a statement + // `cAbiPrebuilt` is left at its default (true), and that is a statement // rather than an omission: `tc` here is always HOST-targeting (see this // function's header), so the helper's C library is the payload's whatever // the project's target side turns out to be. // - // ⚠️ It also corrects a latent defect. The predicate this replaced was + // It also corrects a latent defect. The predicate this replaced was // `!tc.crossTargetFlag.empty()`, and a host toolchain resolved for a cross // build could carry one — in which case the helper lost the payload's own // headers for a reason that had nothing to do with it. @@ -412,7 +412,7 @@ contract_env(const fs::path& root, const fs::path& outDir, const BuildProgramEnv std::vector> e; auto hostT = mcpp::toolchain::triple::host_triple().str(); e.emplace_back("MCPP_TARGET", env.targetTriple.empty() ? hostT : env.targetTriple); - // ⭐⭐ THE SAME VALUE UNFILLED — EMPTY WHEN NOBODY NAMED A TARGET. + // THE SAME VALUE UNFILLED — EMPTY WHEN NOBODY NAMED A TARGET. // // `MCPP_TARGET` above answers "which machine is this for", and filling it // in with the host is right for that question. It cannot answer a different @@ -426,14 +426,14 @@ contract_env(const fs::path& root, const fs::path& outDir, const BuildProgramEnv // the only thing that can name one. A native build on the same machine gets // the SDK and needs nothing from the package. // - // ⚠️ Measured 2026-08-23, `openkal-macos` trying to decide this from what + // Measured 2026-08-23, `openkal-macos` trying to decide this from what // was available. From the host: right for the cross, wrong for // `--target aarch64-macos` ON a Mac (`library not found for -lSystem`). // From `MCPP_TARGET`: right for the cross, wrong for the native build, // because it is never empty (`undefined symbol: wcslen`, `strtoul`, … — // the package's three-name stub had shadowed the vendor's complete one). // - // ⭐ An older mcpp sets neither, and that is the correct answer for it: + // An older mcpp sets neither, and that is the correct answer for it: // it has no graph-supplied target side, so the system is always on the // link and a package should supply nothing. e.emplace_back("MCPP_TARGET_REQUESTED", env.targetTriple); @@ -524,7 +524,7 @@ contract_env(const fs::path& root, const fs::path& outDir, const BuildProgramEnv // rather than a replacement, and why the decision is not made here. if (!env.toolsBin.empty()) { std::string path = env.toolsBin; - // ⚠️ THE INHERITED VALUE IS READ HERE AND NOT ASSUMED. `extraEnv` + // THE INHERITED VALUE IS READ HERE AND NOT ASSUMED. `extraEnv` // replaces a variable outright in the child, so writing only the // project's own directory would silently be the substitution this // deliberately is not. @@ -764,7 +764,7 @@ std::expected run_build_program( // moment a Clang user tries it, since only GCC makes the BMI reachable // without the flags. // - // ⚠️ AHEAD OF THE FAST PATH ON PURPOSE. Withdrawing `reexport = true` from + // AHEAD OF THE FAST PATH ON PURPOSE. Withdrawing `reexport = true` from // an edge leaves the module SET and every interface hash identical, so // nothing in `compilerIdentity` moves. Measured: the replay is prevented // today only because `ctxHash` happens to change too — an incidental @@ -790,7 +790,7 @@ std::expected run_build_program( CacheRecord cache = read_cache(bdir); if (cache_fresh(root, bdir, cache, programHash, compilerHash, ctxHash)) { dirs::apply(m, cache.directives); - // ⚠️ ONE OF TWO SITES, AND THE ONE THAT IS EASY TO FORGET. + // ONE OF TWO SITES, AND THE ONE THAT IS EASY TO FORGET. // // A cache hit does not re-run the program, so an advisory emitted only // on the run path below would appear on the first build of a project @@ -1048,7 +1048,7 @@ std::expected run_build_program( // compile/precompile commands, where a link flag has no business (and for // Clang would perturb the default PIC/PIE codegen of mcpp.o). if (staticHostHelper) compileArgv.push_back(std::string(dial.staticRuntime)); - // ⚠️ A DYNAMIC HELPER ON LINUX GETS `DT_RPATH`, NOT `DT_RUNPATH`. + // A DYNAMIC HELPER ON LINUX GETS `DT_RPATH`, NOT `DT_RUNPATH`. // // The driver's default is the new tag, and a runpath is consulted only for // the helper's OWN needed libraries. A build program that opens a host diff --git a/src/build/cache_key.cppm b/src/build/cache_key.cppm index fad8e517..3222086e 100644 --- a/src/build/cache_key.cppm +++ b/src/build/cache_key.cppm @@ -82,7 +82,7 @@ struct BuildAxes { std::string compilerVersion; std::string driverIdentity; std::string targetTriple; - // ⚠️ The flags the TRIPLE implies, not the ones anyone wrote down. + // The flags the TRIPLE implies, not the ones anyone wrote down. // // A freestanding triple silently carries `-march`/`-mabi`/`-mcmodel`/ // `-ffreestanding`/`-nostdinc++`/`-fno-exceptions`/`-fno-rtti`, and which @@ -98,14 +98,14 @@ struct BuildAxes { // Measured on exactly that upgrade. Empty for hosted targets, so nothing // else's key moves. std::vector targetImpliedFlags; - // ⭐⭐ THE HEADER SET THE DRIVER IS POINTED AT — not the driver. + // THE HEADER SET THE DRIVER IS POINTED AT — not the driver. // // Everything else on this axis describes the COMPILER. Nothing described // the LIBRARY it compiles against, and the two are separately installed: // one clang payload sits above whichever `xim:glibc` and // `xim:linux-headers` the home happens to carry. // - // ⚠️ AND `driverIdentity` CANNOT COVER IT, BY DESIGN. + // AND `driverIdentity` CANNOT COVER IT, BY DESIGN. // `normalize_driver_output` deliberately strips `/home/`, `/tmp/` and // `/var/` paths out of `clang --version`, which is what lets one entry be // shared between two homes. Correct — and it means two homes carrying the @@ -119,7 +119,7 @@ struct BuildAxes { // rather than producing a readable diagnostic. BMIs carry no cross-check // against each other. // - // ⭐ THE `std` CACHE ON THE SAME MACHINE HAS ALWAYS HAD THIS. Its identity + // THE `std` CACHE ON THE SAME MACHINE HAS ALWAYS HAD THIS. Its identity // folds in `std_build_commands`, the whole command line, `-isystem` rows // included. Two caches, one machine, two notions of "the same inputs"; the // dependency one was the short one. @@ -141,7 +141,7 @@ struct BuildAxes { bool debug = false; bool lto = false; bool strip = false; - // ⚠️ Position-independent code, and it is here because it was MISSING. + // Position-independent code, and it is here because it was MISSING. // // `-fPIC` is whole-build: one shared link unit anywhere and every object // in the graph gets it. That was invisible to this key, which hashes a @@ -369,7 +369,7 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, : (tc.binaryPath.empty() ? std::string{} : mcpp::toolchain::hash_file(tc.binaryPath)); b.targetTriple = tc.targetTriple; - // ⚠️⚠️ `tc.targetCxxRuntime` IS PASSED, AND OMITTING IT MADE THIS KEY + // `tc.targetCxxRuntime` IS PASSED, AND OMITTING IT MADE THIS KEY // DESCRIBE A COMPILATION THAT DOES NOT HAPPEN. // // `compile_flags` takes that argument because the answer changes with it: @@ -379,7 +379,7 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, // `plan.toolchain.targetCxxRuntime`); this key did not, so it defaulted to // `false` and hashed the flags of the other configuration. // - // ⭐ THE CONSEQUENCE IS A CACHE HIT ACROSS AN INCOMPATIBILITY, NOT A MISS. + // THE CONSEQUENCE IS A CACHE HIT ACROSS AN INCOMPATIBILITY, NOT A MISS. // Two configurations that must not share a slot hashed to the same key, so // the second build loaded the first's BMIs. Measured 2026-08-25 on a // bare-metal program over openkal-opensbi: @@ -391,7 +391,7 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, // holding five differently-sized copies of that one BMI. Slotting per // configuration was working; choosing the slot was not. // - // ⚠️ IT WAS DORMANT UNTIL THE ARGUMENT EXISTED. Before #486 the two layers + // IT WAS DORMANT UNTIL THE ARGUMENT EXISTED. Before #486 the two layers // computed identical flag lists for every input, so a key that ignored one // of them was still correct. The defect is that a cache key derived its // inputs a second time instead of reading what the build uses — and that @@ -403,7 +403,7 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, b.stdlibId = tc.stdlibId; b.stdlibVersion = tc.stdlibVersion; - // ⭐ THE HEADER SET, TAKEN FROM THE RESOLVERS THAT PRODUCE THE COMMAND + // THE HEADER SET, TAKEN FROM THE RESOLVERS THAT PRODUCE THE COMMAND // LINE — not derived a second time. // // `mcpp.toolchain.linkmodel` is already the single answer to "where does @@ -411,13 +411,13 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, // compile line reads it. Asking it here means the key describes the header // set that will actually be used, and cannot drift from it. // - // ⚠️ RELATIVE, IN TWO TIERS — and `` ALONE IS NOT ENOUGH. + // RELATIVE, IN TWO TIERS — and `` ALONE IS NOT ENOUGH. // // The absolute form names this machine's home, and the whole point of // `normalize_driver_output` stripping paths is that one entry can serve // two homes carrying the same payloads. Leaving these absolute undoes it. // - // ⚠️ MEASURED, AND `` MISSED THE COMMON CASE. This developer's own + // MEASURED, AND `` MISSED THE COMMON CASE. This developer's own // toolchain resolves `CLibMode::Sysroot`, whose only compile token is // // --sysroot=/home//.mcpp/registry/subos/default @@ -427,7 +427,7 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, // second tier for exactly this, mirroring the ``/`` pair // `fill_package_config` already uses for the same reason. // - // ⭐ WHAT THIS AXIS DOES AND DOES NOT SEPARATE, stated so the next reader + // WHAT THIS AXIS DOES AND DOES NOT SEPARATE, stated so the next reader // does not have to re-derive it: // * payload-supplied headers — `/xim-x-glibc/2.44/include` — carry // the VERSION in the path, so two payloads are two keys. This is the diff --git a/src/build/distribution.cppm b/src/build/distribution.cppm index 076a0c87..f100f225 100644 --- a/src/build/distribution.cppm +++ b/src/build/distribution.cppm @@ -103,13 +103,13 @@ enum class Contract { // priority-ordered initializer section, PE has no rpath, ELF has both. enum class Format { Elf, MachO, Pe }; -// ⭐⭐ WHICH FORMAT A TARGET PRODUCES, ASKED OF THE TARGET. +// WHICH FORMAT A TARGET PRODUCES, ASKED OF THE TARGET. // // `hostFallback` is what a triple outside the vocabulary falls back to, and it // is a parameter rather than a compile-time constant so that this function can // be examined without being the machine it is about. // -// ⚠️ THIS USED TO BE A LAMBDA INSIDE A FIFTEEN-HUNDRED-LINE FUNCTION, AND THAT +// THIS USED TO BE A LAMBDA INSIDE A FIFTEEN-HUNDRED-LINE FUNCTION, AND THAT // IS WHY IT HAD NO TEST. It tested the triple for the substrings `apple` and // `darwin`, which are LLVM's words; mcpp's canonical form is `aarch64-macos` // and contains neither, so the test fell through to a question about the HOST @@ -296,7 +296,7 @@ struct MechanismInput { // (measured 2026-08-19). A target-side C++ runtime, if one is wanted, is // an ordinary package — the same way the libc is. bool freestanding = false; - // ⭐⭐ THE HOSTED FORM OF THE LINE ABOVE: a package in the graph supplies + // THE HOSTED FORM OF THE LINE ABOVE: a package in the graph supplies // the C++ runtime, built for this target, and its objects are already on // the link line. // @@ -307,7 +307,7 @@ struct MechanismInput { // the host's, which is the same defect the `freestanding` flag above // exists for; the difference is only that this target has an OS. // - // ⚠️ Measured 2026-08-23, cross-building for `aarch64-macos` over openkal + // Measured 2026-08-23, cross-building for `aarch64-macos` over openkal // right after the format decision was corrected to key on the target — the // wrong format had been masking this: // @@ -408,7 +408,7 @@ Mechanism resolve(const MechanismInput& in) { // mechanism is not a degradation — the artifact genuinely carries // everything it has — so this reports no diagnostic. // - // ⚠️ Placed before the format switch rather than inside it: the format is + // Placed before the format switch rather than inside it: the format is // ELF here, and every ELF cell below reaches for the toolchain's HOST // archives. One of them silently produced a link line with // x86-64 libc++.a on a riscv64 link. diff --git a/src/build/execute.cppm b/src/build/execute.cppm index eb3fea84..89853d12 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -166,7 +166,7 @@ struct BuildCacheEntry { // Did the graph declare a tool on the `when = "run"` tier that this build // did NOT provision? // - // ⚠️ A BUILD INSTALLS LESS THAN A RUN NEEDS, WHICH IS THE POINT OF THE + // A BUILD INSTALLS LESS THAN A RUN NEEDS, WHICH IS THE POINT OF THE // TIER AND ALSO ITS ONE HAZARD. `mcpp build` requests the build tier; a // later `mcpp run` requests more. The fast path exists precisely to skip // the pass that would install the difference, so an entry written by a @@ -177,7 +177,7 @@ struct BuildCacheEntry { // every entry such a cache could hold, because no manifest could express // the tier. bool runTierPending = false; - // ⚠️⚠️ THE FEATURE SET THIS ENTRY'S ARTEFACTS WERE BUILT WITH. + // THE FEATURE SET THIS ENTRY'S ARTEFACTS WERE BUILT WITH. // // The entry is keyed on (target, profile, cache mode) and was matched on // those three alone, while the OUTPUT DIRECTORY is keyed on a fingerprint @@ -575,7 +575,7 @@ struct RunnerChoice { std::string tripleKey; }; -// ⭐⭐ ONE READER FOR FOUR SLOTS, PARAMETERISED BY THE SLOT. +// ONE READER FOR FOUR SLOTS, PARAMETERISED BY THE SLOT. // // `run`, `flash`, `monitor` and `debug` resolve identically: a dependency // supplies a template, the project may override it on the same axis, the @@ -745,7 +745,7 @@ export int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache, } } - // ⚠️ RECLAIM THE STALE CONCURRENCY TOKENS, HERE AND NOT IN prepare. + // RECLAIM THE STALE CONCURRENCY TOKENS, HERE AND NOT IN prepare. // // detach-codegen bounds real compiler concurrency with a semaphore of // directories under `/.mcpp-sched`, released by the supervisor @@ -1141,7 +1141,7 @@ fast_path_identity(const std::filesystem::path& projectRoot, // Try to fast-path: if build.ninja is newer than all inputs, just run ninja. // Returns exit code on fast-path, or nullopt if full rebuild needed. -// ⭐ WHAT THIS PROJECT CAN DO, AS OPPOSED TO WHAT THE ENGINE SUPPORTS. +// WHAT THIS PROJECT CAN DO, AS OPPOSED TO WHAT THE ENGINE SUPPORTS. // // The engine knows no runner names, so it cannot print a static list of them — // and that is the useful property, not a limitation. What a reader wants is @@ -1198,7 +1198,7 @@ export std::optional try_fast_build(const std::filesystem::path& projectRoo std::string_view currentTarget = "") { if (no_cache) return std::nullopt; - // ⚠️⚠️ `--locked` MUST NOT MEET THE FAST PATH, OR IT ASSERTS NOTHING. + // `--locked` MUST NOT MEET THE FAST PATH, OR IT ASSERTS NOTHING. // // The check it names lives at the resolution write point, and the fast path // exists precisely to skip resolution. Measured before this line existed: a @@ -1337,7 +1337,7 @@ std::optional try_fast_run(const std::filesystem::path& projectRoot, auto want = fast_path_identity(projectRoot); if (!want) return std::nullopt; - // ⚠️ THE precondition of this whole function: it exec's the cached + // THE precondition of this whole function: it exec's the cached // artifact itself, so it is only ever valid when that artifact is for THIS // machine. // @@ -1521,12 +1521,12 @@ export int build_run_target(const std::optional& targetName, bool no_cache = false, const std::string& target_triple = {}, bool no_runner = false, - // ⭐ The NAME of the way to reach the artefact. + // The NAME of the way to reach the artefact. // Empty is the default runner — `mcpp run`. Any // other value came from `--runner ` and the // engine has never seen it before. std::string_view runner_name = {}, - // ⭐⭐ THE TWO AXES `build` AND `test` HAVE ALWAYS + // THE TWO AXES `build` AND `test` HAVE ALWAYS // TAKEN, AND `run` DID NOT. // // Both change WHAT IS BUILT, so a `run` that could @@ -1562,12 +1562,12 @@ export int build_run_target(const std::optional& targetName, // manifest to print the note against. if (package_filter.empty() && cache_mode.empty() && !no_cache && target_triple.empty() && !no_runner - // ⚠️ AND NEITHER NEW AXIS IS SET. The cached entry was written for + // AND NEITHER NEW AXIS IS SET. The cached entry was written for // whichever feature set and profile the last build used; taking it // here would silently ignore the flag, which is the same reason // `--cache` and `--profile` bypass it in `cmd_build`. && features.empty() && profile.empty() && accel.empty() - // ⚠️⚠️ THE FAST PATH IS `run`'s, AND ONLY `run`'s. + // THE FAST PATH IS `run`'s, AND ONLY `run`'s. // // It exec's the cached artefact directly — that IS its definition — so // for `flash`, `monitor` or `debug` it would run the program on the @@ -1601,7 +1601,7 @@ export int build_run_target(const std::optional& targetName, auto ctx = prepare_build(/*print_fp=*/false, /*includeDevDeps=*/false, /*extraTargets=*/{}, ov); if (!ctx) { std::println(stderr, "error: {}", ctx.error()); return 2; } - // ⚠️ `target_triple` IS PASSED, AND OMITTING IT WROTE A CROSS BUILD INTO + // `target_triple` IS PASSED, AND OMITTING IT WROTE A CROSS BUILD INTO // THE HOST'S CACHE SLOT. // // The last argument becomes the cache entry's `[target=]` key. `cmd_build` @@ -1667,7 +1667,7 @@ export int build_run_target(const std::optional& targetName, choice.tripleKey, isRunSlot ? std::string("runner") : std::format("runner '{}'", slotName))); - // ⚠️ THE THREE NEW SLOTS HAVE NO FALLBACK, AND `run` STILL DOES. + // THE THREE NEW SLOTS HAVE NO FALLBACK, AND `run` STILL DOES. // // An artefact with no runner on a hosted target is executed directly, and // that is right: the host can run it. There is no such reading of "no @@ -1676,7 +1676,7 @@ export int build_run_target(const std::optional& targetName, // freestanding one. Saying "nothing is configured" beats doing something // that was never asked for. if (!isRunSlot && choice.tmpl.empty()) { - // ⚠️ AND THE MESSAGE LISTS WHAT THIS PROJECT DOES HAVE. A name the + // AND THE MESSAGE LISTS WHAT THIS PROJECT DOES HAVE. A name the // engine does not know is usually a typo or a missing feature, and // "no such runner" alone leaves the reader guessing which. std::string have; @@ -1724,7 +1724,7 @@ export int build_run_target(const std::optional& targetName, tmpl.front() = found.program->string(); argv = mcpp::freestanding::expand(tmpl, exe); for (auto& a : passthrough) argv.push_back(a); - // ⭐ The status word is the NAME the package chose, capitalised. The + // The status word is the NAME the package chose, capitalised. The // engine has no table of verbs to look one up in, which is the point: // `Serve`, `Submit` and `Flash` all read correctly and none is known // here. @@ -2234,7 +2234,7 @@ export int run_tests(std::span passthrough, const bool capture = json || list.size() > 1; const auto deadline = std::chrono::milliseconds( static_cast(testOpts.timeoutSecs) * 1000); - // ⚠️⚠️ A PHYSICAL BOARD IS A MUTEX, AND NOTHING ELSE THIS POOL HAS EVER + // A PHYSICAL BOARD IS A MUTEX, AND NOTHING ELSE THIS POOL HAS EVER // SCHEDULED WAS ONE. // // An emulator takes N concurrent instances; a probe attached to one diff --git a/src/build/flags.cppm b/src/build/flags.cppm index b1dc6af5..e89b54f9 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -61,7 +61,7 @@ struct CompileFlags { std::filesystem::path ccBinary; // gcc / clang (derived; cl.exe = same) std::filesystem::path arBinary; // ar / llvm-ar / lib.exe (empty → PATH) std::filesystem::path ldBinary; // link.exe (SeparateLinker dialects only) - // ⚠️ THE LINK IS DRIVEN BY THIS BINARY INSTEAD OF BY THE COMPILER, AND + // THE LINK IS DRIVEN BY THIS BINARY INSTEAD OF BY THE COMPILER, AND // WHEN IT IS SET THE WHOLE FLAG VOCABULARY CHANGES WITH IT. // // Empty everywhere except a freestanding target whose row carries an @@ -431,7 +431,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // flag outright — `unsupported option '-fPIC' for target // 'x86_64-pc-windows-msvc'`. // - // ⚠️ The condition used to be `!isMsvcDialect`, i.e. the DIALECT. Windows' + // The condition used to be `!isMsvcDialect`, i.e. the DIALECT. Windows' // default toolchain is clang, which speaks the GNU dialect while targeting // the MSVC ABI, so `-fPIC` was emitted and every MSVC-ABI shared build died // in clang-scan-deps before compiling anything. It was unreachable while @@ -512,7 +512,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string compile_toolchain_flags; std::string link_toolchain_flags; std::string link_toolchain_flags_c; // same, minus C++ runtime selection - // ⭐⭐ THE TRIPLE ON THE LINK LINE TOO, AND FOR A DIFFERENT REASON THAN ON + // THE TRIPLE ON THE LINK LINE TOO, AND FOR A DIFFERENT REASON THAN ON // THE COMPILE LINE. // // Compiling without it produces objects for the wrong machine. LINKING @@ -521,7 +521,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // `ld64.lld` for Mach-O, `lld-link` for PE. With no target it picks the // host's. // - // ⚠️ Measured 2026-08-23, cross-linking for macOS from Linux, after the + // Measured 2026-08-23, cross-linking for macOS from Linux, after the // objects were already correct Mach-O: // // ld.lld: error: obj/main.o: unknown file type @@ -590,7 +590,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { mcpp::toolchain::HostFlagOptions hopt; hopt.cfgBypass = mcpp::toolchain::HostFlagOptions::CfgBypass::Always; hopt.macosDeploymentTarget = macosDeploymentTarget; - // ⭐ THE SAME EXPRESSION THE LINK SIDE ASKS, twenty lines further down + // THE SAME EXPRESSION THE LINK SIDE ASKS, twenty lines further down // (`plan.targetSide.cAbi.prebuilt()`). Reading one value at both sites // is what makes it impossible for them to disagree — which they did, // from #511 until now, because only the link side was corrected. @@ -598,7 +598,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { compile_toolchain_flags = mcpp::toolchain::render_tokens( mcpp::toolchain::host_compile_tokens(plan.toolchain, hopt, ninjaEsc)); } else { - // ⚠️ Skipped entirely, not filtered. What this block emits is the + // Skipped entirely, not filtered. What this block emits is the // HOST's world reconstructed by hand — libc++'s headers, glibc's // headers, the Linux UAPI headers — because the cfg that normally // supplies them is bypassed. Every one of those is for the host, and @@ -617,7 +617,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Linker flags that cfg normally provides. The payload C-runtime // flags (-B/-L/loader) are appended via payload_ld below. link_toolchain_flags = crossTarget + " --no-default-config"; - // ⚠️⚠️ THE CONDITION IS WHETHER THE GRAPH SUPPLIES THE C LIBRARY, AND + // THE CONDITION IS WHETHER THE GRAPH SUPPLIES THE C LIBRARY, AND // FOR A LONG TIME IT ASKED WHETHER `--target` HAD BEEN TYPED. // // `crossTarget` is the string `--target=`. It is non-empty @@ -634,12 +634,12 @@ CompileFlags compute_flags(const BuildPlan& plan) { // `xim:glibc` — `-B`, `-L` and the loader — so clang fell back to its // defaults and the startup objects resolved out of /lib. // - // ⭐ `cAbi.prebuilt()` IS THE QUESTION THE COMMENT BELOW ALREADY ASKED, + // `cAbi.prebuilt()` IS THE QUESTION THE COMMENT BELOW ALREADY ASKED, // and it is the same predicate 2026.8.25.1 moved three other decisions // onto. e2e 295 states the invariant as an identity: naming the host's // own target changes nothing. if (!plan.targetSide.cAbi.prebuilt()) { - // ⭐⭐ THE TARGET SIDE COMES FROM THE GRAPH, SO THE HOST'S MODEL + // THE TARGET SIDE COMES FROM THE GRAPH, SO THE HOST'S MODEL // CONTRIBUTES NOTHING — THE SAME REPLACEMENT `stdModuleFlags` // ALREADY MAKES ON THE COMPILE SIDE. // @@ -650,7 +650,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // package that knows a format states its own link line (openkal-musl // carries `-nostdlib` plus that format's entry symbol). // - // ⚠️ Measured 2026-08-23, after the correct linker was finally + // Measured 2026-08-23, after the correct linker was finally // being chosen: // // ld64.lld: error: unknown argument '--as-needed' @@ -1043,7 +1043,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // A program with no C++ runtime needs the driver stopped from adding // one just as much as a program that brought its own. // - // ⚠️ IT WAS THEN `system_from_graph()`, WHICH OVERSHOT IN THE OTHER + // IT WAS THEN `system_from_graph()`, WHICH OVERSHOT IN THE OTHER // DIRECTION. That is an OR over two layers, and a program whose kernel // interface comes from a package while its C library and C++ runtime // are the payload's is served by the payload's archives — yet the OR @@ -1054,7 +1054,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // // measured on `openkal-linux = "0.5.4"` with a `throw` in main. // - // ⭐ THE C LIBRARY IS WHAT DECIDES IT, for the same reason it decides + // THE C LIBRARY IS WHAT DECIDES IT, for the same reason it decides // the link line's search paths: the payload's C++ runtime was // configured against the payload's C library, so it is eligible when // and only when that C library is the one in use. `check_layering` @@ -1221,7 +1221,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // but the link line has a hard 128KiB ceiling (MAX_ARG_STRLEN) that real // workspaces already spend 43% of. std::string payload_ld; - // ⚠️ AND NOT WHEN THE TARGET SIDE COMES FROM THE GRAPH — the second half of + // AND NOT WHEN THE TARGET SIDE COMES FROM THE GRAPH — the second half of // the replacement made where `link_toolchain_flags` is built. `lm` describes // THIS MACHINE's C runtime: `-B`/`-L` into the payload and this host's // dynamic loader. Measured 2026-08-23, with the correct linker already @@ -1230,12 +1230,12 @@ CompileFlags compute_flags(const BuildPlan& plan) { // ld64.lld: error: unknown argument // '--dynamic-linker=…/xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2' // - // ⚠️ The first replacement alone was not enough, and that is the finding: + // The first replacement alone was not enough, and that is the finding: // the C-runtime group reaches the link line through TWO channels, and a // reader who fixed one saw the identical error and could reasonably // conclude the fix had not worked. // - // ⭐⭐ AND THE CONDITION IS THE SAME ONE, MADE CORRECT THE SAME WAY. This + // AND THE CONDITION IS THE SAME ONE, MADE CORRECT THE SAME WAY. This // asked `crossTargetFlag.empty()` — "was `--target` typed" — where it meant // "does the graph supply the C library". `cAbi.prebuilt()` is that // question. The comment above says the group reaches the link line through @@ -1276,14 +1276,14 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Windows MinGW build (host≠target). No rpath/loader/payload model. Static // + libstdc++exp (std::print's __open_terminal/__write_to_terminal live in // libstdc++exp.a, not plain libstdc++). Self-contained binutils → no -B. - // ⚠️ AND ONLY WHILE THE PAYLOAD IS THE TARGET, which is what the paragraph + // AND ONLY WHILE THE PAYLOAD IS THE TARGET, which is what the paragraph // above assumes without saying so: `x86_64-w64-mingw32-g++` needs no // `--target` because it HAS no other. When the target side comes from the // dependency graph the compiler is an ordinary retargetable clang, and this // branch is one of three shaped by the HOST rather than by the target — see // the replacement below, which covers all three at once. // - // ⚠️ THE SAME PREDICATE AS THE REPLACEMENT, BECAUSE THEY ARE COMPLEMENTARY. + // THE SAME PREDICATE AS THE REPLACEMENT, BECAUSE THEY ARE COMPLEMENTARY. // "covers all three at once" is only true while the two conditions are each // other's negation; if this one said `system` and that one said `C library`, // a mingw build whose kernel interface came from the graph and whose C @@ -1296,7 +1296,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string mingw_stdexp; if (caps.stdlib_id == "libstdc++") mingw_stdexp = " -lstdc++exp"; - // ⭐⭐ `crossTarget`, BECAUSE THE PARAGRAPH ABOVE STATES A PRECONDITION + // `crossTarget`, BECAUSE THE PARAGRAPH ABOVE STATES A PRECONDITION // THE CONDITION DOES NOT CHECK: "`x86_64-w64-mingw32-g++` needs no // `--target` because it HAS no other". // @@ -1313,7 +1313,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // — the identical sentence the comment beside `crossTarget` records for // a Mach-O cross, reached through this branch instead. // - // ⭐ Not a new condition but the string that names the machine: a mingw + // Not a new condition but the string that names the machine: a mingw // g++ has an empty `crossTargetFlag`, so this is the identity for the // case the paragraph was written about, and the fix for the other one. f.ld = std::format("{}{}{}{}{}", crossTarget, link_intent_ld, @@ -1325,7 +1325,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { return f; } - // ⭐ THE PART OF THE LINK LINE THAT BELONGS TO THE TARGET, NOT THE PAYLOAD. + // THE PART OF THE LINK LINE THAT BELONGS TO THE TARGET, NOT THE PAYLOAD. // // Empty on every platform whose kernel interface is an instruction rather // than a library — Linux's is `syscall`, so a self-contained libc from the @@ -1386,7 +1386,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // // Native cl.exe (isMsvcDialect, returned above) keeps link.exe: there // the response file is ours, and 2026.8.5.3 already fixed it. - // ⚠️ `full_static` IS ON THIS LINE, AND IT WAS NOT. + // `full_static` IS ON THIS LINE, AND IT WAS NOT. // // The two branches below both carry it; this one did not, and nothing // showed because a Windows host's `-static` for an ELF target was @@ -1442,7 +1442,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string macos_sdk; if (auto sdk = mcpp::platform::macos::sdk_path()) macos_sdk = " -isysroot " + escape_path(*sdk); - // ⭐⭐ AND KEPT, BECAUSE THE GRAPH BRANCH BELOW REPLACES THIS LINE. + // AND KEPT, BECAUSE THE GRAPH BRANCH BELOW REPLACES THIS LINE. // // `-isysroot` and the deployment floor describe the TARGET OS. Every // other token here describes the payload — which is exactly what a @@ -1471,12 +1471,12 @@ CompileFlags compute_flags(const BuildPlan& plan) { // its own. Inject `-latomic` (under --as-needed) after runtime_dirs // so its -L entries are on the search path; self-guards on the lib // actually being present (see atomic_link_flag). - // ⚠️ THE THIRD CHANNEL. `libatomic` is this HOST's, found by looking in + // THE THIRD CHANNEL. `libatomic` is this HOST's, found by looking in // the payload's directories, and `--push-state` / `--as-needed` are GNU // ld spellings that a Mach-O or PE linker does not have. A target whose // runtime comes from the graph gets its own answer to oversized // `std::atomic` from that graph. - // ⭐ THE THIRD CHANNEL, AND THE LAST ONE. `libatomic` is this HOST's, + // THE THIRD CHANNEL, AND THE LAST ONE. `libatomic` is this HOST's, // found in the payload's directories, so it belongs to exactly the // group `link_toolchain_flags` and `payload_ld` belong to — and it // asked the same wrong question they did. Fixing the first two left @@ -1502,7 +1502,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // ── The target side comes from the graph, so the HOST's link is wrong ── // - // ⭐⭐ THE THREE BRANCHES ABOVE ARE SHAPED BY THIS MACHINE, NOT BY THE + // THE THREE BRANCHES ABOVE ARE SHAPED BY THIS MACHINE, NOT BY THE // TARGET. `if constexpr (is_windows)` / `needs_explicit_libcxx` / else is a // question about where mcpp itself was built, and each answer describes a // link on that machine: an SDK path, a deployment target, a loader search @@ -1510,7 +1510,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // is the host or is served by a payload, and wrong when the C library, the // C++ runtime and the platform are packages in the dependency graph. // - // ⚠️ ONLY THE THIRD BRANCH EVER CONSUMED `link_toolchain_flags`, WHICH IS + // ONLY THE THIRD BRANCH EVER CONSUMED `link_toolchain_flags`, WHICH IS // WHERE `--target=` LIVES. So a cross build over openkal linked correctly // from a Linux host and would have handed a Mach-O or an ELF to a linker // told nothing about the target from a macOS or a Windows one. The measured @@ -1524,7 +1524,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // unnecessary but wrong, and appending to it would leave the outcome // depending on the driver's flag ordering rather than on a decision. // - // ⚠️ AND IT REPLACES A SPECIAL CASE RATHER THAN ADDING ONE. The PE branch + // AND IT REPLACES A SPECIAL CASE RATHER THAN ADDING ONE. The PE branch // above carried its own copy of this for one format; this covers PE, Mach-O // and ELF, on every host, with the predicate stated once. // @@ -1540,14 +1540,14 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Mach-O link as `-Wl,-rpath,…/lib/x86_64-unknown-linux-gnu`, which ld64 // accepts and writes into the image), `payload_ld`, `atomic_ld`. // - // ⚠️ AND THE CONDITION IS THE C LIBRARY, NOT THE SYSTEM. Everything the + // AND THE CONDITION IS THE C LIBRARY, NOT THE SYSTEM. Everything the // replacement drops is a way of reaching the PAYLOAD's C library, so a // build whose C library still comes from the payload must not enter here // — however much of the rest of its target side the graph supplies. See // the note beside `Layer::prebuilt` in mcpp.targetside, which records what // this got wrong and what it cost. if (!isFreestandingTarget && !plan.targetSide.cAbi.prebuilt()) { - // ⚠️ ASSEMBLED HERE RATHER THAN TAKEN FROM `link_toolchain_flags`, + // ASSEMBLED HERE RATHER THAN TAKEN FROM `link_toolchain_flags`, // BECAUSE THAT STRING IS ONLY POPULATED WHEN THE PAYLOAD HAS A CONFIG // FILE (`isClangWithCfg`). The Linux payload ships one and the Windows // payload does not, so on a Windows host the replacement emitted no @@ -1605,7 +1605,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { if (plan.toolchain.compiler == mcpp::toolchain::CompilerId::Clang) graphLd += " -fuse-ld=lld"; - // ⭐⭐ AND THE TARGET'S OWN ANCHOR SURVIVES THE REPLACEMENT. + // AND THE TARGET'S OWN ANCHOR SURVIVES THE REPLACEMENT. // // Everything this branch rebuilds describes the PAYLOAD — its `-B`, its // startup objects, its loader — and a graph-supplied C library is @@ -1704,7 +1704,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Reproduced on this toolchain 2026-08-19. in.lld = mcpp::freestanding::resolve_lld(plan.toolchain.binaryPath); in.sysrootLib = plan.toolchain.targetSysrootLib; - // ⚠️ `--no-default-config` FIRST, and it is not hygiene. + // `--no-default-config` FIRST, and it is not hygiene. // // The llvm payload ships bin/clang++.cfg with an unconditional // `-Wl,--dynamic-linker=…/ld-linux-x86-64.so.2`, a host `-L` and @@ -1714,7 +1714,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // RISC-V firmware image with an x86-64 PT_INTERP baked in, which // links clean and reports success. Measured, on this very change, // before this line existed. - // ⚠️ TWO SHAPES, AND WHICH ONE IS A PROPERTY OF THE TARGET'S ROW. + // TWO SHAPES, AND WHICH ONE IS A PROPERTY OF THE TARGET'S ROW. // // With an `lldEmulation` the compiler driver is bypassed entirely, // because for that target it would hand the link to a host `g++` @@ -1738,7 +1738,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { f.bFlag.clear(); f.ldRuntimeFallback.clear(); f.linkage = "static"; - // ⚠️ WHEN THE LINKER IS DRIVEN DIRECTLY, EVERY REMAINING DRIVER + // WHEN THE LINKER IS DRIVEN DIRECTLY, EVERY REMAINING DRIVER // FLAG IS AN ERROR RATHER THAN A NO-OP. Measured on the first // attempt, where two survived the freestanding replacement and // reached `ld.lld`: diff --git a/src/build/graph_shape.cppm b/src/build/graph_shape.cppm index a74f8594..f8581c8e 100644 --- a/src/build/graph_shape.cppm +++ b/src/build/graph_shape.cppm @@ -54,8 +54,19 @@ std::string_view to_string(GraphShape shape) { // schedule must not be replayed under another. Flipping the switch has to // invalidate the graph, and the only way that cannot be forgotten is if the // graph says which schedule produced it. -std::string header_line(GraphShape shape, std::string_view scheduleTag) { - return std::format("# mcpp:graph={};schedule={}", to_string(shape), scheduleTag); +// `accelOverridden` records whether `--accel` / `--no-accel` chose the device +// variant. That variant is in the fingerprint, so the two builds land in +// different directories -- which is exactly why the fast path cannot tell +// them apart: it replays the directory built LAST, before any plan exists to +// say which directory a plain build would choose. Measured: `mcpp build`, +// `mcpp build --no-accel`, `mcpp build` -- the third reported "Finished in +// 0.00s" and `mcpp run` executed the CPU variant. A graph an override chose +// says so, and the fast paths, which run only without overrides, decline it. +std::string header_line(GraphShape shape, std::string_view scheduleTag, + bool accelOverridden = false) { + return std::format("# mcpp:graph={};schedule={};accel={}", + to_string(shape), scheduleTag, + accelOverridden ? "override" : "default"); } // Read the shape back. `nullopt` means "this file does not say" — a build.ninja @@ -106,7 +117,11 @@ std::string read_schedule(const std::filesystem::path& ninjaPath) { auto rest = value.substr(semi + 1); constexpr std::string_view schedPrefix = "schedule="; if (!rest.starts_with(schedPrefix)) return {}; - return std::string(rest.substr(schedPrefix.size())); + rest = rest.substr(schedPrefix.size()); + // Cut at the next field: the selection rides the same line. + if (const auto next = rest.find(';'); next != std::string_view::npos) + rest = rest.substr(0, next); + return std::string(rest); } return {}; } @@ -122,8 +137,38 @@ std::string read_schedule(const std::filesystem::path& ninjaPath) { // it lands in a different build directory: a graph written under one schedule // is structurally unreachable from a build configured with another. The tag on // the line is then for humans and for `mcpp explain`, not for invalidation. +// The device-variant selection this graph was written under: "default" for +// `[build] accel` as the manifest states it, "override" for a `--accel` or +// `--no-accel` build. Empty when the file predates the field, which callers +// treat as a miss for the reason read_shape gives. +std::string read_accel_selection(const std::filesystem::path& ninjaPath) { + std::ifstream input(ninjaPath); + if (!input) return {}; + std::string line; + for (int i = 0; i < 8 && std::getline(input, line); ++i) { + constexpr std::string_view prefix = "# mcpp:graph="; + if (!line.starts_with(prefix)) continue; + auto value = std::string_view(line).substr(prefix.size()); + while (!value.empty() && (value.back() == '\r' || value.back() == ' ')) + value.remove_suffix(1); + constexpr std::string_view key = ";accel="; + const auto at = value.find(key); + if (at == std::string_view::npos) return {}; + auto rest = value.substr(at + key.size()); + if (const auto semi = rest.find(';'); semi != std::string_view::npos) + rest = rest.substr(0, semi); + return std::string(rest); + } + return {}; +} + +// A graph the fast paths may replay: the package's own targets, and the +// device variant the manifest names rather than one a flag chose. Both fast +// paths run only when no override is present, so a graph an override wrote is +// never the graph a plain build would produce. bool is_plain_build_graph(const std::filesystem::path& ninjaPath) { - return read_shape(ninjaPath) == GraphShape::Normal; + return read_shape(ninjaPath) == GraphShape::Normal + && read_accel_selection(ninjaPath) == "default"; } } // namespace mcpp::build diff --git a/src/build/hostprogram.cppm b/src/build/hostprogram.cppm index d3d4cb4a..6e22eae8 100644 --- a/src/build/hostprogram.cppm +++ b/src/build/hostprogram.cppm @@ -57,18 +57,18 @@ inline void include_dir_after(const char* dir) { std::printf("mcpp:include-di // carries one value per line. The artifact path is appended by mcpp, or // substituted for a `{}` token if one is present. // -// ⚠️ Emit the executable as an ABSOLUTE path. A bare name resolves through +// Emit the executable as an ABSOLUTE path. A bare name resolves through // PATH to a shim that dispatches against its OWNER home, which is not // necessarily the home this build uses; measured in CI as // `xlings: 'qemu-system-riscv64' is not installed` from a job where the same // bare name had answered `--version` two steps earlier. `xpkg_dir()` is how a // package finds the payload it declared. // -// ⚠️ Exactly one dependency may supply this. Two board-support packages both +// Exactly one dependency may supply this. Two board-support packages both // claiming to know how to run the artifact is a configuration error, and mcpp // reports it naming both rather than merging them. inline void runner(const char* token) { std::printf("mcpp:runner=%s\n", token); } -// ⭐ A NAMED way of reaching the artefact. The engine learns the name from +// A NAMED way of reaching the artefact. The engine learns the name from // here and knows nothing else about it, so `flash`, `serve`, `deploy`, // `submit` and `logcat` cost the same: nothing. // @@ -78,7 +78,7 @@ inline void runner(const char* name, const char* token) { std::printf("mcpp:runner-named=%s:%s\n", name, token); } // This named runner has no natural end — a console monitor, a debug server. -// ⚠️ DECLARED RATHER THAN DERIVED FROM THE NAME: the engine has no list of +// DECLARED RATHER THAN DERIVED FROM THE NAME: the engine has no list of // names to derive it from, which is the point. inline void runner_longlived(const char* name) { std::printf("mcpp:runner-longlived=%s\n", name); @@ -87,7 +87,7 @@ inline void run_exclusive() { std::printf("mcpp:run-exclusive // Say something to the user and keep going. // -// ⚠️ THIS IS THE ONLY WAY A BUILD PROGRAM CAN SUCCEED AND STILL BE HEARD. +// THIS IS THE ONLY WAY A BUILD PROGRAM CAN SUCCEED AND STILL BE HEARD. // mcpp prints what it captured from a build program only when that program // EXITS NON-ZERO, so a `std::printf` or `std::fprintf(stderr, ...)` note is // invisible on precisely the successful builds that needed it. @@ -97,7 +97,7 @@ inline void run_exclusive() { std::printf("mcpp:run-exclusive // nothing that depends on it". Do not use it for an error: exit non-zero // instead, and the output is printed already. // -// ⚠️ It survives the build cache. A cached run does not re-execute the +// It survives the build cache. A cached run does not re-execute the // program, and an advisory that appeared once and then vanished would read as // "resolved". mcpp replays it on every hit. inline void warning(const char* message) { std::printf("mcpp:warning=%s\n", message); } @@ -116,7 +116,7 @@ inline void warning(const char* message) { std::printf("mcpp:warning=%s // `version-floor-unmet`). A floor nobody stated a fact for is silent: not // knowing is not failing. // -// ⚠️ A fact is persisted with the program's other output and replayed on a +// A fact is persisted with the program's other output and replayed on a // cache hit. State what would change it -- `rerun_if_changed` on the library // the version was read from -- or the fact outlives the machine it described. inline void fact(const char* name, const char* version) { @@ -255,7 +255,7 @@ inline const char* out_dir() { return env_or("MCPP_OUT_DIR" // Where the TOOLCHAIN mcpp resolved for this build lives — the payload root, // the directory whose `bin/` holds the driver. // -// ⚠️ This exists so a package never has to DECLARE a toolchain. A package that +// This exists so a package never has to DECLARE a toolchain. A package that // needs headers the toolchain ships (libc++'s, for a freestanding standard // library subset) previously had to put `xim:llvm` in `[xlings] deps`, which // pinned it to one implementation — and the measured fact is that the same @@ -267,13 +267,13 @@ inline const char* toolchain_dir() { return env_or("MCPP_TOOLCHAI // The two flags mcpp passes to its own compiler: the `--sysroot` and the // directory it names with `-B`. Either is empty when mcpp passes none. // -// ⭐ These are for A SECOND COMPILER — one this rule package runs and mcpp did +// These are for A SECOND COMPILER — one this rule package runs and mcpp did // not resolve. Such a compiler starts with no idea where anything is, and on a // subos the C library is not at `/usr/include` and the assembler is not at // `/usr/bin`; the first `#include` it reaches then fails on `features.h`. // Forwarding these two makes it see what mcpp's own compiler sees. // -// ⚠️ NOT `sysroot_dir()`, four lines down. That one answers a question about +// NOT `sysroot_dir()`, four lines down. That one answers a question about // the TARGET's tier and is empty on a hosted target, which is exactly the case // this pair exists for. inline const char* toolchain_sysroot() { return env_or("MCPP_TOOLCHAIN_SYSROOT"); } @@ -281,7 +281,7 @@ inline const char* toolchain_binutils_dir() { return env_or("MCPP_TOOLCHAI // Which compiler resolved: "gcc", "clang", "msvc", or "" if none did. // -// ⭐ Ask this rather than inferring it from `toolchain_dir()`. The two questions +// Ask this rather than inferring it from `toolchain_dir()`. The two questions // a package has actually needed it for are which runtime library holds the // routines the compiler emits calls to, and which spelling of a binutils tool // exists beside the driver — and both have a different right answer per family @@ -299,7 +299,7 @@ inline const char* sysroot_dir() { return env_or("MCPP_TARGET_S // ── Three answers a board-support package would otherwise hardcode ─────────── // -// ⚠️ The coupling these remove does not appear in any manifest. A board package +// The coupling these remove does not appear in any manifest. A board package // can declare no dependency on LLVM and none on picolibc — and still be unable // to serve a second toolchain or a second C library, because it wrote their // names into its `build.mcpp`. A declared dependency is visible and reviewable; @@ -658,7 +658,7 @@ build_host_module(const fs::path& bdir, const fs::path& compiler, // The interface's LANGUAGE, stated rather than inferred from its extension. // - // ⚠️ Measured on macOS CI: a rule package whose lib root is `rulepkg.ixx` + // Measured on macOS CI: a rule package whose lib root is `rulepkg.ixx` // made `clang++ --precompile rulepkg.ixx -o rulepkg.pcm` EXIT 0 AND WRITE // NOTHING — clang's driver does not recognise `.ixx`, so it treated the file // as a linker input, warned that it was unused, and succeeded. The failure diff --git a/src/build/linkage_form.cppm b/src/build/linkage_form.cppm index c14f7536..b28e34d9 100644 --- a/src/build/linkage_form.cppm +++ b/src/build/linkage_form.cppm @@ -91,7 +91,7 @@ struct PackageFacts { // the only copy in the process"), because that is the only reason anyone // has ever written it — a library another library will `dlopen`. // - // ⚠️ The mirror image is NOT true: `kind = "lib"` is the parser's DEFAULT, + // The mirror image is NOT true: `kind = "lib"` is the parser's DEFAULT, // written by 84 of 130 packages in mcpp-index as boilerplate. Reading it // as "must be static" would freeze the entire ecosystem out of this axis. // Absence of a constraint is not a constraint. @@ -158,7 +158,7 @@ Resolution resolve(const PackageFacts& package, const Admissible& admissible, // The one derivation of "does this build need position-independent code". // -// ⚠️ It used to be a scan of the finished plan for a shared link unit, in +// It used to be a scan of the finished plan for a shared link unit, in // `flags.cppm`, and it was ABSENT FROM THE CACHE KEY. That was survivable // while a package's form was fixed by its author; it stops being survivable // the moment a consumer can ask for the shared form, because the same cache diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 7f9a160c..fe1402b7 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -80,7 +80,7 @@ std::optional check_rule_commands_name_a_program( // Advice appended to a failed build whose linker output names a replaceable // function nothing in the graph defines. Empty when there is nothing to add. // -// ⚠️ The case this exists for reads as a toolchain fault and is not one. A +// The case this exists for reads as a toolchain fault and is not one. A // freestanding target has no compiled `libc++`, so `operator new` is simply // absent; the moment a project uses `std::vector` the link fails naming a // mangled symbol from a header deep inside the standard library, and nothing @@ -234,7 +234,7 @@ std::string join_flags(const std::vector& flags) { // every distributed dylib. `@rpath/` is the only default that travels. // A PE link flag, spelled for the TARGET ABI and wrapped for the driver. // -// ⚠️ NOT a dialect-table entry, and Windows CI is why. Clang targeting the MSVC +// NOT a dialect-table entry, and Windows CI is why. Clang targeting the MSVC // ABI speaks the GNU DIALECT while driving lld-link, so a dialect-keyed spelling // handed it `-Wl,--out-implib,` and lld-link answered `warning: ignoring unknown // argument '--out-implib'` followed by `could not open '…lib': no such file`. @@ -474,7 +474,7 @@ std::string link_failure_advice(std::string_view output) { if (output.find(n) != std::string_view::npos) { missingNew = true; break; } if (!missingNew) return {}; - // ⚠️ NOT a version literal. The advice names a package and a feature, and + // NOT a version literal. The advice names a package and a feature, and // the feature is what pulls the implementation — so this line stays correct // across every release of that package. The version-bearing advice in the // `import std` message needed an e2e to keep it honest (tests/e2e/135); @@ -576,7 +576,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { // #407: the graph declares which mode produced it, because three modes // write this one file and the fast path has to know what it is about to // replay. Must stay within the first few lines — see read_shape. - append(mcpp::build::header_line(plan.graphShape, plan.scheduleTag) + "\n"); + append(mcpp::build::header_line(plan.graphShape, plan.scheduleTag, + plan.accelOverridden) + "\n"); append("ninja_required_version = 1.11\n\n"); // All compile/link flags are computed once via flags.cppm. @@ -643,7 +644,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // // /bin/sh: 1: -shared: not found // - // ⚠️ THIS IS NOT THE FIX FOR THAT, and must not be mistaken for one. It + // THIS IS NOT THE FIX FOR THAT, and must not be mistaken for one. It // makes the message name the linker instead of the shell; the defect is // that a link unit had no inputs, and `prepare.cppm` refuses that now. On // its own this line would only make the shared case quieter — and the @@ -965,7 +966,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { " rspfile_content = $cxx $local_includes $cxxflags $unit_cxxflags{}{} {} $in {}$obj_out\n", module_output_flag, module_src_flags, dial.compileOnly, dial.outputObjPrefix)); - // ⚠️ THE SUPERVISOR OUTLIVES THIS EDGE, THE RSPFILE DOES NOT. + // THE SUPERVISOR OUTLIVES THIS EDGE, THE RSPFILE DOES NOT. // // ninja DELETES `$out.cmd` as soon as the edge finishes, and the edge // finishes when `bmi-compile` returns — which is the moment the BMI is @@ -1215,7 +1216,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { "$in $ldflags $unit_ldflags", "SHARED"); } else if (!flags.ldDriver.empty()) { - // ⚠️ THE LINKER, NOT THE DRIVER, AND THE OBJECTS COME LAST. + // THE LINKER, NOT THE DRIVER, AND THE OBJECTS COME LAST. // // A compiler driver accepts objects anywhere on the line and sorts // them out; `ld` resolves left to right, so a library named before @@ -1370,7 +1371,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // TestBinary and SharedLibrary — so a link unit with no `import std` // anywhere in it still got the module's global initialiser linked in. // - // ⚠️ THE TEST HAS TO BE TRANSITIVE. A unit that never writes `import std` + // THE TEST HAS TO BE TRANSITIVE. A unit that never writes `import std` // itself still needs the initialiser when a module it imports does; asking // only about the unit's own source is the same "the edge exists but nobody // depends on it" mistake as #405. So: reachability over the module graph. @@ -1413,7 +1414,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // mcpp#426: does this link unit contain any C++ at all? // - // ⚠️ THE UNKNOWN CASE IS THE OPPOSITE OF `unit_needs_std`'s. That one treats + // THE UNKNOWN CASE IS THE OPPOSITE OF `unit_needs_std`'s. That one treats // an object it cannot find as "does not need std" — a missing `std.o` is // recoverable. Here a wrong answer is an undefined-symbol link failure, and // objects produced by an `action` (prepare_actions) are NOT in @@ -1605,7 +1606,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // placeholder `prepare_actions` leaves behind. Five consecutive builds, // same result — not a race, never run. // - // ⭐ PER PACKAGE, not per build. `include_dir` colours only the declaring + // PER PACKAGE, not per build. `include_dir` colours only the declaring // package's own TUs (docs/07-build-mcpp.md), so a generated header is // visible to exactly one package and a build-wide phony would encode a // dependency that does not exist. It would also land on the critical path @@ -1684,7 +1685,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // per-edge — telling a plain `.cpp` it is `c++-module` would make // Clang scan an implementation unit as an interface. // - // ⚠️ The value is written WITHOUT its leading space and the + // The value is written WITHOUT its leading space and the // separator lives in the rule's command string above. Ninja // strips leading/trailing whitespace from a variable VALUE, so // `-MF $out.dep$unit_lang` concatenated into @@ -1798,7 +1799,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // The join. THE SOURCE IS AN INPUT, and the BMI is only an // implicit one — the reverse of what this was. // - // ⚠️ WITH THE BMI AS THE ONLY INPUT THIS EDGE GETS CLEANED + // WITH THE BMI AS THE ONLY INPUT THIS EDGE GETS CLEANED // BY THE VERY OPTIMISATION IT IS PART OF. The BMI edge sets // `restat = 1`, and when the new BMI turns out equivalent // `settle_bmi` puts the previous file back so its mtime does @@ -1999,7 +2000,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // either of them IS a C++ unit — asserted rather than assumed, because // the two predicates are computed independently. const bool takesStd = cxxUnit && has_std_artifacts && unit_needs_std(lu); - // ⚠️ `std.compat.o` had no `unit_needs_std` narrowing at all: mcpp#416 + // `std.compat.o` had no `unit_needs_std` narrowing at all: mcpp#416 // fixed `std.o` and left its neighbour unconditional, which put a C++ // TU's global initialiser into every link unit whose toolchain merely // HAD a prebuilt std.compat. Same predicate, same reason. @@ -2128,7 +2129,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // in every graph that builds a static library. if (lu.kind != LinkUnit::StaticLibrary) tail.runtimeFallback = flags.ldRuntimeFallback; - // ⚠️ NOT ON A DIRECT LINK. The tag selects between `DT_RPATH` + // NOT ON A DIRECT LINK. The tag selects between `DT_RPATH` // and `DT_RUNPATH`, entries of a dynamic section; an image with no // loader has neither, and `ld.lld` rejects the flag's `-Wl,` form // outright. @@ -2136,7 +2137,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // The link map — per unit, because it is named after the artifact. if (!fsObjcopy.empty() && (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary)) { - // ⚠️ `-Map=` OR `-Wl,-Map=`, DECIDED BY WHO IS BEING SPOKEN TO. + // `-Map=` OR `-Wl,-Map=`, DECIDED BY WHO IS BEING SPOKEN TO. // With `ldDriver` set the link is `ld.lld` itself, and `-Wl,` // is a driver's way of saying "pass this on" — handed to the // linker it is an unknown option, on the one target where the @@ -2203,7 +2204,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // action phony as an order-only prerequisite (see // `order_only_for`, far above). // - // ⚠️ THIS COMMENT USED TO CLAIM THE FIRST ROW FOR ALL FOUR. "A Source + // THIS COMMENT USED TO CLAIM THE FIRST ROW FOR ALL FOUR. "A Source // action's outputs ARE the compile edge's inputs" is true of a generated // `.cpp` and false of a generated `.h`: `adoptActionOutputs` deliberately // does not adopt a non-TU into the compile set, so nothing consumed the @@ -2307,7 +2308,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // that produce their members, purely for readability — ninja resolves the // whole manifest before building anything. // - // ⚠️ A Source action's outputs enter the graph ONLY through this edge. + // A Source action's outputs enter the graph ONLY through this edge. // They are deliberately not in `default` (above), because being reachable // two ways is how the 0.0.104 soname aliases went missing under explicit // goals. So if this loop stops emitting, the header-only case of mcpp#534 @@ -2383,7 +2384,7 @@ std::string append_goal_phony(std::string& manifest, // Every compile edge of a package that generates its own inputs must wait for // them (mcpp#534). // -// ⭐ WHY A SCAN AND NOT CARE. The order-only string is appended at SEVEN call +// WHY A SCAN AND NOT CARE. The order-only string is appended at SEVEN call // sites — the scan edge, three dyndep object edges, two static-mode object // edges and the asm edge — and an eighth added later without it reintroduces // the defect for that edge kind, silently, in exactly the shape that took a @@ -2396,7 +2397,7 @@ std::string append_goal_phony(std::string& manifest, // `check_inline_command_lengths`, whose comment says it outright: scanning the // emitted manifest covers a new edge kind the day it is added. // -// ⚠️ THE DENOMINATOR IS PART OF THE CHECK. "every edge that should carry it +// THE DENOMINATOR IS PART OF THE CHECK. "every edge that should carry it // does" is vacuously true when no edge should, which is precisely today's // state — so a package with gating actions and zero matched compile edges is // itself the failure, not a pass. @@ -2467,7 +2468,7 @@ std::optional check_action_ordering(const std::string& manifest, // A rule's command must begin with a program. // -// ⭐ WHY THIS SHAPE, and not "no rule may reference an undefined variable". +// WHY THIS SHAPE, and not "no rule may reference an undefined variable". // ninja expands an undefined variable to the empty string, which is a FEATURE // several rules here depend on: `$soname_flag`, `$implib_flag`, `$def_flag` // and `$unit_ldflags` are set per-edge and absent on purpose everywhere else, @@ -2707,7 +2708,7 @@ std::expected NinjaBackend::build(const BuildPlan& plan "compile_commands.json was not updated: {}", cdb.error().message)); } - // ⚠️ A SHARED LIBRARY ON A TARGET WHOSE LINK IS DRIVEN BY THE LINKER, SAID + // A SHARED LIBRARY ON A TARGET WHOSE LINK IS DRIVEN BY THE LINKER, SAID // IN WORDS RATHER THAN AS A MISSING NINJA RULE. // // The direct-linker path defines `cxx_link`, `c_link` and `cxx_archive` and diff --git a/src/build/plan.cppm b/src/build/plan.cppm index b38840d9..7e1ddc95 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -189,6 +189,14 @@ struct BuildPlan { // a shared object` on a file nobody edited. bool needsPic = false; std::string scheduleTag = "none"; + // Whether `--accel` / `--no-accel` selected this graph's device variant + // over `[build] accel`. The variant is in the fingerprint, so the two + // builds land in different directories -- and the fast path, which runs + // before any plan exists, replays whichever directory was built LAST. A + // plain `mcpp build` after `mcpp build --no-accel` therefore reported + // "Finished in 0.00s" and handed back the CPU variant. The graph records + // the selection so the fast path can decline a graph an override chose. + bool accelOverridden = false; // What to hand ninja. Under detach-codegen a compiler stops holding a slot // when it publishes, so this must exceed the real compiler cap or the ready // frontier starves — see the hazard note in schedule/detach_codegen. @@ -263,7 +271,7 @@ struct BuildPlan { // LinkIntent), with one exception: SubosFarm entries have no other // producer, so `flags.cppm` renders them from here, appended last. // - // ⚠️ The farm deliberately does NOT enter `runtimeLibraryDirs`. That + // The farm deliberately does NOT enter `runtimeLibraryDirs`. That // vector becomes LD_LIBRARY_PATH for `mcpp run`, which is inherited by // every child process including host binaries — measured to kill // `xdg-open`/`notify-send` outright when a private libc is on it. The farm @@ -790,7 +798,7 @@ std::vector runtime_search_closure( // ELF only, matching the guard below: `$ORIGIN` is the ELF spelling, and a // format that gets no DT_RPATH gets no entry either. // - // ⚠️ AND ONLY WHEN SOMETHING ACTUALLY EMITS IT. `$ORIGIN` comes from + // AND ONLY WHEN SOMETHING ACTUALLY EMITS IT. `$ORIGIN` comes from // `shared_library_link_flags`, i.e. per CONSUMER of a shared library — a // project with no shared library never gets one. Recording it // unconditionally would swap this issue's asymmetry for its mirror image: diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index e33d2871..76613465 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -108,7 +108,7 @@ namespace mcpp::build { // should not fail outright, only tell the user what it ignored. inline void warn_unknown_xpkg_keys(const mcpp::manifest::Manifest& dm, std::string_view depLabel) { - // ⚠️ A LAYER NAME THIS ENGINE DOES NOT KNOW IS A VERSION GAP, NOT A TYPO, + // A LAYER NAME THIS ENGINE DOES NOT KNOW IS A VERSION GAP, NOT A TYPO, // WHEN IT ARRIVES FROM A DEPENDENCY. // // The reserved `mcpp:` prefix is a closed set so a misspelling cannot @@ -256,7 +256,7 @@ void merge_conditional_config(mcpp::manifest::Manifest& m, const bool generatedPackage = mcpp::pack::is_distribution_package(m); for (auto const& cc : m.conditionalConfigs) { - // ⚠️ THE TWO PASSES MUST BE DISJOINT, AND `matches()` ALONE DOES NOT + // THE TWO PASSES MUST BE DISJOINT, AND `matches()` ALONE DOES NOT // MAKE THEM SO. A layer key answers false here because `layersKnown` is // false — but `cfg(any(linux, c-abi = "musl"))` still matches on its // triple leg, and the second pass would match it again and `append()` @@ -271,7 +271,7 @@ void merge_conditional_config(mcpp::manifest::Manifest& m, // a broader unconditional one under GNU last-wins — which is what // makes an off-OS REMOVAL expressible (`-U` after the base `-D`). if (neutralWins) { - // ⚠️ Drop the LIBRARY REFERENCES, not the whole ldflags list. + // Drop the LIBRARY REFERENCES, not the whole ldflags list. // // Clearing it outright was a measured regression: a PE/MinGW shared // leg's ldflags also carry `-Wl,-Bdynamic`, without which `-static` @@ -527,7 +527,7 @@ export enum class TcOrigin { FirstRun, // chosen and persisted by this very invocation }; -// ⚠️ `GlobalDefault` IS DELIBERATELY NOT LISTED, AND THE REASON IS A MEASURED +// `GlobalDefault` IS DELIBERATELY NOT LISTED, AND THE REASON IS A MEASURED // REGRESSION RATHER THAN A JUDGEMENT ABOUT WHOSE OPINION COUNTS. // // A target row's pin does not name a preferred compiler. It names the payload @@ -552,7 +552,7 @@ export inline bool tc_origin_is_user_explicit(TcOrigin o) { return o == TcOrigin::ManifestToolchain || o == TcOrigin::TargetSection; } -// ⚠️⚠️ MAY A BUILD THAT RESOLVED THIS WAY WRITE THE MACHINE'S DEFAULT? +// MAY A BUILD THAT RESOLVED THIS WAY WRITE THE MACHINE'S DEFAULT? // // `GraphRequirement` is the one origin that must not: it is a property of a // package this project depends on, not of this machine. Two branches persist a @@ -563,7 +563,7 @@ export inline bool tc_origin_is_user_explicit(TcOrigin o) { // Windows box with no toolchain: a bare machine building ONE llvm-requiring // project would have handed llvm to every later project that asked for nothing. // -// ⭐ NAMED RATHER THAN SPELLED INLINE AT EACH SITE. There are two today; the +// NAMED RATHER THAN SPELLED INLINE AT EACH SITE. There are two today; the // third would be written by someone who never read this note, and a predicate // with a name is something they can find. export inline bool tc_origin_may_persist(TcOrigin o) { @@ -697,7 +697,7 @@ export struct BuildContext { // whether a cached build.ninja was generated for the profile being asked // for — and so `Finished ` stops being a hardcoded "release". std::string profile; - // ⭐ WHY THIS COMPILER — carried so the QUERY can answer it too. + // WHY THIS COMPILER — carried so the QUERY can answer it too. // // A build says so on its status line. `mcpp why toolchain --format json` // exists precisely to answer "what would this resolve to, and why", and a @@ -866,7 +866,7 @@ export struct BuildOverrides { // decides the `when = "run"` tool tier. `mcpp run` and `mcpp test` set it; // `mcpp build`, `mcpp pack` and every internal sub-build do not. // - // ⚠️ A SEPARATE FLAG FROM `includeDevDeps`, THOUGH `mcpp test` SETS BOTH. + // A SEPARATE FLAG FROM `includeDevDeps`, THOUGH `mcpp test` SETS BOTH. // One says which PACKAGES enter the graph, the other which TOOLS are // installed, and `mcpp run` needs the second without the first. bool will_run = false; @@ -966,7 +966,7 @@ sysroot_override(const mcpp::manifest::Manifest& m, // shape this codebase keeps paying for. A board package that got the right // answer as a root project and a stale one as a dependency would fail only in // the consuming build, which is the harder direction to debug. -// ⚠️⚠️ A NETWORK STEP OF A BUILD, RETRIED — AND IT HAD NO RETRY AT ALL. +// A NETWORK STEP OF A BUILD, RETRIED — AND IT HAD NO RETRY AT ALL. // // A dependency resolved by `git` is fetched on every machine that has not // cached it, and a transport that hiccups once failed the whole build: @@ -978,13 +978,13 @@ sysroot_override(const mcpp::manifest::Manifest& m, // looks like. Measured twice on 2026-08-23: once in continuous integration and // once locally as `TLS connect error: … unexpected eof while reading`. // -// ⚠️ THREE ATTEMPTS, AND THE LAST FAILURE IS REPORTED UNCHANGED. A wrong URL +// THREE ATTEMPTS, AND THE LAST FAILURE IS REPORTED UNCHANGED. A wrong URL // and a missing branch fail exactly as a transient fault does, so this cannot // tell them apart and does not try: a permanent failure costs three seconds and // produces the message it always did. Hiding a real error behind a retry is the // worse trade, which is why the count is small and the report is untouched. // -// ⚠️ BOTH NETWORK STEPS, not one. The first version retried only the clone — +// BOTH NETWORK STEPS, not one. The first version retried only the clone — // and a probe with a nonexistent repository failed in ONE second, because the // step that runs first is `git ls-remote` and it was still bare. A retry on // half of a path is a retry that reports success at having been added. @@ -1033,7 +1033,7 @@ void fill_target_build_env(mcpp::build::BuildProgramEnv& e, // The C LIBRARY's sub-directory for this ISA profile, from the freestanding // table — the same single read point the compile flags use. // - // ⚠️ Gated on there being a C library at all, and the gate is the point: the + // Gated on there being a C library at all, and the gate is the point: the // value is a multilib convention, so on the zero-libc tier there is nothing // for it to be a convention OF. Emitting `rv64gc/lp64d` there would hand a // kernel a path into a directory that does not exist, and the name of the @@ -1055,7 +1055,7 @@ void fill_target_build_env(mcpp::build::BuildProgramEnv& e, // ── Tool tiers: which of a manifest's declared packages this verb needs ───── // -// ⭐⭐ THE AXIS PACKAGE DEPENDENCIES HAVE HAD SINCE THE BEGINNING, AND TOOLS +// THE AXIS PACKAGE DEPENDENCIES HAVE HAD SINCE THE BEGINNING, AND TOOLS // DID NOT. // // A board-support package names an emulator (needed to run) and a debug probe @@ -1069,7 +1069,7 @@ void fill_target_build_env(mcpp::build::BuildProgramEnv& e, // spelling is the one `[dependencies]` already uses for the same kind of // refinement. // -// ⚠️ `Dev` IS THE ONLY TIER THAT DOES NOT PROPAGATE. It means "when the package +// `Dev` IS THE ONLY TIER THAT DOES NOT PROPAGATE. It means "when the package // that declared it is itself being developed", so `isRoot` decides it. Every // other tier reaches a consumer, which is the whole point of a board package // knowing its own machine. @@ -1108,7 +1108,7 @@ applicable_xlings_addresses(const mcpp::manifest::Manifest& man, // Install a set of `[xlings.workspace]` addresses, and record that the list was // done. // -// ⭐ EXTRACTED SO THE DEPENDENCY GRAPH CAN USE THE SAME PATH. This was the +// EXTRACTED SO THE DEPENDENCY GRAPH CAN USE THE SAME PATH. This was the // root project's provisioning, inline and reachable only from there. A // board-support package that declares the emulator its machine needs is // precisely the thing that should say so once, and a consumer that has to @@ -1124,7 +1124,7 @@ provision_xlings_addresses(const mcpp::config::GlobalConfig& cfg, const std::filesystem::path& legacyStampRoot, std::string_view label) { if (declaredDeps.empty()) return {}; - // ⚠️⚠️ THE STAMP RECORDS A GLOBAL EFFECT, SO IT LIVES WHERE THE + // THE STAMP RECORDS A GLOBAL EFFECT, SO IT LIVES WHERE THE // EFFECT DOES. It used to sit in `/.mcpp/`, while the // installation goes to the registry a few lines below — the // scope difference is deliberate and explained there. Two @@ -1176,7 +1176,7 @@ provision_xlings_addresses(const mcpp::config::GlobalConfig& cfg, // and a stale extra file is cheaper than a downgrade that // re-provisions on every build. // - // ⚠️ IT DOES NOT MEAN "PROVISIONED SUCCESSFULLY". The release + // IT DOES NOT MEAN "PROVISIONED SUCCESSFULLY". The release // that wrote it did not read the result — that is the defect // above — so it means only "this list was attempted". Treating // it as proof would carry the bug across the very upgrade that @@ -1196,7 +1196,7 @@ provision_xlings_addresses(const mcpp::config::GlobalConfig& cfg, }; bool needProvision = (have != want); if (needProvision) { - // ⚠️ THE AUTO-INSTALL GATE, WHICH THIS PATH DID NOT HAVE. + // THE AUTO-INSTALL GATE, WHICH THIS PATH DID NOT HAVE. // // `[toolchain]` is the precedent this whole mechanism cites // ("the same 'declare it and mcpp provisions it on first @@ -1213,7 +1213,7 @@ provision_xlings_addresses(const mcpp::config::GlobalConfig& cfg, // that would otherwise regress. if (mcpp::platform::env::offline_mode() || mcpp::platform::env::no_auto_install()) { - // ⚠️ THE ONE PLACE THE LEGACY STAMP IS TRUSTED, and the + // THE ONE PLACE THE LEGACY STAMP IS TRUSTED, and the // reason is that relocating a record must not refuse a // build that worked yesterday. Every project that had // already provisioned carries the old stamp and no new @@ -1294,7 +1294,7 @@ provision_xlings_addresses(const mcpp::config::GlobalConfig& cfg, auto r = mcpp::xlings::call( mcpp::config::make_xlings_env(cfg), "install_packages", args.dump(), &progress); - // ⚠️⚠️ `if (!r)` IS NOT THE FAILURE TEST, AND TESTING ONLY + // `if (!r)` IS NOT THE FAILURE TEST, AND TESTING ONLY // IT MADE THIS PATH REPORT SUCCESS FOR EVERY FAILURE XLINGS // CAN REPORT. // @@ -1388,7 +1388,7 @@ prepare_build(bool print_fingerprint, // learned by experiment — writing the same value a second time in // `[target.]` and observing that it works. std::string pinReplacedDefault; - // ⭐ THE PACKAGE WHOSE `requires` CHOSE THE COMPILER, AND WHAT IT ASKED FOR. + // THE PACKAGE WHOSE `requires` CHOSE THE COMPILER, AND WHAT IT ASKED FOR. // // Non-empty only when the graph's requirement actually changed the answer. // Reported on the status line for the same reason `pinReplacedDefault` is: @@ -1406,7 +1406,7 @@ prepare_build(bool print_fingerprint, // The target row's toolchain convention, held until the graph is known. // Empty when the row names none or the project named its own. std::string targetPinCandidate; - // ⭐⭐ AND WHETHER THAT PIN IS A CONVENTION OR A CAPABILITY, RECORDED AT + // AND WHETHER THAT PIN IS A CONVENTION OR A CAPABILITY, RECORDED AT // THE SAME READ. // // A hosted row's pin answers "which payload supplies this target's C @@ -1419,7 +1419,7 @@ prepare_build(bool print_fingerprint, // Taken here rather than re-derived at the decision point, because the row // is read exactly once and both facts come out of that read. bool targetPinIsCapability = false; - // ⚠️ THE ROW'S PIN, KEPT EVEN WHEN THE PROJECT NAMED ITS OWN COMPILER — + // THE ROW'S PIN, KEPT EVEN WHEN THE PROJECT NAMED ITS OWN COMPILER — // which is exactly when `targetPinCandidate` above is left empty. // // The candidate answers "should mcpp apply its convention"; this answers @@ -1471,7 +1471,7 @@ prepare_build(bool print_fingerprint, m = mcpp::manifest::load(*root / "mcpp.toml", {.insideWorkspace = true}); if (!m) return std::unexpected(m.error().format()); - // ⚠️ AND ONLY FOR THE ROOT. A layer name this engine does not know is a + // AND ONLY FOR THE ROOT. A layer name this engine does not know is a // typo in the manifest the author is looking at, and a version gap in a // dependency's. The reserved `mcpp:` prefix exists so the first is an error // rather than a silently disabled behaviour; refusing the second as well @@ -1651,7 +1651,7 @@ prepare_build(bool print_fingerprint, // #540: a cfg() predicate mcpp cannot evaluate must say so. // - // ⚠️ A PREDICATE THAT ANSWERS FALSE AND A PREDICATE THAT WAS NEVER + // A PREDICATE THAT ANSWERS FALSE AND A PREDICATE THAT WAS NEVER // UNDERSTOOD USED TO READ THE SAME. `cfgpred` returns false for an unknown // key and for an unknown bareword, and a `[target..build]` section // whose predicate is false is dropped without a word — so a typo, and every @@ -1848,7 +1848,7 @@ prepare_build(bool print_fingerprint, if (!runtimeBindingSnapshot.note.empty()) mcpp::ui::info("Runtime", runtimeBindingSnapshot.note); } - // ⭐⭐ THE `bin` THIS PROJECT'S BUILD PROGRAMS SEE FIRST — derived ONCE, + // THE `bin` THIS PROJECT'S BUILD PROGRAMS SEE FIRST — derived ONCE, // here, from the selection that has just been resolved. // // Empty unless the manifest declared `[xlings].subos`. That is deliberate: @@ -1857,7 +1857,7 @@ prepare_build(bool print_fingerprint, // that has not asked for an environment of its own gets the `PATH` mcpp // was started with, byte for byte. // - // ⚠️ NOT RE-DERIVED AT THE TWO DELIVERY SITES BELOW, AND NOT FROM + // NOT RE-DERIVED AT THE TWO DELIVERY SITES BELOW, AND NOT FROM // `[xlings] deps`. `mcpp::xlings::runtime` is the sole runtime-selection // policy and `RuntimeBinding::subosDir` is its resolved answer; a second // derivation is how a build ends up with two subos and no way to say which @@ -2059,7 +2059,7 @@ prepare_build(bool print_fingerprint, // override and the vocabulary-table convention (pin + default linkage). if (!overrides.target_triple.empty()) { namespace triple = mcpp::toolchain::triple; - // ⚠️ THE SPELLING THE PROJECT WROTE, KEPT FOR EVERY DIAGNOSTIC BELOW. + // THE SPELLING THE PROJECT WROTE, KEPT FOR EVERY DIAGNOSTIC BELOW. // `overrides.target_triple` is canonicalised further down, and until // this variable existed the refusals quoted the canonical form: // `--target aarch64-linux` produced "target 'aarch64-linux-gnu' is @@ -2068,7 +2068,7 @@ prepare_build(bool print_fingerprint, const std::string requestedSpelling = overrides.target_triple; auto parsed = triple::parse(overrides.target_triple); - // ⚠️⚠️ THE REQUEST IS COMPLETED FROM THE VOCABULARY BEFORE ANYTHING + // THE REQUEST IS COMPLETED FROM THE VOCABULARY BEFORE ANYTHING // READS IT, AND THE ORDER RELATIVE TO THE `[target.X]` LOOKUP IS PART // OF THE CONTRACT. // @@ -2130,7 +2130,7 @@ prepare_build(bool print_fingerprint, requestedSpelling, opts)); } if (!known && !hasExplicitSection) { - // ⚠️ "UNKNOWN" IS A CLAIM ABOUT THE VOCABULARY, AND IT WAS FALSE FOR + // "UNKNOWN" IS A CLAIM ABOUT THE VOCABULARY, AND IT WAS FALSE FOR // A WHOLE arch+os FAMILY. // // Measured on 2026.8.26.1: `--target riscv64-linux` reported @@ -2250,7 +2250,7 @@ prepare_build(bool print_fingerprint, servable.empty() ? "(nothing — `mcpp toolchain list`)" : servable, parsed->str()); } - // ⚠️ CAPTURED BEFORE CANONICALISATION, BECAUSE CANONICALISATION IS + // CAPTURED BEFORE CANONICALISATION, BECAUSE CANONICALISATION IS // EXACTLY WHAT DESTROYS IT. // // `str()` renders the filled-in identity, so `x86_64-linux` becomes @@ -2258,7 +2258,7 @@ prepare_build(bool print_fingerprint, // an env segment the project never wrote. The request has to be taken // from the ONLY triple that still knows the difference: this one. if (parsed && parsed->envExplicit) requestedCAbi = parsed->env; - // ⚠️ AND THE SPELLING THE PROJECT USED, FOR THE REPORT ONLY. + // AND THE SPELLING THE PROJECT USED, FOR THE REPORT ONLY. // // The canonical form is the identity — the output directory, the cache // key, the subject of a `cfg()` — and it must stay filled. The REPORT is @@ -2318,7 +2318,7 @@ prepare_build(bool print_fingerprint, // a project does not use. The narrower reading of this guard was // patched with an openkal-specific exception; stating the rule // correctly removes the need for one. - // ⚠️ RECORDED, NOT APPLIED. The convention answers "which payload + // RECORDED, NOT APPLIED. The convention answers "which payload // supplies this target's C library", and whether it is needed depends on // whether the dependency graph supplies one instead. That is knowable // only after resolution, so the decision waits for @@ -2333,7 +2333,7 @@ prepare_build(bool print_fingerprint, targetPinCandidate = std::string(known->pin); targetPinIsCapability = parsed && parsed->pin_is_capability(); } - // ⚠️⚠️ A USER'S EXPLICIT TOOLCHAIN OVERRIDES A CONVENTION, NOT A + // A USER'S EXPLICIT TOOLCHAIN OVERRIDES A CONVENTION, NOT A // CAPABILITY — AND UNTIL THIS LINE IT OVERRODE BOTH. // // The block above deliberately steps aside for an explicit @@ -2345,7 +2345,7 @@ prepare_build(bool print_fingerprint, // construction". A host g++ does not emit riscv64 whatever anyone // declares. // - // ⚠️ Measured 2026-08-26: + // Measured 2026-08-26: // // [toolchain] default = "gcc@16.1.0" // $ mcpp build --target riscv64-none-elf @@ -2359,7 +2359,7 @@ prepare_build(bool print_fingerprint, && tc_origin_is_user_explicit(tcOrigin) && tcSpec.has_value()) { auto declared = mcpp::toolchain::parse_toolchain_spec(*tcSpec); if (declared && declared->family != mcpp::toolchain::Family::Llvm) { - // ⚠️ THE REASON TRAVELS WITH THE ROW. Both rows refuse for the + // THE REASON TRAVELS WITH THE ROW. Both rows refuse for the // same rule and NOT for the same reason, and one sentence // covering both would be wrong about one of them: a PE+musl // target is not bare metal, and a reader told it is stops @@ -2411,7 +2411,7 @@ prepare_build(bool print_fingerprint, // at call time rather than captured: a `[target.'cfg(...)'.build]` section // may set `accel`, and the merge that applies it runs a few lines down. // - // ⚠️⚠️ "NO ACCELERATOR" IS THE EMPTY STRING HERE, NOT `accel_str`'s "(none)". + // "NO ACCELERATOR" IS THE EMPTY STRING HERE, NOT `accel_str`'s "(none)". // // `accel_str` is a DISPLAY function: it prints `(none)` for an empty set so // an ABI tag reads as a sentence. Handing that spelling on as a value made @@ -2476,7 +2476,7 @@ prepare_build(bool print_fingerprint, // Studio. `Origin::Managed` is everything else, including a VERSIONED // msvc spec, and that is the point: what the manifest says is what gets // used, on every machine, instead of whatever this one happens to have. - // ⚠️ RESOLVED HERE, RUN AFTER THE DEPENDENCY GRAPH — AND THE SPLIT IS THE + // RESOLVED HERE, RUN AFTER THE DEPENDENCY GRAPH — AND THE SPLIT IS THE // WHOLE POINT. // // A target row's convention does not name a preferred compiler. It names @@ -2492,11 +2492,11 @@ prepare_build(bool print_fingerprint, // one, because clang alone carries no C runtime for `x86_64-windows-gnu` // while the payload the row names does. // - // ⚠️ The body does not MOVE; only its execution does. Everything between + // The body does not MOVE; only its execution does. Everything between // here and the call site was measured to read `tc` exactly once, and that // one read wanted the target triple rather than the compiler. std::optional tc; - // ⚠️ `std::function` AND NOT `auto`, BECAUSE THE FIRST-RUN BRANCH INSIDE + // `std::function` AND NOT `auto`, BECAUSE THE FIRST-RUN BRANCH INSIDE // CALLS BACK INTO IT. That branch installs a host default and then has to // resolve THAT default for the requested target — which is what the top of // this same function does. Recursing reuses it; writing it a second time @@ -2522,7 +2522,7 @@ prepare_build(bool print_fingerprint, parsedSpec = std::move(*s); tcOriginAxis = mcpp::toolchain::origin_of(*parsedSpec); } - // ⚠️ ASSIGNED, NOT DECLARED. `host_tc_for_build_program` reads it and is + // ASSIGNED, NOT DECLARED. `host_tc_for_build_program` reads it and is // defined outside this lambda, so the declaration lives in the enclosing // scope; the value is still decided here, where the spec is parsed. tcSpecIsMsvc = @@ -2559,7 +2559,7 @@ prepare_build(bool print_fingerprint, } auto pkg = mcpp::toolchain::to_xim_package(*spec); - // ⚠️⚠️ AND NOT INSTALLED WHEN NO PAYLOAD HERE COULD SERVE THE TARGET. + // AND NOT INSTALLED WHEN NO PAYLOAD HERE COULD SERVE THE TARGET. // // `unservedTargetDiagnosis` is decided a thousand lines above and // released a thousand lines below — deliberately, because whether the @@ -2569,7 +2569,7 @@ prepare_build(bool print_fingerprint, // graph supplies the system (and this payload is not wanted) or the // build refuses later (and it is not wanted then either). // - // ⚠️ Measured on ubuntu-24.04-arm, `--target x86_64-linux-musl`: + // Measured on ubuntu-24.04-arm, `--target x86_64-linux-musl`: // // error: toolchain 'gcc@16.1.0': xlings install of // 'xim:x86_64-linux-musl-gcc@16.1.0' failed … @@ -2578,7 +2578,7 @@ prepare_build(bool print_fingerprint, // x86_64-only. The refusal that names this correctly never ran, because // the install failed first and failed hard. // - // ⭐ Skipping leaves BOTH later paths intact; attempting cannot help + // Skipping leaves BOTH later paths intact; attempting cannot help // either of them. const bool targetPayloadUnservable = !unservedTargetDiagnosis.empty() && !spec->target.empty(); @@ -2650,13 +2650,13 @@ prepare_build(bool print_fingerprint, // Canonical rendering, whatever spelling the manifest/config used: // "Resolved gcc@16.1.0 → x86_64-linux-musl → ". // - // ⚠️ AND IT SAYS SO WHEN MCPP CHOSE. A toolchain the user wrote down + // AND IT SAYS SO WHEN MCPP CHOSE. A toolchain the user wrote down // needs no explanation — they can read their own manifest. One this // engine selected from a target row is a decision the user did not // make, and a status line that reports the outcome without the // reason leaves them to discover the rule by experiment. std::string chosenBy; - // ⭐ A COMPILER THE GRAPH ASKED FOR IS ANNOUNCED WITH THE PACKAGE + // A COMPILER THE GRAPH ASKED FOR IS ANNOUNCED WITH THE PACKAGE // THAT ASKED. Without the name this reads as mcpp ignoring the // user's default; with it, it reads as the dependency it is. // The second line appears only when something was displaced — @@ -2885,7 +2885,7 @@ prepare_build(bool print_fingerprint, tcSpec = defaultSpec; tcOrigin = TcOrigin::FirstRun; - // ⭐⭐ AND IF A TARGET WAS ASKED FOR, RESOLVE FOR IT — THIS BRANCH JUST + // AND IF A TARGET WAS ASKED FOR, RESOLVE FOR IT — THIS BRANCH JUST // INSTALLED A HOST COMPILER AND WAS ABOUT TO BUILD WITH IT. // // Everything above answers "this machine has no toolchain, give it @@ -2902,12 +2902,12 @@ prepare_build(bool print_fingerprint, // // Resolved gcc@16.1.0 → x86_64-windows-gnu → …/mingw-cross-gcc/… // - // ⭐ REUSES THE PATH THAT ALREADY KNOWS HOW, rather than repeating what + // REUSES THE PATH THAT ALREADY KNOWS HOW, rather than repeating what // it does. `resolve_target_toolchain` maps a spec plus a target onto a // payload and installs it; the default just chosen is the spec. A // second implementation here would be a second answer to one question, // which is the shape this release exists to remove. - // ⚠️ RECORDED HERE, ACTED ON BELOW — the Windows first-run block that + // RECORDED HERE, ACTED ON BELOW — the Windows first-run block that // follows SETS `overrides.target_triple` itself, and returning from // here would skip it. Its own comment says why that matters: it // persists BOTH axes, and persisting only the target leaves @@ -2921,7 +2921,7 @@ prepare_build(bool print_fingerprint, // Persisting only the target would leave the toolchain axis implicit // (derived from the vocabulary pin) and the two views would disagree. // - // ⚠️⚠️ NOT WHEN THE DEPENDENCY GRAPH SUPPLIED THE ANSWER. This branch's + // NOT WHEN THE DEPENDENCY GRAPH SUPPLIED THE ANSWER. This branch's // condition is `tcSpec.has_value()`, and since 2026.8.26.2 a package's // `requires = ["mcpp:compiler=…"]` can be what made it true — so a bare // Windows box building ONE project with an llvm-requiring dependency @@ -2948,7 +2948,7 @@ prepare_build(bool print_fingerprint, tcOrigin = TcOrigin::FirstRun; } - // ⭐⭐ AND NOW RESOLVE FOR THE TARGET, IF ONE WAS ASKED FOR. + // AND NOW RESOLVE FOR THE TARGET, IF ONE WAS ASKED FOR. // // The first-run branch above answers "this machine has no toolchain, give // it one", and the answer is a HOST payload; `--target` was never read @@ -2964,12 +2964,12 @@ prepare_build(bool print_fingerprint, // // Resolved gcc@16.1.0 → x86_64-windows-gnu → …/mingw-cross-gcc/… // - // ⭐ REUSES THE PATH THAT ALREADY KNOWS HOW rather than repeating it. The + // REUSES THE PATH THAT ALREADY KNOWS HOW rather than repeating it. The // default just chosen is the spec; mapping a spec plus a target onto a // payload (installing it if absent — `autoInstall` was always true there) // is what the top of this function does. Depth is one: the second pass // takes the `tcSpec.has_value()` branch the first run just made true. - // ⚠️⚠️ ONE-SHOT, AND THE FLAG IS SET BEFORE THE CALL, NOT AFTER. + // ONE-SHOT, AND THE FLAG IS SET BEFORE THE CALL, NOT AFTER. // // This line sits OUTSIDE the first-run branch — it has to, because the // Windows block just above sets the target itself — so it is evaluated on @@ -3026,20 +3026,20 @@ prepare_build(bool print_fingerprint, // `tc.targetTriple`, so correcting it here corrects all of them at once — // which is the point of there being one field rather than five answers. // - // ⚠️ THIS USED TO BE SCOPED TO FREESTANDING, WITH THIS REASON: + // THIS USED TO BE SCOPED TO FREESTANDING, WITH THIS REASON: // // The hosted cross targets already resolve a per-target binary, and // overwriting their probed triple would replace a measured fact with // an assumed one for no gain. // - // ⭐⭐ That was true while every hosted cross was served by a payload. It + // That was true while every hosted cross was served by a payload. It // stops being true when the TARGET SIDE comes from the dependency graph: // the C library, the C++ runtime and the platform's own implementation are // then packages built from source, and the compiler is an ordinary clang — // whose `-dumpmachine` answers the host, exactly as the paragraph above // describes for freestanding. // - // ⚠️ Measured 2026-08-23, with an explicit `[target.aarch64-macos] + // Measured 2026-08-23, with an explicit `[target.aarch64-macos] // toolchain = "llvm@…"`. The manifest's cfg evaluation used the REQUESTED // target, so the C library's aarch64 headers were on the command line; the // toolchain's own triple was still the host's, so code generation was @@ -3116,7 +3116,7 @@ prepare_build(bool print_fingerprint, if (auto cfg3 = get_cfg(); cfg3) { auto ref = mcpp::xlings::paths::parse_xpkg_ref(want_sysroot); auto xl = mcpp::config::make_xlings_env(**cfg3); - // ⭐⭐ INSTALLED, NOT MERELY LOOKED UP — THE SAME CHANNEL + // INSTALLED, NOT MERELY LOOKED UP — THE SAME CHANNEL // THE ROW'S TOOLCHAIN PIN GOES THROUGH. // // The row names two things and only one of them used to @@ -3125,7 +3125,7 @@ prepare_build(bool print_fingerprint, // `sysroot` was a pure lookup that returned nullopt and // let the whole block below be skipped without a word. // - // ⚠️ Measured 2026-08-26 in a clean environment (an empty + // Measured 2026-08-26 in a clean environment (an empty // home, so mcpp's registry starts fresh): // // Target riscv64-none-elf @@ -3138,7 +3138,7 @@ prepare_build(bool print_fingerprint, // bare-metal e2e runs on a machine where the gap has // already been papered over. // - // ⚠️ OFFLINE AND `MCPP_NO_AUTO_INSTALL` ARE THE FETCHER'S + // OFFLINE AND `MCPP_NO_AUTO_INSTALL` ARE THE FETCHER'S // DECISION, not re-derived here. One question, one place // that answers it — asking it twice is the shape this // whole release exists to remove. @@ -3216,7 +3216,7 @@ prepare_build(bool print_fingerprint, // honoured exactly. const bool userChoseMsvcItself = tc->compiler == mcpp::toolchain::CompilerId::MSVC; - // ⚠️ AND NOT A COMPILER THE GRAPH REQUIRED. The repair below rewrites + // AND NOT A COMPILER THE GRAPH REQUIRED. The repair below rewrites // the machine's default to winlibs GCC, which is right when mcpp's // own default cannot work here. A family a package REQUIRED is not // mcpp's default to revise: switching to gcc would satisfy nothing — @@ -3361,7 +3361,7 @@ prepare_build(bool print_fingerprint, std::optional> hostTcCache; auto host_tc_for_build_program = [&]() -> std::expected< std::pair, std::string> { - // ⭐⭐ A HOST TOOLCHAIN'S C LIBRARY IS THE PAYLOAD'S, WHATEVER THE + // A HOST TOOLCHAIN'S C LIBRARY IS THE PAYLOAD'S, WHATEVER THE // PROJECT'S TARGET SIDE IS. // // `build.mcpp` is compiled AND RUN on the machine doing the build. Its @@ -3370,14 +3370,14 @@ prepare_build(bool print_fingerprint, // The two are different machines and this function's whole job is to // keep them apart. // - // ⚠️ AND THE NATIVE BRANCH BELOW RETURNS THE MAIN `tc`, WHICH CARRIES + // AND THE NATIVE BRANCH BELOW RETURNS THE MAIN `tc`, WHICH CARRIES // THE OTHER ANSWER. `build_program.cppm`'s own header states the // invariant — "`tc` is always a HOST-targeting toolchain" — and for // every field but this one the native branch satisfied it, because on a // native build the compiler IS the host compiler. `cAbiPrebuilt` is the // first field where "same compiler" and "same target side" come apart. // - // ⚠️ AN INVARIANT, NOT A BUG FIX FOR ANY MEASURED FAILURE. It was + // AN INVARIANT, NOT A BUG FIX FOR ANY MEASURED FAILURE. It was // written while chasing a `features.h: No such file` on openkal-musl's // CI and it is NOT that failure's cause: measured on `origin/main` and // on this branch, the gcc std module carries zero `-isystem`/ @@ -3424,7 +3424,7 @@ prepare_build(bool print_fingerprint, if (hostTcCache) return std::pair{hostTcCache->first, as_host(hostTcCache->second)}; if (!tcSpec || *tcSpec == "system" || tcSpecIsMsvc) { - // ⭐ A READABLE REFUSAL THAT HAD NO CODE, so the target matrix + // A READABLE REFUSAL THAT HAD NO CODE, so the target matrix // recorded four identical `other` cells for it. The sentence was // right; the classification was missing. Measured on windows-2022 // with `msvc@system` declared and any cross target. @@ -3532,12 +3532,12 @@ prepare_build(bool print_fingerprint, // any package, for the same reason the compiler pin is: it is a property // of the target. // - // ⚠️ It rides the SAME channel as `[xlings] deps` rather than getting an + // It rides the SAME channel as `[xlings] deps` rather than getting an // install path of its own — one materialization, one place that can be // wrong. What it must NOT do is depend on the project having an `[xlings]` // section: a bare-metal project written to the template has none, and the // whole point is that it never mentions a libc. - // ⚠️ FROM THE REQUESTED TRIPLE, NOT FROM THE TOOLCHAIN — AND THE TWO WERE + // FROM THE REQUESTED TRIPLE, NOT FROM THE TOOLCHAIN — AND THE TWO WERE // THE SAME VALUE ALL ALONG. // // This read of `tc->targetTriple` was the ONLY thing tying the compiler's @@ -4737,7 +4737,7 @@ prepare_build(bool print_fingerprint, // Which dependency supplied the runner, for the exactly-one-provider // error below. A name rather than a bool: the message has to name both. std::string runnerProvider; - // ⚠️ ONE PROVIDER PER RUNNER NAME. `runner` has had this rule since #544; + // ONE PROVIDER PER RUNNER NAME. `runner` has had this rule since #544; // a NAMED runner inherits it per name, because a board may legitimately // supply `flash` while a different package supplies `monitor`. std::map namedRunnerProvider; @@ -4947,7 +4947,7 @@ prepare_build(bool print_fingerprint, pkg.privateBuild.includeDirsAfter = expandIncludeDirsAfter(packageRoot, manifest); pkg.privateBuild.cflags = manifest.buildConfig.cflags; pkg.privateBuild.cxxflags = manifest.buildConfig.cxxflags; - // ⭐⭐ NOT `= privateBuild` ANY MORE — a package may now say which of + // NOT `= privateBuild` ANY MORE — a package may now say which of // its include directories stop at its own boundary. // // This line took the whole set for as long as the two were the same @@ -4957,7 +4957,7 @@ prepare_build(bool print_fingerprint, // `weak_alias` for musl's own sources, and publishing it hands those // names to every consumer. See BuildInputs::privateIncludeDirs. // - // ⚠️ THE FILTER IS APPLIED AFTER GLOB EXPANSION, so a private entry may + // THE FILTER IS APPLIED AFTER GLOB EXPANSION, so a private entry may // itself be a glob and still name exactly the directories it expands // to. Comparing the unexpanded spellings would let `musl/src/*` be // published because it is not literally equal to `musl/src/include`. @@ -4968,7 +4968,7 @@ prepare_build(bool print_fingerprint, if (std::ranges::find(privateExpanded, d) == privateExpanded.end()) pkg.publicUsage.includeDirs.push_back(d); - // ⚠️ AN ENTRY THAT WITHHOLDS NOTHING IS REPORTED, because the way + // AN ENTRY THAT WITHHOLDS NOTHING IS REPORTED, because the way // it fails is the very defect this key exists to prevent: a // directory the author believes is private stays published, and // nothing about the build looks different until a consumer trips @@ -5180,7 +5180,7 @@ prepare_build(bool print_fingerprint, // Headers reached through `[build].include_dirs` are NOT staged — those // keep pointing at the original install dir via absolutized include paths. // - // ⭐ HEADERS BESIDE A SOURCE ARE A DIFFERENT CASE, AND THEY ARE STAGED. + // HEADERS BESIDE A SOURCE ARE A DIFFERENT CASE, AND THEY ARE STAGED. // // `#include "detail.h"` is resolved relative to the directory of the file // holding the directive, so moving the source moves the search. No @@ -5191,7 +5191,7 @@ prepare_build(bool print_fingerprint, // target/.mangled/openkal-opensbi/__self__/src/time.cpp:44:10: // fatal error: 'sbi.h' file not found // - // ⚠️ THE DIAGNOSIS THIS PRODUCES POINTS AT THE WRONG THING. The path in it + // THE DIAGNOSIS THIS PRODUCES POINTS AT THE WRONG THING. The path in it // is a staging directory the author never wrote, for a header sitting // exactly where the source expects it, and the build that triggered it // asked for nothing unusual — two majors of one dependency is a supported @@ -6124,7 +6124,7 @@ prepare_build(bool print_fingerprint, // ─── The toolchain, resolved now that the graph exists ────────────────── // - // ⚠️ THE TARGET AND THE COMPILER ARE NOT BOUND TOGETHER, AND THE ROW'S + // THE TARGET AND THE COMPILER ARE NOT BOUND TOGETHER, AND THE ROW'S // CONVENTION IS A FALLBACK RATHER THAN A RULE. // // `x86_64-linux-musl → gcc@16.1.0` does not say "prefer gcc". It says "the @@ -6140,7 +6140,7 @@ prepare_build(bool print_fingerprint, // the answer's shape, so it reads the manifests rather than resolving them. { bool graphSuppliesSystem = false; - // ⭐⭐ `requires` IS READ HERE TOO, AND UNTIL THIS LOOP IT WAS ONLY EVER + // `requires` IS READ HERE TOO, AND UNTIL THIS LOOP IT WAS ONLY EVER // CHECKED — A THOUSAND LINES LATER, AGAINST A DECISION THIS BLOCK HAD // ALREADY MADE WITHOUT IT. // @@ -6164,7 +6164,7 @@ prepare_build(bool print_fingerprint, // the default for EVERY project on the box because ONE project's // dependency asked. // - // ⚠️ AND THIS IS THE PLACE, NOT MERELY *A* PLACE. `resolve_target_toolchain` + // AND THIS IS THE PLACE, NOT MERELY *A* PLACE. `resolve_target_toolchain` // has exactly two call sites — its own one-shot recursion, and the one // at the bottom of this block — so every branch inside it, INCLUDING the // first-run install-and-persist path and all three @@ -6178,7 +6178,7 @@ prepare_build(bool print_fingerprint, // this no longer enters. std::string reqCompiler, reqCompilerBy; - // ⭐ A FAMILY NAME BECOMES A CONCRETE SPEC THE SAME WAY IT DOES FOR + // A FAMILY NAME BECOMES A CONCRETE SPEC THE SAME WAY IT DOES FOR // `mcpp toolchain default `, AND FOR THE SAME REASON. // // `requires = ["mcpp:compiler=llvm"]` names a family; the build path @@ -6195,7 +6195,7 @@ prepare_build(bool print_fingerprint, // when the ecosystem moves, with nobody having to remember a // second place. // - // ⚠️ NOT `pins::kFirstRun*`. Those are per-HOST first-run defaults — + // NOT `pins::kFirstRun*`. Those are per-HOST first-run defaults — // `llvm@20.1.7` on macOS, `gcc@16.1.0` on Linux x86_64 — so reading them // would make the version a package requires depend on which machine // built it. A requirement is a property of the package. @@ -6237,7 +6237,7 @@ prepare_build(bool print_fingerprint, // Neither source has one. Saying which family and which two places // were consulted is the difference between an actionable message // and "something went wrong". - // ⚠️ RECORDED, like every other refusal in this function. An + // RECORDED, like every other refusal in this function. An // unnamed branch reports `other`, and this release exists partly // because one of those had a perfectly good name. refusal::record(refusal::Code::CompilerRequirementConflict); @@ -6270,7 +6270,7 @@ prepare_build(bool print_fingerprint, ? pkg.manifest.package.name : std::format("{}@{}", pkg.manifest.package.name, pkg.manifest.package.version); - // ⚠️ TWO DIFFERENT FAMILIES IS AN ERROR RATHER THAN A PICK, the + // TWO DIFFERENT FAMILIES IS AN ERROR RATHER THAN A PICK, the // same rule `provides` already follows one screen down. Choosing // by graph-traversal order would make the answer depend on an // order the author neither writes nor can predict — and unlike a @@ -6295,7 +6295,7 @@ prepare_build(bool print_fingerprint, } } } - // ⚠️ AND A FREESTANDING PIN SURVIVES IT. `graphSuppliesSystem` spans + // AND A FREESTANDING PIN SURVIVES IT. `graphSuppliesSystem` spans // kernel-abi and c-abi, and it correctly cancels a HOSTED row's // convention — that row names the payload the graph is replacing. // A bare-metal row names the only compiler that emits the target. @@ -6322,7 +6322,7 @@ prepare_build(bool print_fingerprint, tcOrigin = TcOrigin::TargetPin; } - // ⭐⭐ THE GRAPH'S REQUIREMENT, TAKEN AS AN INSTRUCTION RATHER THAN AS A + // THE GRAPH'S REQUIREMENT, TAKEN AS AN INSTRUCTION RATHER THAN AS A // TEST TO FAIL LATER. // // Everything above this line decides the compiler from what mcpp knows @@ -6332,13 +6332,13 @@ prepare_build(bool print_fingerprint, // most specific statement in the build. Below the user's own word, above // every default mcpp keeps. // - // ⚠️ THE RANK IS NOT NEW. `TcOrigin` already sorts these, and + // THE RANK IS NOT NEW. `TcOrigin` already sorts these, and // `tc_origin_is_user_explicit` already answers "may mcpp revise this". // The defect was never that the answer was wrong; it was that nobody // asked. `GlobalDefault` is deliberately not user-explicit — see the // note on that function — so a remembered default is exactly the kind of // value this may replace. - // ⚠️ `system` IS LEFT ALONE, AND IT IS THE ONE VALUE HERE THAT IS AN + // `system` IS LEFT ALONE, AND IT IS THE ONE VALUE HERE THAT IS AN // ESCAPE HATCH RATHER THAN AN ANSWER. // // It means "the PATH compiler, whatever it is" — a deliberate opt-out @@ -6358,7 +6358,7 @@ prepare_build(bool print_fingerprint, std::string(mcpp::toolchain::family_name(s->family)); if (haveFamily != reqCompiler) { - // ⚠️ THE PROJECT'S OWN WORD IS NOT REVISED, AND THIS IS THE ONLY + // THE PROJECT'S OWN WORD IS NOT REVISED, AND THIS IS THE ONLY // CASE THAT STILL REFUSES. `[toolchain]`, `[target.X].toolchain` // and `MCPP_TOOLCHAIN` are statements about THIS build; the // graph disagreeing with one of them is a real contradiction and @@ -6367,7 +6367,7 @@ prepare_build(bool print_fingerprint, if (tc_origin_is_user_explicit(tcOrigin)) { // fall through to check_requirements } - // ⚠️⚠️ A ROW'S PIN THAT SURVIVED TO HERE CANNOT BE OVERRIDDEN BY + // A ROW'S PIN THAT SURVIVED TO HERE CANNOT BE OVERRIDDEN BY // A REQUIREMENT, AND THE REASON IS THE SAME ONE THE PIN EXISTS // FOR. // @@ -6384,7 +6384,7 @@ prepare_build(bool print_fingerprint, // here — where both halves are known — beats a compiler // complaining about a file the reader never named. else if (tcOrigin == TcOrigin::TargetPin) { - // ⚠️⚠️ THE TWO ROWS REFUSE UNDER ONE RULE AND FOR TWO + // THE TWO ROWS REFUSE UNDER ONE RULE AND FOR TWO // REASONS, AND ONE REMEDY DOES NOT SERVE BOTH. // // A CONVENTION pin is cancelled by a graph that supplies the @@ -6447,7 +6447,7 @@ prepare_build(bool print_fingerprint, } } } - // ⚠️⚠️ OVERRIDING THE CONVENTION IS ALLOWED; OVERRIDING IT AND SUPPLYING + // OVERRIDING THE CONVENTION IS ALLOWED; OVERRIDING IT AND SUPPLYING // NOTHING IN ITS PLACE IS NOT, AND UNTIL THIS BLOCK IT LOOKED THE SAME. // // A hosted row's pin names the payload that supplies the target's C @@ -6458,7 +6458,7 @@ prepare_build(bool print_fingerprint, // `llvm@22.1.8` plus `openkal-llvm-runtime`, and `graphSuppliesSystem` // is true there. // - // ⚠️ WITH NEITHER, THE BUILD USED TO RUN ANYWAY AND FAIL SOMEWHERE ELSE. + // WITH NEITHER, THE BUILD USED TO RUN ANYWAY AND FAIL SOMEWHERE ELSE. // Measured 2026-08-26 on Linux, `[toolchain] default = "llvm@22.1.8"` // and no dependencies: // @@ -6474,7 +6474,7 @@ prepare_build(bool print_fingerprint, // in the first case, and that belongs to the HOST in the second. There // is no llvm payload supplying either target's C library today. // - // ⭐ THE REFUSAL IS DECIDED HERE BECAUSE ONLY HERE ARE BOTH HALVES + // THE REFUSAL IS DECIDED HERE BECAUSE ONLY HERE ARE BOTH HALVES // KNOWN. The row is read a thousand lines earlier and the graph does // not exist then; `host_can_serve` is family-agnostic and would answer // "yes, some payload here produces it" — the same shape as the family @@ -6616,7 +6616,7 @@ prepare_build(bool print_fingerprint, } // `[targets.*] required_features` on a DEPENDENCY. // - // ⚠️ THIS GATE EXISTED ONLY FOR THE ROOT. The root's targets are + // THIS GATE EXISTED ONLY FOR THE ROOT. The root's targets are // filtered further down against the root's own active features; // a dependency's were never filtered at all, so a descriptor that // wrote `required_features` on a target got the opposite of what @@ -6629,7 +6629,7 @@ prepare_build(bool print_fingerprint, // feature name is package-scoped, so the root's set is a different // vocabulary that happens to share a type. // - // ⚠️⚠️ LIBRARY TARGETS ONLY, and the exclusion is load-bearing. + // LIBRARY TARGETS ONLY, and the exclusion is load-bearing. // // A target requested as a HOST TOOL is what was ASKED FOR, so its // `required_features` become that sub-build's INPUTS instead of a @@ -6711,7 +6711,7 @@ prepare_build(bool print_fingerprint, // doubly-listed gtest_main.cc cannot land twice. auto& bc = pkg.manifest.buildConfig; if (!bc.featureSources.empty()) { - // ⭐ WHETHER A FEATURE *GATES* A SOURCE OR *PROVIDES* IT, AND + // WHETHER A FEATURE *GATES* A SOURCE OR *PROVIDES* IT, AND // THE ANSWER IS WRITTEN IN THE MANIFEST ALREADY. // // Two families of package reach this code and they want @@ -6737,7 +6737,7 @@ prepare_build(bool print_fingerprint, // BEFORE the drop below removes it. A glob in both places is a // gate; a glob in one place is a provider. // - // ⚠️ THIS IS THE FOURTH ATTEMPT, AND THE THIRD WAS ABANDONED ON + // THIS IS THE FOURTH ATTEMPT, AND THE THIRD WAS ABANDONED ON // A MISTAKEN READING. It was recorded as failing because // "gtest's base entry is a glob that MATCHES the file rather // than the same string". Measured against the descriptor the @@ -6780,7 +6780,7 @@ prepare_build(bool print_fingerprint, // visible and stops riscv-virt-rt's feature-only sources // from being compiled without their feature. // - // ⚠️ THE `!` EXCLUSION IS THE WHOLE MECHANISM, NOT THE GLOB + // THE `!` EXCLUSION IS THE WHOLE MECHANISM, NOT THE GLOB // REMOVAL. `src/kal/**` is never IN `bc.sources` — the // package declares no `sources` at all and its files are // matched by the inferred `src/**`. Erasing the string @@ -7047,9 +7047,42 @@ prepare_build(bool print_fingerprint, } return out; }; - auto describe = [&](std::size_t p) { + auto identity = [&](std::size_t p) { + auto const& pkg = packages[p].manifest.package; + return pkg.namespace_.empty() + ? pkg.name : pkg.namespace_ + "." + pkg.name; + }; + // Every host module one package contributes, the lib root first. + // + // The lib root is what a rule package has always been: one unit, + // compiled alone, registered under the name it declares. A package + // that offers several rules through features (mcpp 2026.9.5.3+) + // lists their sources under `[features.] sources`, and those + // globs have been folded into `buildConfig.sources` by now for + // exactly the features the consumer activated. Every module + // INTERFACE unit among them is therefore a host module of its own, + // under its own declared name, and nothing else in the host-module + // path assumes one unit per package: `build_host_module` is per + // unit and the compile loop accumulates BMIs in list order, so a + // feature unit may import the lib root, which precedes it. + // + // Only sources the manifest LISTS take part. The inferred `src/**` + // of a package with no `sources` is not consulted, so a rule + // package published before this round exposes exactly what it + // exposed then; widening that implicitly would compile units that + // were written to be part of an ordinary library, alone. + auto units = [&](std::size_t p) { auto const& depPkg = packages[p]; auto const& pkg = depPkg.manifest.package; + std::vector out; + auto push = [&](std::filesystem::path iface, std::string name) { + prov::HostModule hm; + hm.module = std::move(name); + hm.package = identity(p); + hm.nameSpace = pkg.namespace_; + hm.interface = std::move(iface); + out.push_back(std::move(hm)); + }; // PROBING form: a host-module dependency whose interface is // `.ixx` resolves to a `src/.cppm` that does not exist, // and the consumer's build.mcpp is then handed a path to @@ -7057,13 +7090,39 @@ prepare_build(bool print_fingerprint, auto rel = mcpp::manifest::resolve_lib_root_path( depPkg.manifest, depPkg.root); auto iface = depPkg.root / rel; - prov::HostModule hm; - hm.module = prov::host_module_name(iface, pkg.name); - hm.package = pkg.namespace_.empty() - ? pkg.name : pkg.namespace_ + "." + pkg.name; - hm.nameSpace = pkg.namespace_; - hm.interface = std::move(iface); - return hm; + push(iface, prov::host_module_name(iface, pkg.name)); + // A missing lib root is reported as such by build_host_module, + // and that has to stay the diagnostic. Enumerating the listed + // units first would let one of them collide with the missing + // root's fallback name and report a collision between a file + // and a file that does not exist. + std::error_code ec; + if (!std::filesystem::exists(iface, ec)) return out; + + std::set matched, dropped; + for (auto const& g : depPkg.manifest.buildConfig.sources) { + if (g.empty()) continue; + if (g[0] == '!') { + for (auto& f : mcpp::modgraph::expand_glob(depPkg.root, g.substr(1))) + dropped.insert(f.lexically_normal()); + } else { + for (auto& f : mcpp::modgraph::expand_glob(depPkg.root, g)) + matched.insert(f.lexically_normal()); + } + } + const auto root = iface.lexically_normal(); + for (auto const& f : matched) { // std::set: sorted + if (dropped.contains(f)) continue; + if (std::filesystem::equivalent(f, root, ec)) continue; + std::ifstream is(f); + if (!is) continue; + std::stringstream buf; + buf << is.rdbuf(); + auto name = prov::declared_interface_name(buf.str()); + if (name.empty()) continue; + push(f, std::move(name)); + } + return out; }; for (std::size_t c = 0; c < provisionGraph.visible.size(); ++c) { const auto direct = directHostProviders(c); @@ -7090,10 +7149,10 @@ prepare_build(bool print_fingerprint, for (auto q : path) { if (q == p) started = true; if (!started) continue; - ring += describe(q).package; + ring += identity(q); ring += " -> "; } - ring += describe(p).package; + ring += identity(p); return std::unexpected(std::format( "build rules form an import cycle: {}\n" " A rule's host modules are compiled before " @@ -7105,8 +7164,10 @@ prepare_build(bool print_fingerprint, if (auto r = self(self, q); !r) return r; path.pop_back(); done.insert(p); - ordered.push_back(describe(p)); - ordered.back().importable = isDirect.contains(p); + for (auto& hm : units(p)) { + hm.importable = isDirect.contains(p); + ordered.push_back(std::move(hm)); + } return {}; }; for (auto p : direct) @@ -7589,20 +7650,20 @@ prepare_build(bool print_fingerprint, // Scope::RunGlobal — how the artifact is EXECUTED, forwarded to // the root like link flags but with the opposite merge rule. // - // ⚠️ EXACTLY ONE provider. Link flags from two dependencies + // EXACTLY ONE provider. Link flags from two dependencies // concatenate and that is correct; two runners cannot — appending // produces an argv that is neither one's and fails at exec time // with nothing to say which package contributed which token. So // the second provider is a hard error that names BOTH, because // naming only the loser tells the reader half of what they need. - // ⭐⭐ THE DEFAULT RUNNER AND EVERY NAMED ONE, BY ONE RULE. + // THE DEFAULT RUNNER AND EVERY NAMED ONE, BY ONE RULE. // // Link flags from two dependencies concatenate and that is correct; // two runners for the same name cannot — appending produces an argv // that is neither one's and fails at exec with nothing to say which // package contributed which token. // - // ⚠️ MISSING THIS SITE IS HOW THE FEATURE FAILED FIRST. `apply()` + // MISSING THIS SITE IS HOW THE FEATURE FAILED FIRST. `apply()` // merges a package's directives into its OWN config; this is where a // dependency's RunGlobal entries reach the ROOT. Wiring only the // first left `mcpp run --runner flash` reporting "no such runner" @@ -7644,7 +7705,7 @@ prepare_build(bool print_fingerprint, slot = nr; who = pkg.manifest.package.name; } - // ⚠️ A CLAIM THAT ONLY EVER TIGHTENS. + // A CLAIM THAT ONLY EVER TIGHTENS. if (bcDep.runExclusive && !exclusiveBefore) m->buildConfig.runExclusive = true; m->buildConfig.ldflags.insert(m->buildConfig.ldflags.end(), @@ -7718,7 +7779,7 @@ prepare_build(bool print_fingerprint, // through -- which is why a second backend needs no change here and why // `test_runtime_contract`'s gate stays satisfied. // - // ⚠️ A FLOOR WITH NO FACT IS SILENT. A machine that never declared what + // A FLOOR WITH NO FACT IS SILENT. A machine that never declared what // it has is not a machine that fails the floor; it is one nobody asked. // Reporting a refusal there would turn "we do not know" into "no", and // the whole reason this exists is that a wrong answer is worse than no @@ -7774,7 +7835,7 @@ prepare_build(bool print_fingerprint, // What the packages supplying the target side's layers publish: the header // directories and interface flags the whole build is compiled against. // - // ⭐ ONE SET, TWO READERS, and that is deliberate: it is merged into every + // ONE SET, TWO READERS, and that is deliberate: it is merged into every // package's `privateBuild` (so every compile edge sees it) and handed to // the `std` module's own command line (which is one more translation unit // of the same build). Before this existed, only the second reader was @@ -7816,14 +7877,14 @@ prepare_build(bool print_fingerprint, // for the C++ layer as the spelling that shipped before this one, so an // existing package keeps working unchanged. // - // ⚠️ ONE SUPPLIER PER LAYER, AND TWO IS AN ERROR RATHER THAN A PICK. + // ONE SUPPLIER PER LAYER, AND TWO IS AN ERROR RATHER THAN A PICK. // A C library, a kernel interface and a C++ runtime are mutually // exclusive choices; the same rule already governs `[build] runner` for // the same reason. Until this scan collected candidates instead of // keeping the first acceptable one, two suppliers resolved by graph // traversal order — an order the author neither writes nor can predict — // and the loser's `[build]` section still reached the command line. - // ⭐ `index` — WHICH PACKAGE this candidate is, not just its name. + // `index` — WHICH PACKAGE this candidate is, not just its name. // // Needed once resolution is done: a layer supplied from the graph // publishes an include set the WHOLE build must see (see @@ -7853,7 +7914,7 @@ prepare_build(bool print_fingerprint, : std::format("{}@{}", pkg.manifest.package.name, pkg.manifest.package.version); - // ⚠️ EVERY PACKAGE KIND, NOT ONLY THE ONES WITH AN XPKG + // EVERY PACKAGE KIND, NOT ONLY THE ONES WITH AN XPKG // DESCRIPTOR. `warn_unknown_xpkg_keys` reaches a dependency // resolved through the index; a path or git dependency carries a // manifest of its own and reached no warning at all, so a layer @@ -7910,7 +7971,7 @@ prepare_build(bool print_fingerprint, auto same = std::find_if(slot.begin(), slot.end(), [&](const Candidate& c){ return c.p.name == p.name; }); if (same != slot.end()) { - // ⚠️ `index` MOVES WITH `p` AND NOT ON ITS OWN. The two + // `index` MOVES WITH `p` AND NOT ON ITS OWN. The two // describe one package, and this branch is reached only // from the same `pkgIndex` today — a package carrying both // spellings — so they cannot differ yet. Tying them keeps @@ -7965,7 +8026,7 @@ prepare_build(bool print_fingerprint, in.targetOs = tt->os; in.targetEnv = tt->env; in.freestandingTarget = tt->is_freestanding(); - // ⚠️ NOT `tt->envExplicit`. By this line the triple has been + // NOT `tt->envExplicit`. By this line the triple has been // canonicalised, and the canonical form of `x86_64-linux` is // `x86_64-linux-gnu` — re-parsing it reports a segment the // project never wrote. The request was captured upstream, where @@ -8008,14 +8069,14 @@ prepare_build(bool print_fingerprint, in.cAbi = provider_of(tsd::CapLayer::CAbi); in.cxxAbi = provider_of(tsd::CapLayer::CxxAbi); - // ⚠️⚠️ A ROW THAT LINKS THROUGH lld DIRECTLY, ON A PAYLOAD WITH NO lld. + // A ROW THAT LINKS THROUGH lld DIRECTLY, ON A PAYLOAD WITH NO lld. // // `x86_64-none-elf` is the only row carrying an `lldEmulation`, and its // column comment in mcpp.freestanding.target says why the driver is // bypassed for it: the driver "would hand the link to a host `g++` that // cannot take our linker's path". // - // ⚠️ MEASURED TWICE ON windows-2022, AND THE SECOND TIME WAS MY OWN + // MEASURED TWICE ON windows-2022, AND THE SECOND TIME WAS MY OWN // FALLBACK. First, an empty `resolve_lld` left linker vocabulary on a // driver line: // @@ -8027,7 +8088,7 @@ prepare_build(bool print_fingerprint, // clang++: error: linker (via gcc) command failed // collect2.exe: error: ld returned 1 exit status // - // ⭐ There is no third shape. The row needs lld by name; when the + // There is no third shape. The row needs lld by name; when the // payload has none, the answer is a refusal at the decision, not a // different link. if (auto fsT = tc.has_value() @@ -8056,7 +8117,7 @@ prepare_build(bool print_fingerprint, resolvedTargetSide = tsd::resolve(in); targetSideResolved = true; - // ⭐⭐ RECORDED ON THE TOOLCHAIN THE MOMENT IT IS KNOWN, because three + // RECORDED ON THE TOOLCHAIN THE MOMENT IT IS KNOWN, because three // producers of a compile line need it and only one of them can see // `resolvedTargetSide`. // @@ -8067,14 +8128,14 @@ prepare_build(bool print_fingerprint, // exists to remove, so the answer travels on the value they already // share. // - // ⚠️ HERE AND NOT LATER: `ensure_built` runs at :7368 and every compile + // HERE AND NOT LATER: `ensure_built` runs at :7368 and every compile // line is assembled after it. A std BMI built against a different C // library than its importers is what e2e 181 catches. if (tc) tc->cAbiPrebuilt = resolvedTargetSide.cAbi.prebuilt(); // ── The target side's include set is a property of the BUILD ───────── // - // ⭐⭐ IT WAS ALREADY COMPUTED, AND IT REACHED EXACTLY ONE TRANSLATION + // IT WAS ALREADY COMPUTED, AND IT REACHED EXACTLY ONE TRANSLATION // UNIT. // // A package that supplies a target-side layer publishes the headers the @@ -8100,13 +8161,13 @@ prepare_build(bool print_fingerprint, // `include_dirs` entry reaches the root and its own units, and reaches // no sibling dependency package. // - // ⭐ THE FIX IS THE ONE `mcpp.targetside` OPENS WITH: resolve once, + // THE FIX IS THE ONE `mcpp.targetside` OPENS WITH: resolve once, // after the graph is known, and have every consumer read that one // value. A `publicUsage` describes what a library asks of ITS USERS; a // target side is beneath everything. Modelling the second as the first // is what made it edge-scoped. // - // ⚠️ ONLY LAYERS THE GRAPH SUPPLIES. `Layer::fromGraph()` is the whole + // ONLY LAYERS THE GRAPH SUPPLIES. `Layer::fromGraph()` is the whole // condition. A payload-supplied layer already reaches every unit // through `mcpp.toolchain.hostflags`, and emitting it twice would put // the ordering of one decision in two places. @@ -8143,7 +8204,7 @@ prepare_build(bool print_fingerprint, // target side into the usage requirements of any library this // build packages — a promise about a different machine. // - // ⚠️ APPENDED, so a package's own directories keep coming first. + // APPENDED, so a package's own directories keep coming first. // The target side only has to precede the DRIVER's own defaults, // and those are always searched last. if (!targetSideUsage.includeDirs.empty() @@ -8167,7 +8228,7 @@ prepare_build(bool print_fingerprint, refusal::record(refusal::Code::LayerOrdering); return std::unexpected(*why); } - // ⚠️ REQUIREMENTS ARE CHECKED BEFORE ANYTHING IS COMPILED, WHICH IS THE + // REQUIREMENTS ARE CHECKED BEFORE ANYTHING IS COMPILED, WHICH IS THE // WHOLE POINT OF DECLARING THEM. The combination this rejects — a C++ // runtime configured for one compiler family being handed to another — // otherwise fails inside that runtime's own headers, in a message that @@ -8182,7 +8243,7 @@ prepare_build(bool print_fingerprint, refusal::record(refusal::Code::LayerRequirement); return std::unexpected(*why); } - // ⚠️ A WARNING, NOT A REFUSAL. The graph decides the C library either + // A WARNING, NOT A REFUSAL. The graph decides the C library either // way, so the segment is ignored rather than violated and the artifact // is the same with or without it. Refusing was tried and broke every // project spelling the host target `x86_64-linux-gnu` — which is what @@ -8200,7 +8261,7 @@ prepare_build(bool print_fingerprint, return std::unexpected(unservedTargetDiagnosis); } - // ⚠️ THE TARGET AND THE COMPILER ARE NOT BOUND TOGETHER, AND THE + // THE TARGET AND THE COMPILER ARE NOT BOUND TOGETHER, AND THE // TARGET ROW'S CONVENTION IS A FALLBACK RATHER THAN A RULE. // // A row pins a toolchain because the payload that toolchain belongs to @@ -8210,7 +8271,7 @@ prepare_build(bool print_fingerprint, // that is knowable, because it is the first line at which the graph // exists. // - // ⚠️ The decision itself is NOT revised here. `tc` has been read and + // The decision itself is NOT revised here. `tc` has been read and // mutated at 39 sites between its resolution and this point — the // effective triple, the cross flag, the target sysroot, the MSVC // runtime contract — and re-resolving it here would redo all of them @@ -8242,7 +8303,7 @@ prepare_build(bool print_fingerprint, // no effect and no diagnostic is indistinguishable from one that was // never read. // - // ⚠️ THE C LIBRARY IS THE LAYER THIS DEPENDS ON, NOT "THE SYSTEM". + // THE C LIBRARY IS THE LAYER THIS DEPENDS ON, NOT "THE SYSTEM". // `system_from_graph()` spans two layers, and the arrangement that // separates them is real: a backend running ON a platform takes its // kernel interface from the graph while the C library stays the @@ -8271,11 +8332,11 @@ prepare_build(bool print_fingerprint, // in the graph did not tell anyone what would end up on the link line, // because three places derived it separately and could disagree. // - // ⚠️ AND IT PRINTS ONLY WHAT IS NOT ORDINARY. A zero-configuration build + // AND IT PRINTS ONLY WHAT IS NOT ORDINARY. A zero-configuration build // resolves all five layers from one compiler payload, and five lines // reading `(payload)` answer a question nobody asked. `MCPP_VERBOSE` // prints them all; a diagnostic always does. - // ⚠️⚠️ THE REQUESTED TARGET AND THE RESOLVED ONE MUST NAME THE SAME + // THE REQUESTED TARGET AND THE RESOLVED ONE MUST NAME THE SAME // OPERATING SYSTEM, AND UNTIL THIS LINE NOTHING CHECKED. // // Measured 2026-08-25 in CI, on a machine that had installed only a @@ -8292,7 +8353,7 @@ prepare_build(bool print_fingerprint, // openkal-uefi hit the same fallback at the linker // (`ld: unrecognized option '--subsystem'`). // - // ⭐ THE REPORT ALREADY HELD THE EVIDENCE — this asserts on it rather + // THE REPORT ALREADY HELD THE EVIDENCE — this asserts on it rather // than deriving the question again. A refusal here costs one line; the // alternative is a message about a Win32 function, in a file the reader // did not write, for a decision made in this one. @@ -8301,9 +8362,9 @@ prepare_build(bool print_fingerprint, // vendor difference between `x86_64-windows-gnu` and // `x86_64-w64-windows-gnu` is the normalisation this very line reports, // and refusing on it would reject every correct cross build. - // ⭐ THE NAME THE REPORT PRINTS, DERIVED ONCE AND USED BY BOTH. + // THE NAME THE REPORT PRINTS, DERIVED ONCE AND USED BY BOTH. // - // ⚠️ The first version of this guard read `resolvedTargetCanonical` + // The first version of this guard read `resolvedTargetCanonical` // directly while the report below chose among three sources. They // agreed on the machine it was written on and disagreed in CI, where // the canonical string was empty and the report still named the target @@ -8357,7 +8418,7 @@ prepare_build(bool print_fingerprint, // scan, the `stdModuleFlags` collection, the fingerprint, `compute_flags` — // reads `packages[]` and `*m`, both of which are still writable. // - // ⚠️ EVERY PACKAGE, NOT JUST THE ROOT. The build.mcpp mirror below patches + // EVERY PACKAGE, NOT JUST THE ROOT. The build.mcpp mirror below patches // `packages[0]`, which is right for build.mcpp because a build program // speaks for its own package and the dep loop already handled the others. // Here the motivating case IS a dependency — a package supplying one C++ @@ -8367,7 +8428,7 @@ prepare_build(bool print_fingerprint, // `*m` as well as the snapshots: `canonical_compile_flags(*m)` feeds the // fingerprint, so a contribution reaching the snapshots and not the // manifest would compile with flags the fingerprint does not describe. - // ⚠️⚠️ MUTATING `pkg.manifest` IS NOT ENOUGH, AND THAT IS THE WHOLE + // MUTATING `pkg.manifest` IS NOT ENOUGH, AND THAT IS THE WHOLE // DIFFICULTY OF A LATE PRODUCER. `makePackageRoot` snapshots the manifest's // build inputs into `privateBuild` / `linkUsage`, and the compile and link // edges read THOSE. The build.mcpp tail below solves the identical problem @@ -8795,7 +8856,7 @@ prepare_build(bool print_fingerprint, pkg.root.string())); } tc->stdModuleSource = src; - // ⚠️ AND THE COMPAT MODULE, FROM THE SAME PACKAGE OR NOT AT ALL. + // AND THE COMPAT MODULE, FROM THE SAME PACKAGE OR NOT AT ALL. // // `std.compat` is a second module over the SAME library. Leaving it // pointing at the toolchain's copy does not fail where it is set — it @@ -8836,7 +8897,7 @@ prepare_build(bool print_fingerprint, if (auto spec = mcpp::freestanding::resolve(*fs)) flags += mcpp::freestanding::compile_prefix(*spec, true); } else if (!tc->crossTargetFlag.empty()) { - // ⚠️ `crossTargetFlag` and not `targetTriple`. The triple is mcpp's + // `crossTargetFlag` and not `targetTriple`. The triple is mcpp's // vocabulary (`aarch64-macos`); the flag carries the spelling a // compiler takes (`arm64-apple-macos14.0`). Measured: emitting the // first produced `--target=aarch64-macos`, which clang accepts as a @@ -8844,7 +8905,7 @@ prepare_build(bool print_fingerprint, // aarch64 — the module and its importers would agree with each // other and with nothing else. flags += " " + tc->crossTargetFlag; - // ⚠️ AND THE SECOND CHANNEL. `hostflags.cppm` reaches every ordinary + // AND THE SECOND CHANNEL. `hostflags.cppm` reaches every ordinary // translation unit; this command is assembled here instead, so a // `std.pcm` built with SEH would be imported by units built with // DWARF. Same function, not a second copy of the decision. @@ -8862,7 +8923,7 @@ prepare_build(bool print_fingerprint, flags += " " + mcpp::xlings::shq( std::filesystem::exists(candidate) ? candidate.string() : f); } - // ⚠️ AND THE HEADERS THIS PACKAGE ITSELF IS BUILT AGAINST. + // AND THE HEADERS THIS PACKAGE ITSELF IS BUILT AGAINST. // // The std module source is one of this package's translation units in // every way that matters, and it reaches the C library's headers the @@ -8880,7 +8941,7 @@ prepare_build(bool print_fingerprint, // build path carries directories that exist for its .cpp files and that // shadow the library's headers when a module is compiled against them. // - // ⭐⭐ AND IT IS `targetSideUsage`, NOT THIS PACKAGE'S `publicUsage`. + // AND IT IS `targetSideUsage`, NOT THIS PACKAGE'S `publicUsage`. // // The two are the same set whenever one package supplies every layer, // which is the arrangement this block was written for — so reading the @@ -8890,7 +8951,7 @@ prepare_build(bool print_fingerprint, // both: libc++'s own headers reach ``, which is the C // library's. // - // ⭐ Reading the assembled set also makes this site and every compile + // Reading the assembled set also makes this site and every compile // edge read ONE value. Deriving it here a second time is the shape // #233/#240/#242/#344 each cost a release, and the same set has to // reach both or the `std` BMI describes a different world than the @@ -8919,7 +8980,7 @@ prepare_build(bool print_fingerprint, // no subset of it to build without an OS. Saying "provides no std // module source" sends the reader to look for a broken payload. // - // ⚠️ The line below is copy-pasteable, and that is a PROMISE: it has + // The line below is copy-pasteable, and that is a PROMISE: it has // to resolve today. It briefly did not — an earlier version of this // message named `mcpplibs.std.freestanding` before any such package // existed, so following the advice failed at the very next command @@ -8929,7 +8990,7 @@ prepare_build(bool print_fingerprint, // is back. If it is ever removed from the index, this must change with // it. // - // ⚠️ And the VERSION is part of the promise, not decoration — which is + // And the VERSION is part of the promise, not decoration — which is // how the same defect recurred in a second form. The line said "0.1.0" // after 0.2.0 superseded it in the index, and 0.1.0 is not published, // so pasting it produced @@ -8941,7 +9002,7 @@ prepare_build(bool print_fingerprint, // not fix it either: the request has to name a version the index // actually carries. Publishing a new std-freestanding means updating // this literal in the same change. - // ⚠️ THE QUESTION IS WHETHER A HOSTED STANDARD LIBRARY IS PRESENT, NOT + // THE QUESTION IS WHETHER A HOSTED STANDARD LIBRARY IS PRESENT, NOT // WHETHER THE TARGET IS FREESTANDING. // // Those were the same question for as long as no one had built one for @@ -9150,7 +9211,7 @@ prepare_build(bool print_fingerprint, // directories rather than env vars because the reader is mcpp's own // lookup, not a child process. See BuildContext::xlingsDepBinDirs. // - // ⚠️⚠️ AND EVERY PACKAGE IN THE GRAPH, NOT ONLY THE ROOT — WHICH IS THE + // AND EVERY PACKAGE IN THE GRAPH, NOT ONLY THE ROOT — WHICH IS THE // CASE THIS FEATURE EXISTS FOR. // // A board-support package is precisely the thing that knows which emulator @@ -9168,7 +9229,7 @@ prepare_build(bool print_fingerprint, // nothing. A payload that is declared but not installed contributes // nothing, and the lookup continues to PATH. // - // ⭐⭐ AND THE SET COLLECTED HERE IS ALSO THE SET PROVISIONED. Looking in a + // AND THE SET COLLECTED HERE IS ALSO THE SET PROVISIONED. Looking in a // directory that nothing installed is a lookup that can only fail, and the // engine had exactly that shape: a dependency's declaration was searched // and never acted on. The two definitions are one expression below, so @@ -9194,7 +9255,7 @@ prepare_build(bool print_fingerprint, fromGraph.push_back(spec); } } - // ⚠️ THE ROOT'S OWN PASS RAN LONG AGO, AND THIS ONE MUST NOT REPEAT IT. + // THE ROOT'S OWN PASS RAN LONG AGO, AND THIS ONE MUST NOT REPEAT IT. // The stamp is keyed by the LIST, so provisioning root+graph together // would key a different list than the early pass wrote and re-run an // xlings round trip on every build. Only what the graph added is @@ -9280,7 +9341,7 @@ prepare_build(bool print_fingerprint, // happens here is only the two halves that need this scope: collecting the // facts, and MATERIALISING the answer. // - // ⭐ MATERIALISED AS A TARGET KIND, on purpose. A dependency resolved to + // MATERIALISED AS A TARGET KIND, on purpose. A dependency resolved to // the shared form becomes an ordinary `SharedLibrary` target, so every // emitter mcpp already has applies to it unchanged — the ELF soname and // `$ORIGIN`, the PE import library and generated `.def`, the Mach-O @@ -9294,7 +9355,7 @@ prepare_build(bool print_fingerprint, if (auto parsed = lf::parse(m->buildConfig.dependencyLinkage)) request.whole = *parsed; request.wholeIsExplicit = !m->buildConfig.dependencyLinkage.empty(); - // ⚠️ ONLY THE ROOT MANIFEST'S EDGES. See DependencySpec::linkage — a + // ONLY THE ROOT MANIFEST'S EDGES. See DependencySpec::linkage — a // package deep in the graph imposing a whole-image layout on its // consumer is a supply-chain property, not a convenience. for (auto const& [depName, spec] : m->dependencies) { @@ -9379,7 +9440,7 @@ prepare_build(bool print_fingerprint, if (answer.linkage != lf::DepLinkage::Shared) continue; if (facts.isDistribution) continue; // nothing here to build - // ⚠️ A package that ALREADY declares a shared target has decided + // A package that ALREADY declares a shared target has decided // for itself, and its remaining library targets are not part of // that decision. Flipping them would change what such a package // builds under the DEFAULT request, which is the one property this @@ -9419,6 +9480,10 @@ prepare_build(bool print_fingerprint, ctx.plan.graphShape = (includeDevDeps || !extraTargets.empty()) ? mcpp::build::GraphShape::WithTests : mcpp::build::GraphShape::Normal; + // The device variant an override chose is stamped for the same reason: the + // fast path runs without overrides, so a graph written under one must not + // be the graph it replays. + ctx.plan.accelOverridden = !overrides.accel.empty(); // Resolve the module-edge schedule ONCE, here, where both the toolchain and // the manifest are in hand. The backend writes the graph in this shape, the // graph records the tag, and `mcpp build --verbose` prints the reason — all @@ -9583,7 +9648,7 @@ prepare_build(bool print_fingerprint, // (`[resources]` makes the opposite call on purpose: an icon // belongs to what ships, not to a test runner.) // - // ⭐⭐ A STATIC LIBRARY IS ONE OF THEM, and leaving it out was + // A STATIC LIBRARY IS ONE OF THEM, and leaving it out was // the whole of what C-6 needed. A package whose device code is // its point -- ggml's CUDA backend is 305 `.cu` files behind a // `kind = "lib"` target -- emitted its actions, watched every @@ -10393,7 +10458,7 @@ prepare_build(bool print_fingerprint, } if (!lock.packages.empty() || !lock.indices.empty()) { auto lockPath = workRoot / "mcpp.lock"; - // ⭐⭐ `--locked` ASSERTS THAT THIS RESOLUTION IS THE RECORDED ONE. + // `--locked` ASSERTS THAT THIS RESOLUTION IS THE RECORDED ONE. // // The file has always been written after the walk and never read // back as a constraint; its own header says so ("does not yet pin @@ -10403,7 +10468,7 @@ prepare_build(bool print_fingerprint, // an audit can demand that what resolved today is what was recorded, // and find out when it is not. // - // ⚠️ THE FAILURE NAMES THE DIFFERENCE. "The lock is out of date" is + // THE FAILURE NAMES THE DIFFERENCE. "The lock is out of date" is // true and useless; which package moved, from which version to // which, is what the reader does something about. if (mcpp::platform::env::get("MCPP_LOCKED").value_or("") == "1") { @@ -10714,7 +10779,7 @@ prepare_build(bool print_fingerprint, // placed before any of them would refuse a unit that was about to be // filled. If a fourth source is ever added, it must land before this line. // - // ⚠️ WHY THIS IS AN ERROR AND NOT A WARNING. The two library kinds fail + // WHY THIS IS AN ERROR AND NOT A WARNING. The two library kinds fail // differently and BOTH failures are worse than this message: // // shared — `$cc -shared` over an empty response file. Measured on diff --git a/src/build/prepare_inputs.cppm b/src/build/prepare_inputs.cppm index 94be861f..69208cbf 100644 --- a/src/build/prepare_inputs.cppm +++ b/src/build/prepare_inputs.cppm @@ -7,7 +7,7 @@ // is a bonus and, since the split schedule landed, a smaller one (an interface // now blocks importers for ~22% of its compile rather than all of it). // -// ⚠️ WHAT A SPLIT HAS TO BE TO HELP THE CRITICAL PATH. Extracting a piece that +// WHAT A SPLIT HAS TO BE TO HELP THE CRITICAL PATH. Extracting a piece that // `prepare` then imports makes the chain LONGER, not shorter: `... -> this -> // prepare -> ...` is still serial, and prepare only sheds the cost this module // now pays. It shortens the path only for consumers that can import THIS @@ -59,7 +59,7 @@ namespace cfgpred { // rather than an inference from emptiness because "no layer resolved" and "not // resolved yet" are different answers and only one of them may be reported. // -// ⚠️ `compiler` carries the FAMILY (`llvm`), never the driver (`clang`) — #494 +// `compiler` carries the FAMILY (`llvm`), never the driver (`clang`) — #494 // settled that, on the grounds that every place a user writes the name they // write the family, and reporting the driver would make // `requires = ["mcpp:compiler=llvm"]` permanently unsatisfiable. @@ -133,7 +133,7 @@ inline Ctx context_for(std::string_view targetTriple) { // ── The cfg() vocabulary ──────────────────────────────────────────────────── // -// ⚠️ ONE LIST PER CATEGORY, AND EVERY READER READS IT. #540 found four +// ONE LIST PER CATEGORY, AND EVERY READER READS IT. #540 found four // hand-written copies of other vocabularies in this repository, all drifted; // the diagnostic added below would have been the fifth if it had transcribed // these names instead of sharing them. @@ -162,7 +162,7 @@ inline bool is_cfg_layer_key(std::string_view k) { // expr := all(list) | any(list) | not(expr) | key="value" | bareword // key ∈ kCfgTripleKeys ∪ kCfgLayerKeys bareword ∈ kCfgBarewords // -// ⚠️ THE EVALUATOR IS ALSO THE VALIDATOR. `seenKeys`/`seenWords` let one +// THE EVALUATOR IS ALSO THE VALIDATOR. `seenKeys`/`seenWords` let one // traversal answer three questions — does it match, does it name a layer, does // it name anything at all — because a separate validator would be a SECOND // parser of the same grammar, and this repository has already paid for one of @@ -176,7 +176,7 @@ struct Parser { bool eat(char ch) { ws(); if (i < s.size() && s[i] == ch) { ++i; return true; } return false; } std::string ident() { ws(); std::size_t b = i; - // ⚠️ `-` and `+` ARE IDENTIFIER CHARACTERS, because the layer names are + // `-` and `+` ARE IDENTIFIER CHARACTERS, because the layer names are // `c-abi`, `c++-abi`, `compiler-runtime` and `kernel-abi`. Without them // `cfg(c-abi = "musl")` scanned as the bareword `c` followed by // garbage, so the one thing a diagnostic could report was the letter @@ -466,7 +466,7 @@ std::string canonical_package_build_metadata( s += pkg.manifest.package.version; s += " source="; s += pkg.manifest.package.sourceProvenance; - // ⭐⭐ WHAT THIS PACKAGE IS BUILT WITH, AND NOT ONLY WHAT IT ASKS THE + // WHAT THIS PACKAGE IS BUILT WITH, AND NOT ONLY WHAT IT ASKS THE // RUNTIME FOR. // // Only the root's compile inputs used to reach the fingerprint, through @@ -476,7 +476,7 @@ std::string canonical_package_build_metadata( // kept the same output directory, and the fast path replayed a // build.ninja generated before the edit. // - // ⚠️ AND THE WAY THAT SHOWS IS THAT THE EDIT APPEARS TO HAVE HAD NO + // AND THE WAY THAT SHOWS IS THAT THE EDIT APPEARS TO HAVE HAD NO // EFFECT. Measured 2026-08-23 on a path dependency: a flag added to // `[build] cflags` was absent from the generated `unit_cflags` after a // rebuild, absent after touching the sources, and present the moment diff --git a/src/build/refusal.cppm b/src/build/refusal.cppm index 374e0fbe..519b4518 100644 --- a/src/build/refusal.cppm +++ b/src/build/refusal.cppm @@ -1,6 +1,6 @@ // mcpp.build.refusal — the machine-readable identity of a target refusal. // -// ⭐⭐ WHY A CODE AND NOT THE SENTENCE. +// WHY A CODE AND NOT THE SENTENCE. // // mcpp refuses a target for several distinct reasons, and every one of them // leaves the same trace: `prepare_build` returns `std::unexpected()`. @@ -10,7 +10,7 @@ // reworded `cannot emit it` to `cannot be emitted by`, and a test that had // been asserting a refusal silently began asserting nothing. // -// ⚠️ THE MESSAGE IS STILL A PROMISE. This does not replace it — a refusal that +// THE MESSAGE IS STILL A PROMISE. This does not replace it — a refusal that // does not name the target, the reason and the way out is a defect whether or // not it carries a code, and the e2e tests keep asserting exactly that. What // the code replaces is *classification*: "did it refuse, and for which reason" @@ -25,7 +25,7 @@ // them reads. `mcpp::diag` settled the same trade-off the same way: a per-run // sink, written where the fact is known and read where it is needed. // -// ⚠️ SET IMMEDIATELY BEFORE THE `return`, NEVER EARLIER. A code set at the top +// SET IMMEDIATELY BEFORE THE `return`, NEVER EARLIER. A code set at the top // of a branch that then does not refuse is worse than no code at all: it makes // a successful build report a reason. @@ -35,7 +35,7 @@ import std; export namespace mcpp::build::refusal { -// ⭐ ONE CODE PER DECISION, and the decisions are the ones a reader of the +// ONE CODE PER DECISION, and the decisions are the ones a reader of the // target matrix has to tell apart. Adding a refusal branch means adding a code // here — an unnamed branch reports `other`, which is a visible admission // rather than a silent merge into a neighbouring reason. @@ -114,7 +114,7 @@ Code take(); namespace mcpp::build::refusal { namespace { -// ⚠️ `thread_local`: `prepare_build` recurses for nested host sub-builds, and +// `thread_local`: `prepare_build` recurses for nested host sub-builds, and // those run on the calling thread — but a build program's own sub-build must // not leave its refusal behind for the outer one. Same thread, so the sink is // shared deliberately; the outer refusal is recorded last and wins, which is diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index e3248d3b..074b76ed 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -147,7 +147,7 @@ struct SymbolProvisionFinding { // Evaluate the symbol-provision invariant on the images this run produced. // -// ⚠️ A SEPARATE ENTRY POINT rather than more of validate_changed_artifacts, +// A SEPARATE ENTRY POINT rather than more of validate_changed_artifacts, // and the reason is a gate rather than tidiness: that function returns early // unless the runtime binding's provider is glibc, because everything it checks // is glibc closure physics. This check is ELF physics — a dynamically linked @@ -630,7 +630,7 @@ ValidationReport validate_changed_artifacts( } } } - // ⚠️ A BINDING THAT CANNOT BE EVALUATED IS ONE FACT, NOT ONE PER ARTIFACT. + // A BINDING THAT CANNOT BE EVALUATED IS ONE FACT, NOT ONE PER ARTIFACT. // // On a brand-new MCPP_HOME the first build finds `binding.loader` and // `binding.libraryDirs` both empty (the second build has them; #417), and @@ -874,7 +874,7 @@ check_symbol_provision(const mcpp::build::BuildPlan& plan, auto searchDirs = runtime_search_dirs(plan); - // ⚠️ WHAT AN UNCHANGED ARTIFACT KEEPS. + // WHAT AN UNCHANGED ARTIFACT KEEPS. // // Re-parsing every image on every drive is what made the loader-tag check // cost 158.7s of a 190s hot run, so an artifact whose stat did not move is @@ -962,7 +962,7 @@ check_symbol_provision(const mcpp::build::BuildPlan& plan, auto facts = mcpp::platform::elf::inspect_elf_runtime(artifact); if (!facts) continue; // not ELF: no flat namespace - // ⚠️ PT_INTERP, not the ELF type. A PIE executable is ET_DYN, exactly + // PT_INTERP, not the ELF type. A PIE executable is ET_DYN, exactly // like a shared library, and whether mcpp's toolchain emits PIE is the // payload compiler's default — mcpp passes neither -pie nor -no-pie. // Keying on ET_EXEC would make this check read "nothing to inspect" diff --git a/src/build/schedule/detach_codegen.cppm b/src/build/schedule/detach_codegen.cppm index 2e77ff77..e123a33f 100644 --- a/src/build/schedule/detach_codegen.cppm +++ b/src/build/schedule/detach_codegen.cppm @@ -40,7 +40,7 @@ // baseline wall=80.51s ninja -j32 compilers<=32 // split wall=39.23s ninja -j192 compilers<=32 // -// ⚠️ FOUR HAZARDS, EVERY ONE OF WHICH BIT DURING DEVELOPMENT: +// FOUR HAZARDS, EVERY ONE OF WHICH BIT DURING DEVELOPMENT: // // 1. THE COMPILER MUST NOT INHERIT ninja's PIPE. ninja finishes an edge when // the pipe reaches EOF, NOT when its direct child exits. An inherited pipe @@ -178,7 +178,7 @@ void write_rc(const std::filesystem::path& slot, int rc) { // filesystem mcpp targets, it needs no daemon and no shared memory. A holder // never waits for another token, so this cannot deadlock between holders. // -// ⚠️ WHAT IT CAN DO IS OUTLIVE ITS HOLDER. A token is released by the +// WHAT IT CAN DO IS OUTLIVE ITS HOLDER. A token is released by the // supervisor that took it; a supervisor killed before its cleanup (Ctrl-C, the // OOM killer, a reboot) leaves the directory behind, and nothing in this file // ever reclaims one. The reclaim is therefore done ONCE PER BUILD, in prepare, @@ -253,7 +253,7 @@ void copy_first_rule(const std::filesystem::path& from, const std::filesystem::p // A BMI's IDENTITY, so publication can be detected without the file ever having // to be absent. // -// ⚠️ THE OLD DESIGN CREATED THE HOLE IT WAS TRYING TO AVOID. Phase 1 used to +// THE OLD DESIGN CREATED THE HOLE IT WAS TRYING TO AVOID. Phase 1 used to // `rename(bmi, bmi.bak)` before spawning the compiler — the comment said it was // so "its mere presence can never be mistaken for the new one landing". That is // a real hazard, but the cure left the module with NO BMI ON DISK from that @@ -280,7 +280,7 @@ BmiIdentity bmi_identity(const std::filesystem::path& p) { BmiIdentity id; if (p.empty()) return id; std::error_code ec; - // ⚠️ ASSIGN NOTHING BEFORE CHECKING `ec`. `file_size` returns + // ASSIGN NOTHING BEFORE CHECKING `ec`. `file_size` returns // `static_cast(-1)` when it fails, so writing it into the struct // first makes "this file is missing" compare UNEQUAL to a default-built // identity — which is exactly the sentinel used for "there was no previous @@ -351,7 +351,7 @@ bool spawn_detached(const std::vector& argv) { int run_to_completion(std::string_view command, const std::filesystem::path& logPath) { - // ⚠️ `cmd.exe /c` DOES NOT USE CreateProcess ARGUMENT QUOTING. + // `cmd.exe /c` DOES NOT USE CreateProcess ARGUMENT QUOTING. // // This built the line with `join_command({"cmd.exe", "/c", command})`, which // treats the whole compiler invocation as ONE argv element: it wraps it in @@ -480,7 +480,7 @@ int compile_release_at_bmi(const CompileRequest& req) { if (before.present) { std::filesystem::copy_file( req.bmi, backup, std::filesystem::copy_options::overwrite_existing, ec); - // ⚠️ AND CARRY THE MTIME ACROSS. `copy_file` stamps the copy with + // AND CARRY THE MTIME ACROSS. `copy_file` stamps the copy with // the time of the copy, and `settle_bmi` restores this file when the // new BMI turns out equivalent — precisely so the mtime does NOT // advance and ninja's restat stops the cascade. Without this line @@ -534,7 +534,7 @@ int compile_release_at_bmi(const CompileRequest& req) { // it back rather than leaving the unit with no BMI at all. restore_backup(req.bmi); } else { - // ⚠️ THE COMPILER CAN FINISH BETWEEN THE TWO CHECKS ABOVE. + // THE COMPILER CAN FINISH BETWEEN THE TWO CHECKS ABOVE. // // The loop tests `file_exists(bmi)` first and `rc` second, so a // unit whose compile is shorter than one poll interval lands @@ -584,7 +584,7 @@ int compile_release_at_bmi(const CompileRequest& req) { int supervise(const std::filesystem::path& slot, const std::filesystem::path& semaphoreToken, std::string_view command) { - // ⚠️ EVERY EXIT FROM HERE MUST LEAVE AN `.rc`, INCLUDING THE ONES THAT DO + // EVERY EXIT FROM HERE MUST LEAVE AN `.rc`, INCLUDING THE ONES THAT DO // NOT RUN A COMPILER. Phase 1 and phase 2 both wait on that file; a // supervisor that bails without writing one is indistinguishable from one // that was never started, and both waiters can only end on a timeout. The diff --git a/src/build/schedule/policy.cppm b/src/build/schedule/policy.cppm index e6ed986c..edaf15b1 100644 --- a/src/build/schedule/policy.cppm +++ b/src/build/schedule/policy.cppm @@ -148,7 +148,7 @@ int resolve_jobs(const manifest::Manifest& m, // against the live host. Computed by the CALLER so `decide` stays pure, and // needed because 0 is not a usable answer for every strategy: // -// ⚠️ UNDER DetachCodegen, `hostJobs == 0` MEANS NO BOUND AT ALL. A detached +// UNDER DetachCodegen, `hostJobs == 0` MEANS NO BOUND AT ALL. A detached // compiler stops holding a ninja slot the moment it publishes its BMI, so // ninja's -j is no longer a limit on how many compilers are running — the // semaphore is. A cap of 0 disables the semaphore (`acquire_token` returns diff --git a/src/build/symbol_provision.cppm b/src/build/symbol_provision.cppm index 93018473..624de9e9 100644 --- a/src/build/symbol_provision.cppm +++ b/src/build/symbol_provision.cppm @@ -33,7 +33,7 @@ // mcpp binary (measured: 0 of 217 dynamic symbols). Stage two runs only when // stage one is not, and it is the stage that makes the report TRUE. // -// ⚠️ STAGE TWO IS NOT OPTIONAL, and the reason is mcpp's own doing. A +// STAGE TWO IS NOT OPTIONAL, and the reason is mcpp's own doing. A // `kind = "shared"` dependency's link unit receives only ITS OWN objects // (mcpp.build.plan), so a static package underneath it lands in the CONSUMER's // executable instead, and the shared library binds back to it at run time. @@ -226,7 +226,7 @@ std::string Report::explain(std::string_view artifact) const { // WHY it matters, then what to do — IN THE ORDER THAT ACTUALLY WORKS. // - // ⚠️ `dependency_linkage = "shared"` is deliberately NOT first, and that + // `dependency_linkage = "shared"` is deliberately NOT first, and that // ordering was corrected against a measurement rather than reasoned. On a // real graph (a package staging glib, whose libgio pulls libz.so.1, plus a // statically built compat.zlib) switching the form does remove the diff --git a/src/build/version_floor.cppm b/src/build/version_floor.cppm index c753383c..60097330 100644 --- a/src/build/version_floor.cppm +++ b/src/build/version_floor.cppm @@ -27,7 +27,7 @@ // // provides = ["cuda.driver=12.4"] // -// ⚠️ NO VENDOR NAME APPEARS IN THIS FILE, and that is the point rather than a +// NO VENDOR NAME APPEARS IN THIS FILE, and that is the point rather than a // coincidence. `cuda.driver` is data flowing through: this module reads a name, // a relation and a version, and knows nothing about what any of them mean. A // second backend needs no change here. diff --git a/src/cli.cppm b/src/cli.cppm index bad95341..46163237 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -169,7 +169,7 @@ int run(int argc, char** argv) { // need a parameter threaded down. Same shape as MCPP_VERBOSE above, and // it makes `MCPP_OFFLINE=1` and `--offline` literally the same switch. else if (a == "--offline") mcpp::platform::env::set("MCPP_OFFLINE", "1"); - // ⭐ `--locked` rides the same side channel, and for the stronger form + // `--locked` rides the same side channel, and for the stronger form // of the same reason: its consumer is the resolution write point deep // in mcpp.build.prepare, and it applies to every command that resolves // — build, run, test, flash, monitor, debug — so a per-subcommand @@ -390,7 +390,7 @@ int run(int argc, char** argv) { .action(wrap_rc(cmd_build))) .subcommand(cl::App("run") .description("Build + run a binary target (after `--`, args are passed to it)") - // ⚠️ Named `bin`, NOT `target`, and the rename is load-bearing. + // Named `bin`, NOT `target`, and the rename is load-bearing. // // This positional is a BINARY NAME from [[bin]]/src layout. It was // called `target` — the same word as the cross-target axis — and @@ -425,7 +425,7 @@ int run(int argc, char** argv) { .help("Deprecated alias for --cache=off (also clears the build dir)")) .option(cl::Option("no-runner") .help("Execute the artifact directly, ignoring any [target.].runner (a host that runs it natively)")) - // ⭐⭐ THE TWO AXES `build` AND `test` HAVE ALWAYS TAKEN. + // THE TWO AXES `build` AND `test` HAVE ALWAYS TAKEN. // // Both decide WHAT IS BUILT, so without them `run` could only // execute whatever a previous `build` happened to leave behind — @@ -443,7 +443,7 @@ int run(int argc, char** argv) { .help("Build profile to run (dev | release | )")) .option(cl::Option("release").help("Shorthand for --profile release")) .option(cl::Option("dev").help("Shorthand for --profile dev")) - // ⭐⭐ THE WAY TO REACH THE ARTEFACT, BY NAME. + // THE WAY TO REACH THE ARTEFACT, BY NAME. // // `mcpp run` is universal — every domain has one. HOW the artefact // is reached is not: an MCU is flashed, a service is deployed, a @@ -512,7 +512,7 @@ int run(int argc, char** argv) { .subcommand(cl::App("why") .description("Explain how the toolchain / runtime / deps / runners were resolved") .arg(cl::Arg("topic").help("toolchain | runtime | deps | runners (default: all)")) - // ⭐ `--target` / `--toolchain` make this a QUERY rather than a + // `--target` / `--toolchain` make this a QUERY rather than a // report on the current directory's default: "what would a build // for THIS pair resolve to" is the question the target matrix asks // once per cell, and it builds nothing. @@ -603,7 +603,7 @@ int run(int argc, char** argv) { .help("Package namespace for the emitted descriptor " "(overrides [package] namespace). Emits both " "`namespace` and the fully-qualified `name`"))) - // ⭐ `sbom` belongs HERE rather than at the top level: `emit` + // `sbom` belongs HERE rather than at the top level: `emit` // already means "generate a document describing this project" and // already carries `-o`. A separate `mcpp sbom` would be a second // spelling of an abstraction that exists. @@ -970,7 +970,7 @@ int run(int argc, char** argv) { {"xpkg parse", {}}, {"cache list", {}}, {"toolchain list", {Effect::InitMcppHome}}, - // ⚠️⚠️ `why toolchain` DECLARES MORE THAN IT USUALLY DOES, AND THAT + // `why toolchain` DECLARES MORE THAN IT USUALLY DOES, AND THAT // IS THE CORRECT DIRECTION. // // It answers a question without building, which reads like a pure diff --git a/src/cli/cmd_publish.cppm b/src/cli/cmd_publish.cppm index dab84430..d6da52da 100644 --- a/src/cli/cmd_publish.cppm +++ b/src/cli/cmd_publish.cppm @@ -58,7 +58,7 @@ export int cmd_pack(const mcpplibs::cmdline::ParsedArgs& parsed) { } if (auto v = parsed.value("output")) opts.output = *v; - // ⚠️ `value()`, not `option_or_empty()`, for `profile` — and NOT for + // `value()`, not `option_or_empty()`, for `profile` — and NOT for // anything whose name a positional shares. `ParsedArgs::value()` falls back // to a same-named positional when the option is unset, which is how // `mcpp run q` once became `--target=q`. `pack`'s positional is `target`, diff --git a/src/cli/cmd_sbom.cppm b/src/cli/cmd_sbom.cppm index b24efb05..7e452e1e 100644 --- a/src/cli/cmd_sbom.cppm +++ b/src/cli/cmd_sbom.cppm @@ -1,7 +1,7 @@ // mcpp.cli.cmd_sbom — the dependency graph, in the format a procurement // process asks for. // -// ⭐⭐ THIS IS AN OUTPUT FORMAT, NOT A NEW MECHANISM, AND THAT IS THE WHOLE +// THIS IS AN OUTPUT FORMAT, NOT A NEW MECHANISM, AND THAT IS THE WHOLE // REASON IT IS CHEAP. // // Everything a software bill of materials names — which components went into @@ -11,7 +11,7 @@ // in a shape a legal or security review can consume. It resolves nothing, // builds nothing and asks the network for nothing. // -// ⚠️ AND IT READS THE LOCK RATHER THAN RE-RESOLVING, WHICH IS THE POINT. An +// AND IT READS THE LOCK RATHER THAN RE-RESOLVING, WHICH IS THE POINT. An // SBOM produced by resolving again would describe a graph that may differ from // the one that was built — which is the single thing an SBOM must never do. // If the lock is stale, `--locked` is the tool that says so; this command @@ -65,7 +65,7 @@ std::string purl(const mcpp::lockfile::LockedPackage& p) { return std::format("pkg:mcpp/{}/{}@{}", ns, p.name, p.version); } -// ⚠️ AN UNKNOWN LICENCE IS REPORTED AS UNKNOWN, NEVER OMITTED AND NEVER +// AN UNKNOWN LICENCE IS REPORTED AS UNKNOWN, NEVER OMITTED AND NEVER // GUESSED. A component with no `licenses` key reads as "not examined"; one // carrying a wrong identifier reads as examined and is worse than silence. The // lock records no licence — it records resolution — so unless the package's own @@ -106,7 +106,7 @@ int cmd_sbom(const mcpplibs::cmdline::ParsedArgs& parsed) { "mcpp.lock exists but could not be read: {}", lock.error().message)); return 2; } else { - // ⚠️ NOT AN ERROR, AND NOT SILENCE EITHER. A project with no + // NOT AN ERROR, AND NOT SILENCE EITHER. A project with no // dependencies has no lock and its bill of materials is one component, // which is a true answer. A project that has never been built also has // no lock, and that answer would be false. The note distinguishes them diff --git a/src/cli/cmd_self.cppm b/src/cli/cmd_self.cppm index 13988b88..f118c370 100644 --- a/src/cli/cmd_self.cppm +++ b/src/cli/cmd_self.cppm @@ -85,7 +85,7 @@ export int cmd_doctor(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) { } export int cmd_why(const mcpplibs::cmdline::ParsedArgs& parsed) { - // ⚠️ `--format` IS ONLY DEFINED FOR THE `toolchain` TOPIC, and saying so + // `--format` IS ONLY DEFINED FOR THE `toolchain` TOPIC, and saying so // beats emitting an envelope whose `data` silently omits what was asked // for. `runtime` and `deps` have their own shapes and no consumer yet; // inventing one here would publish a contract nobody has read. diff --git a/src/doctor.cppm b/src/doctor.cppm index b8f12a99..e393cf40 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -784,7 +784,7 @@ int print_stored_runtime_resolution() { // `mcpp why [topic]` / `mcpp resolve --explain`. -// ⭐⭐ THE SAME RESOLUTION `why toolchain` PRINTS, AS DATA. +// THE SAME RESOLUTION `why toolchain` PRINTS, AS DATA. // // A build for one (target, toolchain) pair resolves five layers, a driver, a // triple and a sysroot, and then either proceeds or refuses. Every one of those @@ -793,7 +793,7 @@ int print_stored_runtime_resolution() { // was measured on 2026-08-26: rewording one refusal turned an e2e assertion // into a no-op, silently, in the same session that wrote it. // -// ⚠️ EXIT 0 WHENEVER THE QUESTION WAS ANSWERED, INCLUDING WHEN THE ANSWER IS +// EXIT 0 WHENEVER THE QUESTION WAS ANSWERED, INCLUDING WHEN THE ANSWER IS // "REFUSED". This is a query: "would this build, and if not why" is answered // successfully by "no, because the row's pin is a capability". Overloading the // exit code would give a client exactly the ambiguity the envelope exists to @@ -805,22 +805,22 @@ int print_stored_runtime_resolution() { export int why_toolchain_json(std::string_view target, std::string_view tcSpec) { mcpp::build::BuildOverrides ov; ov.target_triple = std::string(target); - // ⚠️ `MCPP_TOOLCHAIN`, not a field on the overrides — that is the channel + // `MCPP_TOOLCHAIN`, not a field on the overrides — that is the channel // `--toolchain` already uses, and `prepare_build` reads it as a // user-explicit declaration. Adding a second way in would give the two // spellings different provenance, and provenance is exactly what the // convention/capability distinction turns on. // - // ⚠️ RESTORED AFTERWARDS. This is a library function; leaving a declared + // RESTORED AFTERWARDS. This is a library function; leaving a declared // toolchain in the environment would make the NEXT thing this process does // inherit a compiler nobody asked it for. - // ⭐ `ScopedEnv` already exists for exactly this and restores the prior + // `ScopedEnv` already exists for exactly this and restores the prior // value, including "there was none". std::optional tcGuard; if (!tcSpec.empty()) tcGuard.emplace("MCPP_TOOLCHAIN", std::string(tcSpec)); - // ⚠️⚠️ CLEARED BEFORE THE CALL, NOT ONLY READ AFTER IT. + // CLEARED BEFORE THE CALL, NOT ONLY READ AFTER IT. // // The sink is per-thread and `prepare_build` recurses for tool // provisioning. Reading it afterwards without clearing first means a code @@ -829,7 +829,7 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) // than none: it is a specific, plausible, wrong answer. (void)mcpp::build::refusal::take(); - // ⚠️⚠️ STDOUT BELONGS TO THE ENVELOPE, AND `prepare_build` NARRATES. + // STDOUT BELONGS TO THE ENVELOPE, AND `prepare_build` NARRATES. // // `Resolving toolchain` / `Resolved …` / `Target … → …` are status lines // for a person, and they go to stdout. A client is told to detect the @@ -857,9 +857,9 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) /*includeDevDeps=*/false, {}, ov); std::vector diags; if (!ctx) { - // ⚠️ `take()` AFTER the call and only here: a site that recorded a code + // `take()` AFTER the call and only here: a site that recorded a code // and then did not refuse would otherwise leak it into the next query. - // ⚠️⚠️ `None` MEANS "NOTHING RECORDED", AND HERE THAT IS NOT "no reason" + // `None` MEANS "NOTHING RECORDED", AND HERE THAT IS NOT "no reason" // — the build demonstrably refused. Reporting `none` beside // `refused` gives one word two meanings, which is the defect this // whole release is about, reintroduced in the machinery built to @@ -867,7 +867,7 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) // a site that had no code, and the matrix recorded // `unsupported / none`. // - // ⭐ `other` is a visible admission: a refusal exists and its branch + // `other` is a visible admission: a refusal exists and its branch // has not been named yet. auto code = mcpp::build::refusal::take(); if (code == mcpp::build::refusal::Code::None) @@ -895,7 +895,7 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) data["status"] = "ok"; data["reason"] = "none"; - // ⭐ AND WHY THIS ONE. The build's status line says it; a consumer of the + // AND WHY THIS ONE. The build's status line says it; a consumer of the // machine interface asking "what would this resolve to, and why" would // otherwise have to parse that prose — the substring matching this document // exists to remove. `requiredBy` and `replaced` are empty unless something @@ -916,7 +916,7 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) {"llvm", ts.llvmTriple}, }; - // ⭐⭐ THE C LIBRARY MODEL, NOT `tc.sysroot`. + // THE C LIBRARY MODEL, NOT `tc.sysroot`. // // `tc.sysroot` is what the driver reports for `-print-sysroot`, and it is // frequently empty for a toolchain that nonetheless receives an explicit @@ -939,7 +939,7 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) const auto& srPath = lm.mode == mcpp::toolchain::CLibMode::Sysroot ? lm.sysroot : lm.crtDir; - // ⚠️⚠️ AND WHETHER THIS MODEL IS THE ONE IN THE ARTIFACT, BECAUSE THE + // AND WHETHER THIS MODEL IS THE ONE IN THE ARTIFACT, BECAUSE THE // DOCUMENT USED TO ANSWER THE SAME QUESTION TWICE AND DIFFERENTLY. // // Measured on 2026.8.26.1, one `mcpp why toolchain --format json` over an @@ -957,7 +957,7 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) // which one governed, which is the same defect as a name that contradicts a // fact printed under it. // - // ⭐ ONE FIELD ADDED, NONE CHANGED. `mcpp.why.toolchain` promises that + // ONE FIELD ADDED, NONE CHANGED. `mcpp.why.toolchain` promises that // fields are added and never removed and that a field's meaning never // changes (docs/11 §6). Renaming `cLibrary`, or widening `mode` with a // `graph` value, would break that promise for a document whose whole point @@ -1045,7 +1045,7 @@ export int why_report(const std::string& topic) { } } if (all) (void)print_stored_runtime_resolution(); - // ⭐ WHERE A NAMED RUNNER BECOMES DISCOVERABLE. + // WHERE A NAMED RUNNER BECOMES DISCOVERABLE. // // The engine knows no runner names, which is what keeps `mcpp flash` from // being a top-level command that is dead in every project that is not diff --git a/src/fallback/install_integrity.cppm b/src/fallback/install_integrity.cppm index 2f0b383f..2a93b01b 100644 --- a/src/fallback/install_integrity.cppm +++ b/src/fallback/install_integrity.cppm @@ -35,7 +35,7 @@ inline constexpr std::string_view kInstallMarker = ".mcpp_ok"; // Did the package put anything in its own directory? // -// ⭐⭐ THE MARKER IS ONLY AS GOOD AS THE EVIDENCE IT IS WRITTEN FROM, and for +// THE MARKER IS ONLY AS GOOD AS THE EVIDENCE IT IS WRITTEN FROM, and for // three releases that evidence was `exitCode == 0 && exists(verdir)` — the // installer's own report plus a directory the installer creates before it does // any work. mcpp#533: a package-identity collision made xlings skip the @@ -49,7 +49,7 @@ inline constexpr std::string_view kInstallMarker = ".mcpp_ok"; // `.xpkg-install.json` and `mcpp_generated/` and nothing else — and it needs // no knowledge of what the descriptor was supposed to produce. // -// ⚠️ DELIBERATELY WEAK, in both directions. It cannot tell a RIGHT tree from a +// DELIBERATELY WEAK, in both directions. It cannot tell a RIGHT tree from a // wrong one, and it must not: a package that legitimately installs no payload // exists (xlings has type-only packages, and #531 provisions `[xlings] deps` // for packages mcpp consumes nothing from). So the caller WITHHOLDS THE MARKER diff --git a/src/freestanding/linkline.cppm b/src/freestanding/linkline.cppm index d0c873c8..5c6a1022 100644 --- a/src/freestanding/linkline.cppm +++ b/src/freestanding/linkline.cppm @@ -18,7 +18,7 @@ // the only form where "what ends up on the command" equals "what this module // decided". // -// ⚠️ THE LINKER IS ADDRESSED BY ABSOLUTE PATH +// THE LINKER IS ADDRESSED BY ABSOLUTE PATH // // `-fuse-ld=lld` resolves by NAME, and on a machine with binutils earlier on // PATH it finds GNU ld, which then fails with @@ -94,7 +94,7 @@ inline std::string link_flags(const Spec& s, const LinkInputs& in, out += " -Wl,--gc-sections"; if (!in.lld.empty()) out += " -fuse-ld=" + esc(in.lld); - // ⚠️ BEFORE the libraries the board selects, and it has to be on THIS line + // BEFORE the libraries the board selects, and it has to be on THIS line // rather than in the generic ld flags: a freestanding link line is // REPLACED wholesale (the payload cfg would otherwise inject a host // dynamic linker), so anything appended to the ordinary ldflags earlier is @@ -114,7 +114,7 @@ inline std::string link_flags(const Spec& s, const LinkInputs& in, // driver line and a linker line share no vocabulary, and writing them together // would mean a conditional at every token. // -// ⚠️ WHAT CLANG WOULD HAVE ADDED, AND WHAT IS DELIBERATELY NOT REPRODUCED. +// WHAT CLANG WOULD HAVE ADDED, AND WHAT IS DELIBERATELY NOT REPRODUCED. // Measured on llvm 22.1.8 for the two rows whose driver does reach lld: it // passes `-Bstatic`, `-m `, `-X`, and three `-L` paths under // `lib/clang-runtimes//lib` and `lib/clang/22/lib/`. The `-L`s diff --git a/src/freestanding/target.cppm b/src/freestanding/target.cppm index 0773972c..17777fff 100644 --- a/src/freestanding/target.cppm +++ b/src/freestanding/target.cppm @@ -44,7 +44,7 @@ struct Spec { // own multilib convention (`/`), carried here so a wrapper // package can name its layout without re-deriving the profile. std::string_view libdir; - // ⚠️ FLAGS THIS ISA REQUIRES THAT THE FOUR COLUMNS ABOVE CANNOT EXPRESS. + // FLAGS THIS ISA REQUIRES THAT THE FOUR COLUMNS ABOVE CANNOT EXPRESS. // // The table had three flag columns because three were enough for RISC-V and // aarch64, where everything a freestanding build needs is an ISA profile, a @@ -54,7 +54,7 @@ struct Spec { // Empty for every row that does not, which is every row but one. std::span extra; - // ⚠️ NON-EMPTY MEANS "THIS TARGET'S LINK CANNOT GO THROUGH THE COMPILER + // NON-EMPTY MEANS "THIS TARGET'S LINK CANNOT GO THROUGH THE COMPILER // DRIVER", AND THAT IS A PROPERTY OF CLANG RATHER THAN OF THE ISA. // // Every freestanding link is driven by clang, which selects a toolchain @@ -86,7 +86,7 @@ struct Spec { // inline braces because a `span` must refer to storage that outlives it, and a // temporary array in an aggregate initialiser does not. inline constexpr std::string_view kX86_64NoneExtra[] = { - // ⚠️ WITHOUT THIS, EVERY TRAP HANDLER ON THIS TARGET CORRUPTS THE FUNCTION + // WITHOUT THIS, EVERY TRAP HANDLER ON THIS TARGET CORRUPTS THE FUNCTION // IT INTERRUPTED, AND NOTHING REPORTS IT. // // The System V x86-64 ABI reserves 128 bytes below `rsp` — the red zone — @@ -108,7 +108,7 @@ inline constexpr std::string_view kX86_64NoneExtra[] = { "-mno-red-zone", }; -// ⚠️ THE SOFT-FLOAT M-PROFILE ROWS, AND WHY THE FLOAT ABI IS NOT ENOUGH. +// THE SOFT-FLOAT M-PROFILE ROWS, AND WHY THE FLOAT ABI IS NOT ENOUGH. // // `thumbv7em-none-eabi` and `-eabihf` differ in their float ABI, and clang // derives that from the triple with no help from us (measured, `-###`: @@ -124,14 +124,14 @@ inline constexpr std::string_view kX86_64NoneExtra[] = { // under either ABI. A plain Cortex-M4 — the part this row exists to serve, // since the -eabihf row serves M4F — has no FPU, and the instruction faults. // -// ⚠️ The failure is a hard fault at run time on real silicon, with a clean +// The failure is a hard fault at run time on real silicon, with a clean // compile and a clean link. It is the shape this table exists to prevent. // // `-mfpu=none` restores the property the row's name claims. Measured: the // instruction count returns to zero, and the `-eabihf` rows must NOT receive // this flag — there it would discard the FPU the row exists to use. // -// ⚠️ EVERY SOFT ROW CARRIES IT, INCLUDING THE ONES WHERE NOTHING WAS MEASURED. +// EVERY SOFT ROW CARRIES IT, INCLUDING THE ONES WHERE NOTHING WAS MEASURED. // // Only `thumbv7em-none-eabi` was observed emitting an FPU instruction; // `thumbv6m`, `thumbv7m` and `thumbv8m.base` describe architectures with no FPU @@ -148,7 +148,7 @@ inline constexpr std::string_view kThumbSoftExtra[] = { "-mfpu=none", }; -// ⭐⭐ AND THE SAME RULE ON A-PROFILE, WHERE IT WAS MEASURED SEPARATELY RATHER +// AND THE SAME RULE ON A-PROFILE, WHERE IT WAS MEASURED SEPARATELY RATHER // THAN ASSUMED TO CARRY OVER. // // `armv7a-none-eabi` is a different architecture from `thumbv7em`, so the @@ -164,7 +164,7 @@ inline constexpr std::string_view kArmSoftExtra[] = { "-mfpu=none", }; -// ⚠️ THE `libdir` COLUMN WAS EMPTY ON THE LAST TWO ROWS UNTIL 2026-08-21, AND +// THE `libdir` COLUMN WAS EMPTY ON THE LAST TWO ROWS UNTIL 2026-08-21, AND // THAT WAS CORRECT UNTIL THE DAY IT WAS NOT. // // It names the sub-directory of a multilib C library — picolibc's own @@ -177,13 +177,13 @@ inline constexpr std::string_view kArmSoftExtra[] = { // `'stdio.h' file not found` — the package is installed and correct, and the // engine cannot find the profile inside it. // -// ⚠️ Filling this does NOT give those targets a C library by default. The +// Filling this does NOT give those targets a C library by default. The // column is consulted only when a sysroot has been resolved, and the target // TABLE still binds none for these two rows — the zero-libc tier stays the // default and the package stays opt-in. This makes the opt-in work; it does not // take the opt-out away. // -// ⚠️ Defaults, not the only possibility. `rv64gc/lp64d` is what qemu `virt` +// Defaults, not the only possibility. `rv64gc/lp64d` is what qemu `virt` // runs and what the first BSP targets; a board that needs `rv32imac/ilp32` // selects it through its own manifest, not by editing this table. The table // exists so that `--target riscv64-none-elf` alone is enough to produce a @@ -192,7 +192,7 @@ inline constexpr Spec kTable[] = { // triple march mabi mcmodel libdir { "riscv64-none-elf", "rv64gc", "lp64d", "medany", "rv64gc/lp64d" }, { "riscv32-none-elf", "rv32imac", "ilp32", "medany", "rv32imac/ilp32" }, - // ⚠️ `aapcs` AND NOT `lp64`. The two RISC-V rows above spell their ABI the + // `aapcs` AND NOT `lp64`. The two RISC-V rows above spell their ABI the // way RISC-V does, and the obvious extrapolation to aarch64 is `lp64` — // which is what LP64 aarch64 actually is, and which clang rejects: // @@ -209,13 +209,13 @@ inline constexpr Spec kTable[] = { // it. RISC-V needs `medany` for the corresponding reason — its default // assumes the low 2GiB, and bare-metal RISC-V runs at 0x80000000. // - // ⚠️ THE LIBDIR COLUMN IS EMPTY, AND THAT IS THE ROW'S CHARACTER. It names + // THE LIBDIR COLUMN IS EMPTY, AND THAT IS THE ROW'S CHARACTER. It names // a C library's multilib directory, and this target's row in the target // table resolves no C library: `aarch64-none-elf` is the zero-libc tier. // The engine reads this column only when a sysroot exists, so an invented // value would be a value nothing could ever check. { "aarch64-none-elf", "armv8-a", "aapcs", "small", "armv8-a/aapcs" }, - // ⚠️ `x86-64` WITH A HYPHEN, WHICH IS THE ONE PLACE THIS TRIPLE'S TWO + // `x86-64` WITH A HYPHEN, WHICH IS THE ONE PLACE THIS TRIPLE'S TWO // SPELLINGS DIVERGE. The triple segment is `x86_64` with an underscore and // the `-march` value is `x86-64` with a hyphen; deriving one from the other // is a substitution that looks harmless and produces `unknown target CPU`. @@ -236,7 +236,7 @@ inline constexpr Spec kTable[] = { // manifest; that is a linker-script decision and cannot be a default, // because the two are wrong for each other rather than merely suboptimal. // - // ⚠️ THE LIBDIR IS EMPTY FOR THE SAME REASON aarch64's IS: this row + // THE LIBDIR IS EMPTY FOR THE SAME REASON aarch64's IS: this row // resolves no C library, so a value here could never be checked. { "x86_64-none-elf", "x86-64", "sysv", "small", "x86-64/sysv", kX86_64NoneExtra, "elf_x86_64" }, @@ -251,7 +251,7 @@ inline constexpr Spec kTable[] = { // the x86_64 row's problem does not recur here. Verified end to end: a // freestanding thumb object links under `ld.lld` and boots under QEMU. // - // ⚠️⚠️ `libdir` IS THE TRIPLE HERE, NOT `/` LIKE EVERY OTHER + // `libdir` IS THE TRIPLE HERE, NOT `/` LIKE EVERY OTHER // ROW, AND THE DIFFERENCE IS AN ABI SUBSTITUTION THAT NOTHING REPORTS. // // The column names the sub-directory a multilib C library uses, and on @@ -267,13 +267,13 @@ inline constexpr Spec kTable[] = { // hard-float build, sitting exactly where a soft-float program would find // it. Nothing failed at build time. // - // ⚠️ Filling this does NOT give these rows a C library by default: the + // Filling this does NOT give these rows a C library by default: the // column is read only once a sysroot has been resolved, and the target // table still binds none. The zero-libc tier stays the default and // `[target.] sysroot = "xim:picolibc-arm@1.8.12"` is the opt-in. // This makes the opt-in work; it does not take the opt-out away. // - // ⚠️ `-mabi=aapcs`, matching the aarch64 row and for the same reason: on + // `-mabi=aapcs`, matching the aarch64 row and for the same reason: on // ARM `-mabi` names a procedure call standard, not a data model. // triple march mabi mcmodel libdir extra { "thumbv6m-none-eabi", "armv6-m", "aapcs", "", "thumbv6m-none-eabi", kThumbSoftExtra }, @@ -285,7 +285,7 @@ inline constexpr Spec kTable[] = { { "thumbv8m.main-none-eabihf","armv8-m.main","aapcs","", "thumbv8m.main-none-eabihf" }, // ── ARMv7-A (Cortex-A, 32-bit) ────────────────────────────────────────── // - // ⭐ THE FIRST 32-BIT MACHINE WITH A MEMORY MANAGEMENT UNIT, AND THAT IS + // THE FIRST 32-BIT MACHINE WITH A MEMORY MANAGEMENT UNIT, AND THAT IS // WHY IT IS HERE RATHER THAN BEING A SECOND SPELLING OF THE M ROWS. // // Every other 32-bit row in this table is M-profile: an MPU that describes @@ -295,17 +295,17 @@ inline constexpr Spec kTable[] = { // looks like — short descriptors are 32 bits wide, long (LPAE) ones 64. // That question cannot be asked on a machine with no entries. // - // ⚠️ `mcmodel` and `libdir` are empty for the reasons the M rows give. + // `mcmodel` and `libdir` are empty for the reasons the M rows give. // `lldEmulation` is empty because clang's BareMetal toolchain covers arm: // measured, the driver reaches `ld.lld` and the x86_64 row's problem does // not recur. // - // ⚠️ THE TIER IS `verified` AND THE MEASUREMENT IS NAMED. 2026-09-04 under + // THE TIER IS `verified` AND THE MEASUREMENT IS NAMED. 2026-09-04 under // `xim:qemu-arm@9.2.4-1`: both rows built an image that BOOTED on // `-M virt -cpu cortex-a15`, printed over semihosting and reported its exit // status. // - // ⚠️ AND THE SEMIHOSTING EXIT CALL IS NOT SPELLED THE WAY M-PROFILE SPELLS + // AND THE SEMIHOSTING EXIT CALL IS NOT SPELLED THE WAY M-PROFILE SPELLS // IT. `SYS_EXIT` (0x18) on AArch32 takes the reason code in `r1` directly; // the `{reason, code}` block every Cortex-M board here passes is // `SYS_EXIT_EXTENDED` (0x20), which exists because a 32-bit `r1` cannot @@ -314,7 +314,7 @@ inline constexpr Spec kTable[] = { // a board fact rather than a target fact, recorded here because it is where // the next person to write such a board will look. // - // ⚠️ `libdir` IS THE TRIPLE HERE TOO, AND FOR THE SAME REASON AS THE + // `libdir` IS THE TRIPLE HERE TOO, AND FOR THE SAME REASON AS THE // M ROWS — even though no package carries an A-profile multilib yet. // // The column is a CONVENTION about where a sysroot puts a profile, not a @@ -367,7 +367,7 @@ inline std::vector compile_flags(const Spec& s, // `-ffreestanding` so the ordering of this function's output stays a // function of the table rather than of the row. for (auto flag : s.extra) out.emplace_back(flag); - // ⭐⭐ ONE SECTION PER FUNCTION, SO THE LINKER CAN DROP WHAT NOTHING CALLS. + // ONE SECTION PER FUNCTION, SO THE LINKER CAN DROP WHAT NOTHING CALLS. // // These two flags do nothing on their own; they are the half of // `--gc-sections` that has to happen at compile time, and the link half is @@ -375,7 +375,7 @@ inline std::vector compile_flags(const Spec& s, // project because a dependency's translation units must carry them too, and // a project cannot reach those. // - // ⚠️ WHY THIS BECAME NECESSARY RATHER THAN MERELY NICE. A dependency's + // WHY THIS BECAME NECESSARY RATHER THAN MERELY NICE. A dependency's // object files enter the link unconditionally (docs/13), unlike an archive // member, which is pulled only while its symbol is undefined. That costs // nothing when the C library is a prebuilt archive and the target has @@ -384,14 +384,14 @@ inline std::vector compile_flags(const Spec& s, // Cortex-M part has kilobytes: without this, every image carries the whole // of the C library whether or not it calls into it. // - // ⚠️ AND IT MAKES A LINKER SCRIPT LOAD-BEARING IN A NEW WAY: an interrupt + // AND IT MAKES A LINKER SCRIPT LOAD-BEARING IN A NEW WAY: an interrupt // vector table is referenced by nothing — the hardware reads it by address // — so `--gc-sections` collects it. A board's script must say // `KEEP(*(.vectors))`. Measured: with the KEEP present, a dead function is // dropped and the table survives; the image boots. out.emplace_back("-ffunction-sections"); out.emplace_back("-fdata-sections"); - // ⭐ AND `-ffreestanding` ITSELF IS ONE OF THE THINGS THE GRAPH DECIDES. + // AND `-ffreestanding` ITSELF IS ONE OF THE THINGS THE GRAPH DECIDES. // // The paragraph above this function names what the flag changes: "no `main` // special-casing, no builtin-to-libcall rewrites it cannot back up". Both @@ -400,7 +400,7 @@ inline std::vector compile_flags(const Spec& s, // `hosted` is the language's own word for not-freestanding, so a provider // of that capability is asserting exactly the condition this flag denies. // - // ⚠️ Measured 2026-08-23, and the way it showed was not a diagnostic about + // Measured 2026-08-23, and the way it showed was not a diagnostic about // the flag. A bare-metal program whose `main` was an ordinary C++ `int // main()` failed to link with `undefined symbol: main`, while `nm` on its // own object showed `_Z4mainv` — under `-ffreestanding` a C++ `main` is not @@ -411,7 +411,7 @@ inline std::vector compile_flags(const Spec& s, // main()`, which is a workaround for a claim the build was making on the // program's behalf and that was no longer true. if (!targetCxxRuntime) out.emplace_back("-ffreestanding"); - // ⭐⭐ UNWIND TABLES, WHICH THE COMPILER TURNS OFF FOR THIS KIND OF TARGET + // UNWIND TABLES, WHICH THE COMPILER TURNS OFF FOR THIS KIND OF TARGET // AND WHICH NOTHING IN THE BUILD OTHERWISE SAYS. // // On a hosted ELF target clang emits `.eh_frame` for every function by @@ -422,7 +422,7 @@ inline std::vector compile_flags(const Spec& s, // libunwind's C++ half, the program) and are ABSENT for everything else, // which on this stack means the C library and libunwind's own C sources. // - // ⚠️ AND A PARTIAL SET OF TABLES DOES NOT DEGRADE — IT STOPS THE WALK. + // AND A PARTIAL SET OF TABLES DOES NOT DEGRADE — IT STOPS THE WALK. // Measured 2026-08-23 on riscv64-none-elf, and the measurement is worth // keeping because every intermediate reading pointed elsewhere: // @@ -441,7 +441,7 @@ inline std::vector compile_flags(const Spec& s, // ELF target already gets by default, and the rest of this stack was // developed against that behaviour. if (targetCxxRuntime) out.emplace_back("-fasynchronous-unwind-tables"); - // ⚠️ No C++ standard library headers. Not a preference — the toolchain's + // No C++ standard library headers. Not a preference — the toolchain's // libc++ headers are built for the HOST: `#include ` resolves to // libc++'s wrapper, which opens `<__config_site>`, which is generated per // installation for the host configuration and is simply absent for this @@ -456,7 +456,7 @@ inline std::vector compile_flags(const Spec& s, // Kept in both cases: a target-side C++ library reaches a consumer through // its own include dirs and modules, never through the compiler's. out.emplace_back("-nostdinc++"); - // ⚠️ Exceptions and RTTI off, and this belongs HERE — with the target — for + // Exceptions and RTTI off, and this belongs HERE — with the target — for // the same reason `-ffreestanding` does: it is a property every TU in the // graph must agree on. // @@ -482,7 +482,7 @@ inline std::vector compile_flags(const Spec& s, // target-built libc++abi and unwinder has a real case for turning these // back on, and that is the point at which this becomes a manifest key. // - // ⭐ AND THAT POINT HAS ARRIVED. A package that provides the capability + // AND THAT POINT HAS ARRIVED. A package that provides the capability // `hosted-standard-library' for this target IS the board described above: // it carries libc++abi and libunwind compiled for it. With one present, // forcing these off is what breaks the build --- the runtime is compiled diff --git a/src/hooks.cppm b/src/hooks.cppm index 6f3e721f..dc1314e2 100644 --- a/src/hooks.cppm +++ b/src/hooks.cppm @@ -1,6 +1,6 @@ // mcpp.hooks — running the project's `[hooks]` lifecycle commands. // -// ⚠️ EXPERIMENTAL. A hook cannot currently change whether a build succeeded: +// EXPERIMENTAL. A hook cannot currently change whether a build succeeded: // every failure is a warning, and `side_effect = true` is refused by the // manifest parser. Only the ROOT project's hooks are ever run — a dependency's // `[hooks]` is inert. See docs/05-mcpp-toml.md §2.16. @@ -131,7 +131,7 @@ event_command(const mcpp::manifest::Hooks& hooks, Event event) { // message is identical either way; only its severity and the build's fate // differ. // -// ⚠️ WHILE `[hooks]` IS EXPERIMENTAL, `sideEffect` IS ALWAYS FALSE — the +// WHILE `[hooks]` IS EXPERIMENTAL, `sideEffect` IS ALWAYS FALSE — the // manifest parser refuses `side_effect = true` (see // modules/manifest/src/toml.cppm). The `true` branch below is therefore // unreachable today ON PURPOSE: it is the behaviour the key will select when diff --git a/src/modgraph/scanner.cppm b/src/modgraph/scanner.cppm index c4635a27..1ef0d032 100644 --- a/src/modgraph/scanner.cppm +++ b/src/modgraph/scanner.cppm @@ -1,9 +1,9 @@ // mcpp.modgraph.scanner — regex-based scan of .cppm/.cpp for module statements. // // Hard constraints (per docs/01 §4.2): -// ✗ no #if/#ifdef-guarded import -// ✗ no header units (import "h" / import ) -// ✗ files outside [modules].sources glob +// not supported: #if/#ifdef-guarded import +// not supported: header units (import "h" / import ) +// not supported: files outside [modules].sources glob // // Returns a Graph or a list of detailed errors. @@ -591,7 +591,7 @@ std::expected scan_file(const std::filesystem::path& file // `u.kind`; nothing re-derives it from the extension. u.kind = mcpp::classify(file, extTable); - // ⚠️ A file `sources` matched but the classifier cannot place. + // A file `sources` matched but the classifier cannot place. // // This used to be accepted, and what it produced was a compile edge whose // object NOTHING LINKS. Measured with an `.ixx` that no @@ -1024,7 +1024,7 @@ void scan_one_into(ScanResult& result, // package whose `.ixx` sources are all behind an inactive feature would // otherwise fail to build. // - // ⚠️ And not at all when this package HAS no sources to look at. A + // And not at all when this package HAS no sources to look at. A // `host-module = true` dependency has its source globs emptied on purpose — // that is how a build rule is kept out of the consumer's binary — so every // one of its declared extensions then looks dead. The author of an `.ixx` diff --git a/src/modgraph/validate.cppm b/src/modgraph/validate.cppm index 47eeb12f..f00fb3a5 100644 --- a/src/modgraph/validate.cppm +++ b/src/modgraph/validate.cppm @@ -155,7 +155,7 @@ ValidateReport validate(const Graph& g, } else if (std::ranges::any_of(g.units, [](auto const& u) { return u.provides.has_value(); })) { - // ⚠️ THE PREDICATE ABOVE IS `has_lib_target` — "does this + // THE PREDICATE ABOVE IS `has_lib_target` — "does this // produce a library" — and the property this warning is // about is "is this a C++ MODULE library". They came apart // the moment the index grew source-built C packages diff --git a/src/pack/manifest_emit.cppm b/src/pack/manifest_emit.cppm index 622462c3..0e747d9a 100644 --- a/src/pack/manifest_emit.cppm +++ b/src/pack/manifest_emit.cppm @@ -96,7 +96,7 @@ struct PackageDoc { // The cfg() predicate that selects exactly `triple`. // -// ⚠️ NOT a bare `[target.''.build]` key. A bare triple is only matched +// NOT a bare `[target.''.build]` key. A bare triple is only matched // when the user passes `--target`; a plain `mcpp build` resolves the host and // used to compare it against an empty string, so the section was silently // inert. That is fixed (mcpp.build.prepare_inputs), but generating cfg() is @@ -202,7 +202,7 @@ std::string emit_package_manifest(const PackageDoc& doc) { o += std::format("[target.'{}'.build]\n", cfg_predicate_for(leg.triple)); // `-L` + `-l`, on every target including PE. // - // ⚠️ NAMING THE FILE BY PATH INSTEAD DOES NOT WORK, and it is worth + // NAMING THE FILE BY PATH INSTEAD DOES NOT WORK, and it is worth // writing down because it looks strictly better. A bare path is the one // spelling every driver accepts (cl, clang, gcc, link.exe) and it names // the exact file rather than asking the linker to search. Measured: it @@ -222,7 +222,7 @@ std::string emit_package_manifest(const PackageDoc& doc) { // these flags (cl rejects `-L`). Recorded in docs/12 rather than papered // over — mcpp's own Windows default is clang, which takes them. // - // ⚠️ `-Wl,-Bdynamic` is REQUIRED for a PE shared leg, and only there. + // `-Wl,-Bdynamic` is REQUIRED for a PE shared leg, and only there. // mcpp gives PE executables `-static` (the self-contained C++ runtime // contract: no libstdc++-6.dll beside the exe), and `-static` puts ld in // static-only mode, where it refuses an import library and reports @@ -254,7 +254,7 @@ std::string emit_package_manifest(const PackageDoc& doc) { // newer mcpp seeing this block drops that leg's library references and // uses these instead — see merge_conditional_config. // - // ⚠️ NOT FOR A PE/MinGW SHARED LEG, and this was measured rather than + // NOT FOR A PE/MinGW SHARED LEG, and this was measured rather than // reasoned. That leg's line is `-L… -Wl,-Bdynamic -lmathkit`, and // `-Wl,-Bdynamic` only works IMMEDIATELY BEFORE the `-l` it enables: // mcpp gives PE executables `-static`, which leaves ld in static-only diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 65ac5518..de0dd2c2 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -894,7 +894,7 @@ namespace detail { // No patchelf step and no wrapper script: "put the DLLs next to the exe" IS // the relocation rule on this format, which is why the row for it in the // design's layering table reads "no operation". -// ⚠️ THIS FUNCTION ONCE CRASHED THE COMPILER, and the shape it crashed on is +// THIS FUNCTION ONCE CRASHED THE COMPILER, and the shape it crashed on is // worth not reintroducing. // // clang 20.1.7 targeting the MSVC ABI — the pinned Windows toolchain — @@ -1126,7 +1126,7 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) // point becomes `run.sh` which invokes the bundled ld with // --library-path → fully portable across glibc versions. if (plan.opts.mode != Mode::Static) { - // ⚠️ THE BUILT BINARY, NOT THE STAGED COPY, and the difference is + // THE BUILT BINARY, NOT THE STAGED COPY, and the difference is // `$ORIGIN`. // // The closure is resolved by running the artifact under its own diff --git a/src/pack/relocate.cppm b/src/pack/relocate.cppm index 6d21cdab..07d6c01b 100644 --- a/src/pack/relocate.cppm +++ b/src/pack/relocate.cppm @@ -58,7 +58,7 @@ // strings, `.dynsym` names, `.gnu.version_r`). Silently truncating a symbol // name is a worse outcome than a dead string. // -// ⚠️ CONSEQUENCE FOR ANY GUARD: "this artifact carries no build-machine path" +// CONSEQUENCE FOR ANY GUARD: "this artifact carries no build-machine path" // must be asked of the DYNAMIC ENTRIES, never of the file's bytes. A // byte-pattern check reports a correctly relocated artifact as dirty. diff --git a/src/pack/strip.cppm b/src/pack/strip.cppm index 637ec2e7..ab214373 100644 --- a/src/pack/strip.cppm +++ b/src/pack/strip.cppm @@ -16,7 +16,7 @@ // differently: `[profile].strip` is "how is this built", `[pack].strip` is // "what travels". Documented in docs/02 and docs/12. // -// ⚠️ THE DIVISION BY ARTIFACT SHAPE IS LOAD-BEARING, AND IT IS MEASURED +// THE DIVISION BY ARTIFACT SHAPE IS LOAD-BEARING, AND IT IS MEASURED // // `strip --strip-all` on a `.a` removes the ARCHIVE SYMBOL INDEX, and the // package then fails at the consumer's link with a message that names neither @@ -58,7 +58,7 @@ // archiver, and for the same reason: shipping the artifact anyway is the // silent-wrong-answer this feature exists to remove. // -// ⚠️ KEYING THIS ON THE COMPILER WOULD BE WRONG IN BOTH DIRECTIONS. clang +// KEYING THIS ON THE COMPILER WOULD BE WRONG IN BOTH DIRECTIONS. clang // targeting `x86_64-windows-msvc` produces `.pdb` debug info and would be // asked to strip in-band; Apple's clang ships no `llvm-strip`, so a // compiler-keyed rule would REFUSE every `mcpp pack` on macOS for a format that diff --git a/src/pm/publisher.cppm b/src/pm/publisher.cppm index 8cfb95c2..04eafd0e 100644 --- a/src/pm/publisher.cppm +++ b/src/pm/publisher.cppm @@ -134,7 +134,7 @@ std::string platform_block(std::string_view version, const ReleaseInfo::PerPlatf // would produce a descriptor whose edges depend on which machine packed it, // and nothing downstream could tell. // -// ⚠️ A package still on the superseded `[xlings] deps` emits NO edge, exactly +// A package still on the superseded `[xlings] deps` emits NO edge, exactly // as before. That is not an oversight: `deps` is resolved for the host at // load, so the per-platform declaration a descriptor needs is already gone by // the time this runs, and writing the host's answer into all three blocks diff --git a/src/scaffold/template.cppm b/src/scaffold/template.cppm index 51853759..a6f121f0 100644 --- a/src/scaffold/template.cppm +++ b/src/scaffold/template.cppm @@ -120,7 +120,7 @@ struct TemplateMeta { std::vector injectSelfFeatures; // [template.inject] self = false // - // ⚠️ A TEMPLATE THAT MUST NOT DEPEND ON ITS OWN PACKAGE IS NOT HYPOTHETICAL. + // A TEMPLATE THAT MUST NOT DEPEND ON ITS OWN PACKAGE IS NOT HYPOTHETICAL. // // The self-dependency exists so a template cannot drift from the library // that ships it, and for almost every template that is right. It is wrong diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index 709c00e0..c94089d6 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -186,7 +186,7 @@ std::vector std_module_build_commands(const Toolchain& tc, std::string_view sysrootFlag, std::string_view cppStandardFlag) { auto relBmi = std::filesystem::relative(bmiPath, cacheDir).string(); - // ⚠️ A PACKAGE-PROVIDED std MODULE REPLACES THE TOOLCHAIN'S SYSROOT FLAGS + // A PACKAGE-PROVIDED std MODULE REPLACES THE TOOLCHAIN'S SYSROOT FLAGS // RATHER THAN BEING APPENDED TO THEM. // // Those flags describe the standard library the COMPILER ships and the C @@ -204,7 +204,7 @@ std::vector std_module_build_commands(const Toolchain& tc, // built without it is built for whatever machine is doing the building. if (!tc.stdModuleFlags.empty()) sysrootFlag = {}; const std::string& extraFlags = tc.stdModuleFlags; - // ⚠️ The codegen step compiles a BMI, which already carries what the + // The codegen step compiles a BMI, which already carries what the // headers contributed; only the machine has to be restated. See // Toolchain::stdModuleTargetFlags. const std::string& codegenFlags = tc.stdModuleTargetFlags; @@ -221,7 +221,7 @@ std::vector std_module_build_commands(const Toolchain& tc, std::string ixxFlags = (ext == ".ixx") ? " -x c++-module -Wno-include-angled-in-module-purview" : ""; - // ⚠️ AND THE RESERVED-NAME WARNING UNCONDITIONALLY, WHICH IS WHAT THE OTHER + // AND THE RESERVED-NAME WARNING UNCONDITIONALLY, WHICH IS WHAT THE OTHER // BRANCH DOES. // // `export module std;` is a reserved identifier and every standard library @@ -238,7 +238,7 @@ std::vector std_module_build_commands(const Toolchain& tc, // A warning that is correct, unavoidable, and printed on every build is // noise of the kind that hides the next one. ixxFlags += " -Wno-reserved-module-identifier"; - // ⚠️ `extraFlags` IS ON BOTH COMMANDS HERE, AND IT WAS ON NEITHER. + // `extraFlags` IS ON BOTH COMMANDS HERE, AND IT WAS ON NEITHER. // // This branch was written when a Windows host built for itself against the // MSVC STL, and `stdModuleFlags` did not exist — so the omission was not @@ -246,7 +246,7 @@ std::vector std_module_build_commands(const Toolchain& tc, // could supply its own `std` module, because that string is where the // package's own headers, `-nostdinc` and the target triple live. // - // ⚠️ Measured 2026-08-23, a Windows host cross-building for + // Measured 2026-08-23, a Windows host cross-building for // `x86_64-linux-gnu` over openkal — the command it produced carried FIVE // tokens: // @@ -339,7 +339,7 @@ std::vector std_compat_build_commands(const Toolchain& tc, { auto relBmi = std::filesystem::relative(bmiPath, cacheDir).string(); auto relStdBmi = std::filesystem::relative(stdBmiPath, cacheDir).string(); - // ⚠️ THE SAME REPLACEMENT THE `std` BUILDER MAKES, FOR THE SAME REASON. + // THE SAME REPLACEMENT THE `std` BUILDER MAKES, FOR THE SAME REASON. // // `std.compat` is a second module over the SAME library, and it therefore // needs the same headers, the same target and the same configuration. This @@ -347,7 +347,7 @@ std::vector std_compat_build_commands(const Toolchain& tc, // replaced it — so a package-provided pair had one module built against its // own libc++ and the other against the toolchain's. // - // ⚠️ It does not fail where the two are chosen. Measured on a macOS cross: + // It does not fail where the two are chosen. Measured on a macOS cross: // // error: std module precompile failed (rc=1): // …/openkal-llvm-runtime/llvm-generated/std.compat.cppm:16 @@ -366,7 +366,7 @@ std::vector std_compat_build_commands(const Toolchain& tc, // Note: the path after = must NOT be shell-quoted separately; the // entire -fmodule-file flag is a single token to the compiler. // - // ⚠️⚠️ ABSOLUTE PATHS AND NO `cd`, AND ONE FORM RATHER THAN TWO. + // ABSOLUTE PATHS AND NO `cd`, AND ONE FORM RATHER THAN TWO. // // This used to be `cd && … pcm.cache/std.pcm …`. `cd X && …` // DOES NOT CHANGE THE DRIVE in cmd.exe — the build cache lives under the @@ -380,7 +380,7 @@ std::vector std_compat_build_commands(const Toolchain& tc, // // — and `std.pcm` had been built successfully one command earlier. // - // ⚠️ The obvious repair was a `#if defined(_WIN32)` branch, which is what + // The obvious repair was a `#if defined(_WIN32)` branch, which is what // the `std` builder above has. It was written and then withdrawn: a branch // that only compiles on one platform is a branch this machine cannot check, // and every defect this session found in the host dimension had exactly diff --git a/src/toolchain/compat.cppm b/src/toolchain/compat.cppm index 7521712a..acf165e4 100644 --- a/src/toolchain/compat.cppm +++ b/src/toolchain/compat.cppm @@ -108,7 +108,7 @@ std::optional normalize_spec(std::string_view compilerIn, out.version = version; // ── canonical families pass through ───────────────────────────────────── - // ⚠️ `openkal-llvm` NORMALISES TO `llvm`, AND USED TO BE A FAMILY OF ITS + // `openkal-llvm` NORMALISES TO `llvm`, AND USED TO BE A FAMILY OF ITS // OWN. It named the same payload and carried a fact about where the TARGET // SIDE comes from — which `mcpp.targetside` now resolves from what packages // declare, after the graph exists, where the fact actually lives. The diff --git a/src/toolchain/gcc.cppm b/src/toolchain/gcc.cppm index d50b4c75..1a425995 100644 --- a/src/toolchain/gcc.cppm +++ b/src/toolchain/gcc.cppm @@ -26,7 +26,7 @@ find_binutils_bin(const std::filesystem::path& compilerBin); // `-B`, or empty when it must not be pointed at one — which is every toolchain // that is not a glibc GCC. // -// ⭐ ONE STATEMENT OF A GUARD THAT HAD THREE COPIES. musl-cross-make and +// ONE STATEMENT OF A GUARD THAT HAD THREE COPIES. musl-cross-make and // MinGW-w64 payloads bundle their own as/ld, and for a cross target the host's // binutils would mis-assemble — the Linux `as` rejects MinGW's PE/SEH // directives `.def` / `.seh_proc`. Only the glibc GCC needs the external diff --git a/src/toolchain/hostflags.cppm b/src/toolchain/hostflags.cppm index fabbe241..f01e8730 100644 --- a/src/toolchain/hostflags.cppm +++ b/src/toolchain/hostflags.cppm @@ -77,7 +77,7 @@ struct HostFlagOptions { // rejects a module built for a different deployment target outright. std::string macosDeploymentTarget; - // ⭐⭐ DOES THE TARGET'S C LIBRARY COME FROM A DIRECTORY THAT EXISTED + // DOES THE TARGET'S C LIBRARY COME FROM A DIRECTORY THAT EXISTED // BEFORE DEPENDENCY RESOLUTION? — `plan.targetSide.cAbi.prebuilt()`, READ // rather than derived. // @@ -88,11 +88,11 @@ struct HostFlagOptions { // while depending on nothing answers yes to the first and no to the // second. // - // ⚠️ THE LINK SIDE OF THIS DEFECT WAS FIXED IN 2026.8.26.1 (#511) AND THIS + // THE LINK SIDE OF THIS DEFECT WAS FIXED IN 2026.8.26.1 (#511) AND THIS // SIDE WAS NOT. Measured on 2026.8.26.2, same machine, same compiler, same // target, differing only in whether it was spelled out: // - // $ mcpp build ldflags: identical ✔ + // $ mcpp build ldflags: identical // $ mcpp build --target x86_64-unknown-linux-gnu cxxflags: SIX tokens gone // // --no-default-config -nostdinc++ @@ -106,7 +106,7 @@ struct HostFlagOptions { // payload — the silent ABI mix; on one without, it fails naming the // payload. // - // ⭐ e2e 295 states the invariant ("naming the host's own target changes + // e2e 295 states the invariant ("naming the host's own target changes // nothing") and compared only `^ldflags`, so the identity held one line // above the line where it did not. It now compares both. // @@ -161,7 +161,7 @@ std::vector host_compile_tokens(const Toolchain& tc, const auto dm = resolve_clang_driver(tc); const auto lm = resolve_link_model(tc); - // ⭐⭐ THE TRIPLE, SAID OUT LOUD, WHEN NOTHING ELSE SAYS IT. + // THE TRIPLE, SAID OUT LOUD, WHEN NOTHING ELSE SAYS IT. // // Every hosted cross this build tool could do was served by a payload whose // driver had exactly one target — `x86_64-w64-mingw32-g++` needs no @@ -173,7 +173,7 @@ std::vector host_compile_tokens(const Toolchain& tc, // which emits every format it was built with, and which will emit for THIS // machine unless told otherwise. // - // ⚠️ Measured 2026-08-23. A build for `aarch64-macos` with an explicit + // Measured 2026-08-23. A build for `aarch64-macos` with an explicit // `[target.aarch64-macos] toolchain = "llvm@…"` resolved the whole graph, // took the C library's aarch64 headers, and compiled with no `--target` — // host code generation, target declarations. It was caught by an assertion @@ -188,11 +188,11 @@ std::vector host_compile_tokens(const Toolchain& tc, // reads it. if (!tc.crossTargetFlag.empty()) out.push_back(tc.crossTargetFlag); - // ⭐⭐ AND WHAT A `throw` AND A `thread_local` COMPILE INTO, WHICH IS A + // AND WHAT A `throw` AND A `thread_local` COMPILE INTO, WHICH IS A // PROPERTY OF THE GRAPH AND NOT OF ANY ONE PACKAGE — see // `graph_runtime_compile_flags` for what and why. // - // ⚠️ IT WAS DECLARED PER-PACKAGE, WHICH IS EXACTLY AS FAR AS IT REACHED. + // IT WAS DECLARED PER-PACKAGE, WHICH IS EXACTLY AS FAR AS IT REACHED. // `openkal-llvm-runtime` set `-fdwarf-exceptions` in its own `[build]`, so // its objects agreed with each other and nothing else did. Measured // 2026-08-23 — every object compiled, and the link said: @@ -215,7 +215,7 @@ std::vector host_compile_tokens(const Toolchain& tc, // returning early. const bool trustCfg = !bypassCfg && dm.hasCfg; - // ⚠️ AND NOT WHEN THE TARGET SIDE COMES FROM THE GRAPH — the compile-side + // AND NOT WHEN THE TARGET SIDE COMES FROM THE GRAPH — the compile-side // counterpart of the replacement `flags.cppm` makes on the link line. // // These tokens are the payload's: `-isystem /include/c++/v1` and @@ -223,7 +223,7 @@ std::vector host_compile_tokens(const Toolchain& tc, // library are packages, and the payload's copies are built for the machine // doing the building. // - // ⚠️ `-nostdinc++` DOES NOT REMOVE THEM, which is what makes this its own + // `-nostdinc++` DOES NOT REMOVE THEM, which is what makes this its own // fix rather than a flag. That option suppresses the DRIVER's own C++ // search; a path put there explicitly with `-isystem` stays. Measured // 2026-08-23, cross-compiling openkal-windows — a package that uses no C++ @@ -235,7 +235,7 @@ std::vector host_compile_tokens(const Toolchain& tc, // mingw's own header asked for ``, and the payload's libc++ was // still ahead of the sysroot that had just been pointed at the right place. // - // ⭐ READ, NOT DERIVED — see HostFlagOptions::cAbiPrebuilt for the + // READ, NOT DERIVED — see HostFlagOptions::cAbiPrebuilt for the // measurement that replaced `!tc.crossTargetFlag.empty()` here. This site // and `flags.cppm`'s link side now ask one question of one value, so they // cannot disagree. @@ -245,7 +245,7 @@ std::vector host_compile_tokens(const Toolchain& tc, for (auto& t : dm.compile_tokens(esc, opt.clangStdlibSelect)) out.push_back(t); } else if (bypassCfg) { - // ⭐⭐ THE BYPASS IS NOT PART OF THE PAYLOAD'S HEADER SET, AND IT WAS + // THE BYPASS IS NOT PART OF THE PAYLOAD'S HEADER SET, AND IT WAS // BEING SUPPRESSED WITH IT. // // The payload's `-isystem` rows describe a C library this target does @@ -255,7 +255,7 @@ std::vector host_compile_tokens(const Toolchain& tc, // command line depend on what happened to be installed when the // payload landed. // - // ⚠️ Measured on 2026.8.26.2: `mcpp build --target ` + // Measured on 2026.8.26.2: `mcpp build --target ` // dropped `--no-default-config`, so clang read `bin/clang++.cfg` and // the build silently inherited that machine's install. It is also what // made a hand-written `-clang++.cfg` a working workaround for diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index dd41437d..dc7fa627 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -63,7 +63,7 @@ std::vector parse_version_components(std::string_view s) { // "15.1" → highest 15.1.Y // "15.1.0" → exact match (or empty if not present) // Empty result = no match. -// ⭐ EXPORTED SO THAT "WHICH VERSION OF THIS FAMILY" HAS ONE ANSWER. +// EXPORTED SO THAT "WHICH VERSION OF THIS FAMILY" HAS ONE ANSWER. // `mcpp toolchain default llvm` resolves a bare family through these two, and // `prepare_build` now has to answer the same question when the dependency graph // asks for a compiler family by name. A second implementation there would be @@ -267,7 +267,7 @@ export bool remove_payload_tree(const std::filesystem::path& root, // Still held. Move the held FILES out instead of waiting on a process we // do not own. // - // ⚠️ NOT by renaming the payload directory. Windows lets you rename an + // NOT by renaming the payload directory. Windows lets you rename an // open FILE — that is how an updater replaces a running .exe — but it // does NOT let you rename a DIRECTORY that contains one. An earlier // version of this function renamed `root` and was wrong about exactly @@ -432,7 +432,7 @@ bool version_greater(const std::string& a, const std::string& b) { return b < a; // stable tie-break for non-numeric tails } -// ⭐⭐ `json` SUPPRESSES EVERY HUMAN LINE AND EMITS THE SAME TWO TABLES AS DATA. +// `json` SUPPRESSES EVERY HUMAN LINE AND EMITS THE SAME TWO TABLES AS DATA. // // The two tables here — installed toolchains, and target rows with their status // — are what `tests/matrix/scan.sh` and four e2e tests need to know before they @@ -442,7 +442,7 @@ bool version_greater(const std::string& a, const std::string& b) { // and the one that read `$NF` picked up `(default)` — a value that appears on // exactly the row most likely to be chosen. // -// ⚠️ ONE SOURCE, TWO RENDERINGS. The rows are built once above and rendered +// ONE SOURCE, TWO RENDERINGS. The rows are built once above and rendered // either way at the bottom; a second enumeration for the machine path is how // the two come to disagree about what is installed. export int toolchain_list(const mcpp::config::GlobalConfig& cfg, @@ -543,20 +543,20 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, // MSVC is never in xpkgs — it's located on the machine. Show it so // `toolchain list` reflects everything `toolchain default` accepts. // - // ⚠️ It joins `toolchains` on the machine path rather than getting a + // It joins `toolchains` on the machine path rather than getting a // section of its own: to a client, "a compiler I can select" is one list, // and where mcpp found it is a property of the entry, not a reason for a // second array. if (mcpp::platform::is_windows && json) { if (auto inst = mcpp::toolchain::msvc::detect_installation()) - // ⚠️⚠️ `version` MUST BE WHAT `--toolchain` ACCEPTS, AND THIS EMITTED + // `version` MUST BE WHAT `--toolchain` ACCEPTS, AND THIS EMITTED // WHAT cl.exe REPORTS. // // A system Visual Studio is selected as `msvc@system`; its COMPILER // version is `19.44.35228`, which `parse_toolchain_spec` rejects by // design — it "names a COMPILER version, not a toolset". // - // ⭐ THE MACHINE INTERFACE HAS TO ROUND-TRIP. A consumer reads + // THE MACHINE INTERFACE HAS TO ROUND-TRIP. A consumer reads // `family` and `version`, joins them, and hands the result back; // `tests/matrix/scan.sh` did exactly that and every msvc cell came // back `build-failed` with mcpp refusing its own output. Measured on @@ -596,7 +596,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, std::string target; // canonical triple std::string note; // "host" / "static" / "PE" / "cross" tags std::string toolchain; // "gcc 16.1.0" or "—" - // ⚠️⚠️ `toolchain` MEANS TWO DIFFERENT THINGS DEPENDING ON THE ROW, and + // `toolchain` MEANS TWO DIFFERENT THINGS DEPENDING ON THE ROW, and // that is fine for a column a person reads and wrong for a field a // program reads. On an INSTALLED row it is the payload that is here; on // a vocabulary row it is the target table's convention pin. A row can @@ -607,7 +607,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, // this field and got `x86_64-linux-gnu` on the CI runner, where the pin // is empty. It then asserted a refusal that correctly did not happen. // - // ⭐ So the convention travels in its own field, empty when there is + // So the convention travels in its own field, empty when there is // none. Same defect family as the release this was written for. std::string pin; // the target table's convention pin, or empty std::string status; // installed | available | planned @@ -667,7 +667,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, auto t = mcpp::toolchain::triple::parse(info.canonical); if (!t) continue; bool planned = info.tier == "planned"; - // ⭐⭐ A ROW NO PAYLOAD SERVES IS NOT THEREFORE UNBUILDABLE. + // A ROW NO PAYLOAD SERVES IS NOT THEREFORE UNBUILDABLE. // // `host_can_serve` answers "can this host serve the target FROM A // PAYLOAD". Dropping the row presents that as "can this host build for @@ -683,7 +683,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, // — because its system came from the dependency graph, which is what // the row's own note in the target table says happens. // - // ⚠️ AND NOT EVERY ABSENT ROW IS THAT. `x86_64-windows-msvc` and + // AND NOT EVERY ABSENT ROW IS THAT. `x86_64-windows-msvc` and // `aarch64-macos` are absent on a Linux host CORRECTLY: MSVC and the // macOS SDK are host-only and no dependency substitutes for them. // The discriminator is already in the table and needs no new field — @@ -975,7 +975,7 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg, // pipeline shared by every toolchain install path, dispatched and // made idempotent inside ensure_post_install_fixup. // - // ⚠️ RESOLVE THE RUNTIME BINDING HERE, do not let the fixup guess. + // RESOLVE THE RUNTIME BINDING HERE, do not let the fixup guess. // The fixup is a CONSUMER of RuntimeBinding (`prepare.cppm` says so // where it does the same thing) and it no longer derives an identity // of its own — so a caller that passes nothing gets no fixup at all. diff --git a/src/toolchain/post_install.cppm b/src/toolchain/post_install.cppm index 7327a500..73790f4c 100644 --- a/src/toolchain/post_install.cppm +++ b/src/toolchain/post_install.cppm @@ -410,12 +410,12 @@ select_glibc_payload_lib(const std::filesystem::path& glibcRoot, runtimeId)); } - // ⭐ ONE RESOLVER, TWO CALLERS. `payload_dir_for_version` also answers + // ONE RESOLVER, TWO CALLERS. `payload_dir_for_version` also answers // `probe`'s compile-side discovery; see its own header for why a request // and a resolution are two vocabularies, and why a unique component-wise // refinement is an answer while a directory-order pick is not. // - // ⚠️ THE FIRST VERSION OF THIS FIX SPELLED IT HERE, and that left the other + // THE FIRST VERSION OF THIS FIX SPELLED IT HERE, and that left the other // caller: the toolchain then installed and the compile line came out // without the glibc include directory, which reads as // `features.h: No such file` from inside libstdc++'s own headers. @@ -579,7 +579,7 @@ ensure_post_install_fixup(const mcpp::config::GlobalConfig& cfg, std::filesystem::path glibcLibDir; std::string skipped; if constexpr (mcpp::platform::is_linux) { - // ⚠️ ONE DERIVATION, AND IT IS NOT THIS FUNCTION'S. + // ONE DERIVATION, AND IT IS NOT THIS FUNCTION'S. // // The caller resolves a RuntimeBinding first and hands the identity in; // `prepare.cppm` says so where it does it ("The fixup is itself a diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index adb0c9dd..dff4b0a6 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -374,11 +374,11 @@ payload_root_for_binding(const std::filesystem::path& compilerBin, const auto name = binding.substr(0, at); const auto version = std::string(binding.substr(at + 1)); - // ⭐ THE DIRECTORY IS NAMED AFTER WHAT THE REQUEST RESOLVED TO, NOT AFTER + // THE DIRECTORY IS NAMED AFTER WHAT THE REQUEST RESOLVED TO, NOT AFTER // THE REQUEST. `payload_dir_for_version` is the one answer to that, shared // with the toolchain post-install fixup — see its header. // - // ⚠️ THIS SITE'S FAILURE DOES NOT NAME A VERSION. The fixup at least says + // THIS SITE'S FAILURE DOES NOT NAME A VERSION. The fixup at least says // which payload it wanted; here the include directory is simply never // added, and what the user reads comes from inside libstdc++: // diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 3c15fa7e..609bcc34 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -28,7 +28,7 @@ import mcpp.toolchain.triple; export namespace mcpp::toolchain { -// ⚠️ A FAMILY IS A COMPILER, AND `openkal-llvm` WAS NOT ONE. +// A FAMILY IS A COMPILER, AND `openkal-llvm` WAS NOT ONE. // // It named the same llvm payload as `Llvm` and existed to carry one fact: that // a project's headers, C library, C++ runtime and platform implementation come @@ -413,7 +413,7 @@ XimToolchainPackage to_xim_package(const ToolchainSpec& spec) { return pkg; } if (spec.family == Family::Llvm) { - // ⭐ ONE PAYLOAD. The `openkal-llvm` spelling normalises to this family and + // ONE PAYLOAD. The `openkal-llvm` spelling normalises to this family and // installs nothing of its own — it is a statement about where the // TARGET SIDE comes from, and the compiler is the llvm payload either // way. A user who has one has both. @@ -425,7 +425,7 @@ XimToolchainPackage to_xim_package(const ToolchainSpec& spec) { // Family::Gcc — the target decides the payload. const auto& t = spec.target; - // ⚠️⚠️ `&& t.os == "linux"` — AND THE PARAGRAPH BELOW ALREADY SAID SO. + // `&& t.os == "linux"` — AND THE PARAGRAPH BELOW ALREADY SAID SO. // // "Canonical linux-musl triples coincide with the GNU tool spelling" is a // statement about linux-musl, and the condition asked only whether the C @@ -604,7 +604,7 @@ bool host_can_serve(const triple::Triple& target) { if (target.os == "linux") { if constexpr (mcpp::platform::is_linux) { - // ⚠️⚠️ "SELF-CONTAINED" IS ABOUT THE PAYLOAD'S CONTENTS, NOT ABOUT + // "SELF-CONTAINED" IS ABOUT THE PAYLOAD'S CONTENTS, NOT ABOUT // WHICH HOSTS IT IS PUBLISHED FOR — and this line read it as both. // // A musl payload really does carry its own sysroot, so no host-side @@ -618,7 +618,7 @@ bool host_can_serve(const triple::Triple& target) { // admitted, resolved a package with no aarch64 asset, and failed at // install — `mismatch / build-failed` in the target matrix, twice. // - // ⭐ The native row stays reachable on every arch: `musl-gcc` + // The native row stays reachable on every arch: `musl-gcc` // publishes both, which is why `aarch64-linux-musl` is `ok` there. const bool crossArch = target.arch != mcpp::platform::host_arch; if (target.is_musl()) @@ -634,7 +634,7 @@ bool host_can_serve(const triple::Triple& target) { && target.is_musl() && target.arch == mcpp::platform::host_arch; } - // ⚠️ THE mingw CROSS IS PUBLISHED FOR ONE HOST ARCH. `mingw-cross-gcc` + // THE mingw CROSS IS PUBLISHED FOR ONE HOST ARCH. `mingw-cross-gcc` // declares `archs = { "x86_64" }`, so a Linux host that is not x86_64 // cannot obtain it — measured on ubuntu-24.04-arm, where the row was // listed and its refusal carried no reason at all (`unsupported / other`). @@ -643,7 +643,7 @@ bool host_can_serve(const triple::Triple& target) { return mcpp::platform::is_linux && mcpp::platform::host_arch == "x86_64"; } - // ⚠️⚠️ PE + musl HAS NO PAYLOAD ON ANY HOST, INCLUDING WINDOWS. + // PE + musl HAS NO PAYLOAD ON ANY HOST, INCLUDING WINDOWS. // // `triple::pin_is_capability()` already says so — no gcc emits a PE with a // musl C library, and LLVM cannot spell the triple — and chapter 16 states @@ -662,7 +662,7 @@ bool host_can_serve(const triple::Triple& target) { // Linux the same cell already answered `host-cannot-serve`, which is the // right answer everywhere. // - // ⭐ The graph path is untouched: this refusal is held and released only + // The graph path is untouched: this refusal is held and released only // when nothing supplies the target's system, and `graph × windows-musl` is // `ok` on both hosts. if (target.is_pe() && target.is_musl()) return false; @@ -676,7 +676,7 @@ bool host_can_serve(const triple::Triple& target) { // unlike every hosted case above, which needs a C library that only exists // for some (host, target) pairs. // - // ⚠️ Serviceable is not the same as complete: a target with no C library + // Serviceable is not the same as complete: a target with no C library // still links only `-nostdlib` programs. That gap belongs to the ecosystem // (a libc wrapper package), and saying `false` here would hide it behind // "this host cannot build it", which is the wrong diagnosis. @@ -686,7 +686,7 @@ bool host_can_serve(const triple::Triple& target) { } std::vector available_toolchain_indexes() { - // ⚠️⚠️ NOT EVERY FAMILY EXISTS FOR EVERY (OS, ARCH), AND THIS LIST USED TO + // NOT EVERY FAMILY EXISTS FOR EVERY (OS, ARCH), AND THIS LIST USED TO // SAY OTHERWISE. // // The branches below are per-OS and there were none per-ARCH, so an aarch64 @@ -701,12 +701,12 @@ std::vector available_toolchain_indexes() { // promised would work. Same family as the rest of this release: a table // that answers a narrower question than the one it is asked. // - // ⭐ THIS IS A POLICY STATEMENT, NOT A COPY OF THE INDEX. It says which + // THIS IS A POLICY STATEMENT, NOT A COPY OF THE INDEX. It says which // families mcpp SUPPORTS on this host — the same kind of statement `tier` // makes for a target row — and the plan that retires it is // `.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md` §P1. // - // ⚠️ AND ITS PREMISE IS ASSERTED IN CI, so it cannot outlive its reason. + // AND ITS PREMISE IS ASSERTED IN CI, so it cannot outlive its reason. // `ci-target-matrix.yml`'s aarch64 job checks that no linux-aarch64 llvm // asset has appeared; the day one does, that step reds and names this // gate. A deferral nobody rechecks is indistinguishable from a defect. diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index c60cfd71..243dfdb2 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -249,7 +249,7 @@ std::expected ensure_built( HostFlagOptions hopt; hopt.cfgBypass = HostFlagOptions::CfgBypass::Always; hopt.clangStdlibSelect = true; - // ⚠️ THE STD MODULE IS A TRANSLATION UNIT OF THIS BUILD, so it must see the + // THE STD MODULE IS A TRANSLATION UNIT OF THIS BUILD, so it must see the // same header set every unit importing it sees. Taken from the toolchain // rather than re-derived: prepare records the resolved answer there once, // and a std BMI built against a different C library than its importers is diff --git a/src/xlings/xlings.cppm b/src/xlings/xlings.cppm index c131a545..150d1696 100644 --- a/src/xlings/xlings.cppm +++ b/src/xlings/xlings.cppm @@ -46,7 +46,7 @@ namespace pinned { // actions, which is how CI's sandbox sat on 0.4.30 unnoticed while // everything else had moved on. Don't reintroduce a hand-maintained list. // - // ⚠️ THIS IS A FLOOR, not just the current pick, and it has been raised + // THIS IS A FLOOR, not just the current pick, and it has been raised // twice for reasons that both still hold. // // First, at 2026.8.27.5. Below 2026.8.27.2 the bundled xlings takes a @@ -129,7 +129,7 @@ namespace paths { // Where that package's payload is, or nullopt if it is not installed. // - // ⚠️ A PINNED ref resolves to exactly its version and to nothing else. A + // A PINNED ref resolves to exactly its version and to nothing else. A // build that asked for 1.8.12 and silently got 1.9.0 is the kind of answer // that is only discovered later, in the artifact. An unpinned ref takes // the highest version present — compared by numeric segments, because a @@ -207,7 +207,7 @@ namespace paths { find_home_tool(std::string_view tool, std::string_view requiredRelPath = {}); - // ⭐⭐ WHICH INSTALLED PAYLOAD DIRECTORY ANSWERS A REQUESTED VERSION. + // WHICH INSTALLED PAYLOAD DIRECTORY ANSWERS A REQUESTED VERSION. // // A request and a resolution are two vocabularies for one fact. A // RuntimeBinding carries the DECLARED version (`glibc@2.44`); xlings names @@ -216,7 +216,7 @@ namespace paths { // every lookup that spells the directory by the declared version stops // finding it. // - // ⚠️ STATED ONCE HERE BECAUSE IT HAS TWO CALLERS AND THEY FAILED + // STATED ONCE HERE BECAUSE IT HAS TWO CALLERS AND THEY FAILED // SEPARATELY. `post_install`'s toolchain fixup and `probe`'s compile-side // payload discovery each spelled it themselves; fixing one left the other, // and the second failure did not name a version at all — it read @@ -225,7 +225,7 @@ namespace paths { // // because the glibc include directory had simply not been added. // - // ⭐ A REFINEMENT, NOT A DIRECTORY-ORDER PICK. `2.44.2` is what the request + // A REFINEMENT, NOT A DIRECTORY-ORDER PICK. `2.44.2` is what the request // `2.44` resolved to: its version COMPONENTS begin with the requested ones. // `2.4` does not answer `2.44`, because the comparison is per component and // not per character. And when two payloads both refine the request, this @@ -810,7 +810,7 @@ XpkgRef parse_xpkg_ref(std::string_view spec) { // Where that package's payload is, or nullopt if it is not installed. // -// ⚠️ A PINNED ref resolves to exactly its version and to nothing else. A build +// A PINNED ref resolves to exactly its version and to nothing else. A build // that asked for 1.8.12 and silently got 1.9.0 is the kind of answer that is // only discovered later, in the artifact. An unpinned ref takes the highest // version present — compared by numeric segments, because a plain string sort @@ -902,7 +902,7 @@ find_sibling_tool(const std::filesystem::path& compilerBin, } std::optional active_home_xpkgs() { - // ⭐⭐ `mcpp::home::root()`, NOT A FOURTH DERIVATION OF IT. + // `mcpp::home::root()`, NOT A FOURTH DERIVATION OF IT. // // This function used to resolve the home itself — `$MCPP_HOME`, else // `$HOME/.mcpp` — which is two of the three answers `mcpp.home` gives. The @@ -913,7 +913,7 @@ std::optional active_home_xpkgs() { // everything else in the process — and what payload discovery produces is // `-isystem` rows on every compile command. // - // ⚠️ `mcpp.home`'s own header opens with this: "Every path under the mcpp + // `mcpp.home`'s own header opens with this: "Every path under the mcpp // home must be derived from here. Before #311 this logic existed in three // places … the copies drifted." This was the fourth copy, and it drifted // the same way. @@ -1007,7 +1007,7 @@ find_sibling_package(const std::filesystem::path& compilerBin, if (auto found = find_package_in_xpkgs(*xpkgs, packageName, requiredRelPath)) return found; - // ⚠️⚠️ THE `~/.xlings` FALLBACK IS GONE, AND ITS REMOVAL IS THE POINT. + // THE `~/.xlings` FALLBACK IS GONE, AND ITS REMOVAL IS THE POINT. // // It used to read: "Also check ~/.xlings/data/xpkgs/ (xlings global home) // as fallback." That was written when one machine had one home. It means @@ -1023,7 +1023,7 @@ find_sibling_package(const std::filesystem::path& compilerBin, // half of mcpp#514, where a project's dependency units carried `-isystem` // rows naming a home the build was not using. // - // ⭐ THE DIRECTION IS THE SAFE ONE. Not finding a payload is reported — + // THE DIRECTION IS THE SAFE ONE. Not finding a payload is reported — // `probe.cppm` already has the verbose branch for it, and a glibc build // that then fails at `` says so at the first compile. // Finding the WRONG one says nothing at all, and this codebase has paid @@ -1035,7 +1035,7 @@ find_sibling_package(const std::filesystem::path& compilerBin, std::optional payload_dir_for_version(const std::filesystem::path& packageRoot, std::string_view version) { - // ⚠️ AN EMPTY REQUEST IS NOT A REQUEST FOR EVERYTHING. `packageRoot / ""` + // AN EMPTY REQUEST IS NOT A REQUEST FOR EVERYTHING. `packageRoot / ""` // is `packageRoot` itself, which IS a directory — so without this the // exact-match branch below would hand back the package root and every // caller would treat that container as a payload. Both callers today diff --git a/tests/e2e/130_freestanding_riscv_build_and_run.sh b/tests/e2e/130_freestanding_riscv_build_and_run.sh index b04012f3..c23106df 100755 --- a/tests/e2e/130_freestanding_riscv_build_and_run.sh +++ b/tests/e2e/130_freestanding_riscv_build_and_run.sh @@ -4,7 +4,7 @@ # RISC-V firmware image from a C++20 MODULE interface unit plus assembly, and # `mcpp run --target-triple` boots it in qemu. # -# ⚠️ SOFT `requires:`, and the guard lives elsewhere on purpose. +# SOFT `requires:`, and the guard lives elsewhere on purpose. # # `requires-hard:` (missing capability FAILS instead of skipping) exists and is # the right tool for "this runner is misconfigured" — but it cannot express @@ -159,7 +159,7 @@ readelf -h "$img" | grep -q '0x80200000' || { readelf -h "$img" | grep -i entry; exit 1; } # ── run ───────────────────────────────────────────────────────────────────── -# ⚠️ BOTH spellings, and the pairing is the test. +# BOTH spellings, and the pairing is the test. # # `run` also takes a POSITIONAL binary name, and the arg parser falls back # from an unset option to a positional of the same name. While that positional diff --git a/tests/e2e/131_freestanding_bsp_supplies_everything.sh b/tests/e2e/131_freestanding_bsp_supplies_everything.sh index ad42d0cf..3a4c0bc2 100755 --- a/tests/e2e/131_freestanding_bsp_supplies_everything.sh +++ b/tests/e2e/131_freestanding_bsp_supplies_everything.sh @@ -10,7 +10,7 @@ # it — a user who never types picolibc, compiler-rt, crt0, `-nostdlib`, # `-mcmodel` or a load address, and still gets `printf` with a float. # -# ⚠️ THE SEAM UNDER TEST (measured 2026-08-19, probe Z1) +# THE SEAM UNDER TEST (measured 2026-08-19, probe Z1) # # link-search / link-lib / link-script Scope::LinkGlobal → reach the consumer # include-dir / cflag / cfg Scope::PackagePrivate → do NOT @@ -93,7 +93,7 @@ int main() { std::println("mcpp:link-lib=semihost"); std::println("mcpp:link-lib=clang_rt.builtins-{}", rt); std::println("mcpp:link-script={}/picolibcpp.ld", lib); - // ⭐ The runner too: the package that knows the board resolves the + // The runner too: the package that knows the board resolves the // emulator absolutely and says how to drive it. The consumer's manifest // below has no [target.*] section at all — that is N1 of the plan. const char* qemu = std::getenv("MCPP_XPKG_XIM_QEMU_RISCV_DIR"); @@ -145,7 +145,7 @@ extern "C" int main() { } EOF -# ⚠️ This manifest IS the assertion. Nothing here names picolibc, compiler-rt, +# This manifest IS the assertion. Nothing here names picolibc, compiler-rt, # crt0, a linker script, a load address, -nostdlib, -mcmodel — or an emulator. # There is no [target.*] section at all: the runner comes from the BSP. cat > mcpp.toml <<'EOF' @@ -171,7 +171,7 @@ grep -q 'MALLOC-OK' run.log || { # ── the two include seams, which are NOT the same seam ────────────────────── # -# ⚠️ This used to test both with `#include ` and expect it to FAIL. +# This used to test both with `#include ` and expect it to FAIL. # That criterion was wrong once the target's C library became the target's: # `` now arrives the way it does on a hosted build, from mcpp, for # every unit — nobody declares glibc on x86_64 either. Testing the private @@ -218,7 +218,7 @@ if "$MCPP" build --target riscv64-none-elf > leak.log 2>&1; then exit 1 fi -# ── ⚠️ build, THEN run: the sequence that shipped broken in 2026.8.19.2 ───── +# ── build, THEN run: the sequence that shipped broken in 2026.8.19.2 ───── # # `mcpp run` on its own was correct; the SECOND invocation was not. With the # target named in the MANIFEST rather than on the command line, `mcpp build` @@ -228,7 +228,7 @@ fi # Running `target/riscv64-none-elf/.../bin/firmware` ← no emulator # exit=1 # -# ⚠️ A FRESH project, not the one above, and that is load-bearing. The run +# A FRESH project, not the one above, and that is load-bearing. The run # fast path also requires mcpp.toml to be older than build.ninja, and a # rebuild does not rewrite build.ninja when its content is unchanged — so in a # directory whose manifest has been edited in place the fast path is already diff --git a/tests/e2e/132_freestanding_test_and_artifacts.sh b/tests/e2e/132_freestanding_test_and_artifacts.sh index 4d9b17af..c042edb1 100755 --- a/tests/e2e/132_freestanding_test_and_artifacts.sh +++ b/tests/e2e/132_freestanding_test_and_artifacts.sh @@ -2,7 +2,7 @@ # requires: llvm qemu-riscv unix-shell # `mcpp test` on bare metal, and the artifact set a flasher needs. # -# ⚠️ TWO PLAN ASSUMPTIONS THIS TEST EXISTS BECAUSE THEY WERE WRONG +# TWO PLAN ASSUMPTIONS THIS TEST EXISTS BECAUSE THEY WERE WRONG # # The design called for a `batch` mode (all cases in one image) and a # structured stdout protocol, on two premises. Both were measured false: @@ -72,7 +72,7 @@ int main() { std::println("mcpp:link-lib=clang_rt.builtins-{}", rt); std::println("mcpp:link-script={}/picolibcpp.ld", lib); - // ⭐ The runner, from the package that knows the board. The consumer's + // The runner, from the package that knows the board. The consumer's // manifest below has no [target.*] section at all. const char* qemu = std::getenv("MCPP_XPKG_XIM_QEMU_RISCV_DIR"); if (qemu && *qemu) { @@ -122,7 +122,7 @@ import board; extern "C" int main() { board::print("case three\n"); return 1; } EOF -# ⚠️ No [target.*] section: the runner comes from the BSP. +# No [target.*] section: the runner comes from the BSP. cat > mcpp.toml <<'EOF' [package] name = "fw" @@ -144,7 +144,7 @@ if "$MCPP" test --target riscv64-none-elf > test.log 2>&1; then fi grep -q 'ok_one ... ok' test.log || { cat test.log; echo "ok_one did not pass"; exit 1; } grep -q 'ok_two ... ok' test.log || { cat test.log; echo "ok_two did not pass"; exit 1; } -# ⚠️ The failure has to be NAMED. "2 passed; 1 failed" without a name is a +# The failure has to be NAMED. "2 passed; 1 failed" without a name is a # harness that tells you to go looking. grep -q 'deliberate_fail ... FAIL' test.log || { cat test.log; echo "the failing case was not named"; exit 1; } @@ -160,7 +160,7 @@ elf="$(find target/riscv64-none-elf -name firmware -type f | head -1)" grep -q 'Size .*text .*data .*bss' build.log || { cat build.log; echo "no size summary"; exit 1; } -# ⚠️ The one that matters: `.bin` is a real edge on the ELF, not a side effect +# The one that matters: `.bin` is a real edge on the ELF, not a side effect # of the link command happening to run. Change a source and its CONTENT must # change — a mtime-only check would pass even if the edge were missing. before="$(sha256sum "$elf.bin" | cut -d' ' -f1)" @@ -171,7 +171,7 @@ after="$(sha256sum "$elf.bin" | cut -d' ' -f1)" echo ".bin did not change after a source edit — it is not a real ninja edge" exit 1; } -# ⚠️ The `.map` is written by a FLAG on the link command, not by its own edge, +# The `.map` is written by a FLAG on the link command, not by its own edge, # so it is easy to leave undeclared — and then nothing tracks it. Delete it and # ninja must put it back; without the implicit-output declaration the ELF is # up to date, ninja has nothing to do, and the map stays gone. diff --git a/tests/e2e/133_freestanding_std_subset.sh b/tests/e2e/133_freestanding_std_subset.sh index 46a89eb7..a28835bc 100755 --- a/tests/e2e/133_freestanding_std_subset.sh +++ b/tests/e2e/133_freestanding_std_subset.sh @@ -8,7 +8,7 @@ # them is one per-target file, `__config_site`, which the llvm payload ships # only for its own host triple. # -# ⚠️ MEASURED, and the control group is what makes the number mean anything: +# MEASURED, and the control group is what makes the number mean anything: # with a synthesised `__config_site`, 103 of libc++'s 110 headers compile for # riscv64-none-elf. The 7 that fail (generator, hazard_pointer, rcu, # spanstream, stacktrace, stdfloat, text_encoding) fail on an x86_64 host with @@ -50,7 +50,7 @@ cd "$TMP" "$MCPP" new board > /dev/null cd board rm -f src/main.cpp tests/*.cpp 2>/dev/null || true -# ⚠️ NO libc anywhere in this package. The target's C library is the TARGET's, +# NO libc anywhere in this package. The target's C library is the TARGET's, # resolved by mcpp from the target's own row exactly as the compiler is; the # board selects OUT of it (which crt0, which libraries) and names the linker # script it wants, and asks where the sysroot is rather than declaring one. @@ -96,7 +96,7 @@ cd "$TMP" "$MCPP" new stdfs > /dev/null cd stdfs rm -f src/main.cpp tests/*.cpp src/stdfs.cppm 2>/dev/null || true -# ⚠️ NO [xlings] section. The C++ headers come from whatever toolchain mcpp +# NO [xlings] section. The C++ headers come from whatever toolchain mcpp # resolved, and the target's C headers mcpp already put on the compile line. # Declaring either would pin this package to one implementation, one libc and # one ISA — which is what an earlier version of it did. @@ -106,7 +106,7 @@ name = "stdfs" version = "0.1.0" EOF -# ⚠️ The config has to be a FILE. `_LIBCPP_HAS_THREADS` and friends are read +# The config has to be a FILE. `_LIBCPP_HAS_THREADS` and friends are read # from `__config_site` by <__config>; defining them with -D does not reach the # check at all (measured — the obvious `-D_LIBCPP_HAS_THREADS=0` does nothing). cat > build.mcpp <<'EOF' @@ -167,7 +167,7 @@ export module mcpplibs.std.freestanding; #include "std/ranges.inc" EOF -# ⚠️ Declared through libc++'s OWN header. The real symbol lives in the ABI +# Declared through libc++'s OWN header. The real symbol lives in the ABI # inline namespace (std::__1::), so a hand-written `namespace std { ... }` # definition compiles, links nothing, and leaves the undefined-symbol error # looking exactly as it did before. diff --git a/tests/e2e/134_target_sysroot_and_queries.sh b/tests/e2e/134_target_sysroot_and_queries.sh index 60894ffd..812bd382 100755 --- a/tests/e2e/134_target_sysroot_and_queries.sh +++ b/tests/e2e/134_target_sysroot_and_queries.sh @@ -2,7 +2,7 @@ # requires: llvm unix-shell # `[target.].sysroot` and the three target queries a board package asks. # -# ⚠️ WHY THIS TEST IS TWO-SIDED THROUGHOUT +# WHY THIS TEST IS TWO-SIDED THROUGHOUT # # Both features here are of the kind whose presence and absence look identical # on a machine that is already configured. A sysroot override that silently did @@ -93,7 +93,7 @@ manifest "" cat with_libc.log; echo "the target row's C library should have been usable"; exit 1; } # ── 4. `sysroot = ""` really removes it ───────────────────────────────────── -# ⚠️ THE LOAD-BEARING ASSERTION. Without it, an override that parsed and did +# THE LOAD-BEARING ASSERTION. Without it, an override that parsed and did # nothing would pass every other check in this file. manifest ' [target.riscv64-none-elf] diff --git a/tests/e2e/135_diagnostic_promise_resolves.sh b/tests/e2e/135_diagnostic_promise_resolves.sh index 7c440a2f..383d2d1b 100755 --- a/tests/e2e/135_diagnostic_promise_resolves.sh +++ b/tests/e2e/135_diagnostic_promise_resolves.sh @@ -3,7 +3,7 @@ # A diagnostic that prints a copy-pasteable dependency line must print one that # resolves. # -# ⚠️ THIS TEST EXISTS BECAUSE THE SAME DEFECT SHIPPED TWICE. +# THIS TEST EXISTS BECAUSE THE SAME DEFECT SHIPPED TWICE. # # The freestanding `import std;` message ends in a `[dependencies]` block the # reader is meant to paste. It has been wrong twice, in two different ways: @@ -21,7 +21,7 @@ # proof: the comment from the first repair was sitting right above the line # that broke. # -# ⚠️ THE TEST MUST NOT SPELL THE VERSION. Asserting `std-freestanding = "0.3.0"` +# THE TEST MUST NOT SPELL THE VERSION. Asserting `std-freestanding = "0.3.0"` # would copy the literal into a second place and check that the two copies # agree, which is true even when both are wrong. What is checked instead is the # property that matters: whatever the diagnostic prints, RESOLVES. diff --git a/tests/e2e/136_aarch64_zero_libc_target.sh b/tests/e2e/136_aarch64_zero_libc_target.sh index 0bd08c24..934033e2 100755 --- a/tests/e2e/136_aarch64_zero_libc_target.sh +++ b/tests/e2e/136_aarch64_zero_libc_target.sh @@ -2,7 +2,7 @@ # requires: llvm unix-shell # `aarch64-none-elf`: the second bare-metal architecture, on the zero-libc tier. # -# ⚠️ WHY A TARGET ROW NEEDS A TEST AT ALL +# WHY A TARGET ROW NEEDS A TEST AT ALL # # A row in the target table is four strings, and three of them can be wrong in # ways that still produce a build. A wrong `-mabi` is rejected by the driver, @@ -29,7 +29,7 @@ cd "$TMP" cd a64probe rm -f tests/*.cpp 2>/dev/null || true -# ⚠️ NO `[target.*]` SECTION, AND THAT IS THE ASSERTION. The zero-libc tier is +# NO `[target.*]` SECTION, AND THAT IS THE ASSERTION. The zero-libc tier is # this row's own property, not something the project asked for — which is the # difference from `riscv64-none-elf`, where declining the C library takes an # explicit `sysroot = ""`. @@ -94,7 +94,7 @@ NM=$(find "$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm" -name llvm-nm 2>/dev/null echo "the linker script's load address did not reach the image"; exit 1; } # ── 3. Nothing is left undefined ──────────────────────────────────────────── -# ⚠️ The load-bearing one. A row whose sysroot column named a payload that does +# The load-bearing one. A row whose sysroot column named a payload that does # not exist would add no paths and produce exactly this symptom later, inside # whichever package first included a C header — so the absence of a C library is # asserted as an absence of undefined symbols rather than as a missing key. diff --git a/tests/e2e/137_x86_64_zero_libc_target.sh b/tests/e2e/137_x86_64_zero_libc_target.sh index 099fe9ee..949359b4 100755 --- a/tests/e2e/137_x86_64_zero_libc_target.sh +++ b/tests/e2e/137_x86_64_zero_libc_target.sh @@ -3,7 +3,7 @@ # `x86_64-none-elf`: the third bare-metal architecture, and the first whose # LINK the compiler driver refuses to perform. # -# ⚠️ WHAT THIS ROW ADDED THAT THE OTHER TWO DID NOT NEED +# WHAT THIS ROW ADDED THAT THE OTHER TWO DID NOT NEED # # `riscv64-none-elf` and `aarch64-none-elf` are four strings in a table and # nothing else: clang has a BareMetal toolchain for both, so it drives their @@ -32,7 +32,7 @@ cd "$TMP" cd x86probe rm -f tests/*.cpp 2>/dev/null || true -# ⚠️ NO `[target.*]` SECTION. Like `aarch64-none-elf`, the zero-libc tier is +# NO `[target.*]` SECTION. Like `aarch64-none-elf`, the zero-libc tier is # this row's own property rather than something the project asks for. cat > mcpp.toml <<'EOF' [package] @@ -104,7 +104,7 @@ UNDEF=$("$NM" -u "$ELF" | wc -l) [ "$UNDEF" -eq 0 ] || { "$NM" -u "$ELF"; echo "the image expects $UNDEF symbols nothing provides"; exit 1; } -# ⚠️ An image with no operating system under it must not name an interpreter. +# An image with no operating system under it must not name an interpreter. # The direct-linker path says `--no-dynamic-linker` where the driver path said # `-static`; a PT_INTERP here would be that substitution having been dropped, # and it is exactly the defect the `--no-default-config` comment in flags.cppm @@ -143,7 +143,7 @@ grep -q -- "-mno-red-zone" verbose.log || { # ── 7. A shared library on this target is refused in words ───────────────── # -# ⚠️ THE DIRECT-LINKER PATH DEFINES NO SHARED-LIBRARY RULE, AND WITHOUT A CHECK +# THE DIRECT-LINKER PATH DEFINES NO SHARED-LIBRARY RULE, AND WITHOUT A CHECK # THE FAILURE NAMES AN INTERNAL ONE: # # ninja: error: build.ninja:88: unknown build rule 'cxx_shared' diff --git a/tests/e2e/138_feature_sources_gate_vs_provider.sh b/tests/e2e/138_feature_sources_gate_vs_provider.sh index f976d537..e7bb2410 100755 --- a/tests/e2e/138_feature_sources_gate_vs_provider.sh +++ b/tests/e2e/138_feature_sources_gate_vs_provider.sh @@ -3,7 +3,7 @@ # A feature that GATES a source and a feature that PROVIDES one, under both # `mcpp build` and `mcpp test`. # -# ⚠️ THIS DEFECT SURVIVED FOR MONTHS BECAUSE NO TEST COVERED BOTH FAMILIES, AND +# THIS DEFECT SURVIVED FOR MONTHS BECAUSE NO TEST COVERED BOTH FAMILIES, AND # THREE ATTEMPTED FIXES EACH BROKE THE ONE THEY DID NOT COVER. # # Two shapes of package reach the same code in `prepare_build`, and under @@ -49,7 +49,7 @@ cat > provider/mcpp.toml <<'EOF' name = "provider" version = "0.1.0" -# ⚠️ NO `[build] sources`, WHICH IS THE POINT. The base set is the inferred +# NO `[build] sources`, WHICH IS THE POINT. The base set is the inferred # `src/**`, which matches `src/gated/only_with_feature.cpp` — so removing the # feature's glob STRING from a list it was never in gates nothing. [features] @@ -103,7 +103,7 @@ cat > gate/mcpp.toml <<'EOF' name = "gate" version = "0.1.0" -# ⚠️ `src/gated_main.cpp` IS IN BOTH LISTS, EXACTLY AS gtest'S DESCRIPTOR HAS IT. +# `src/gated_main.cpp` IS IN BOTH LISTS, EXACTLY AS gtest'S DESCRIPTOR HAS IT. # The package provides the file; the feature is a switch over it. [build] sources = ["src/lib.cpp", "src/gated_main.cpp"] @@ -133,7 +133,7 @@ fi rm -rf target "$MCPP" test > test.log 2>&1 || { cat test.log; echo "gate: test failed"; exit 1; } -# ⚠️ THE ASSERTION IS THAT IT IS STILL THERE. A fix that excluded every +# THE ASSERTION IS THAT IT IS STILL THERE. A fix that excluded every # inactive feature glob under `mcpp test` would remove it — and would then # break gtest, whose per-test main detection needs to see the file in order to # prune it. Measured once as `ld returned 1 exit status` across mcpp's own diff --git a/tests/e2e/139_build_program_advisory.sh b/tests/e2e/139_build_program_advisory.sh index 61b43cba..247db6e6 100755 --- a/tests/e2e/139_build_program_advisory.sh +++ b/tests/e2e/139_build_program_advisory.sh @@ -2,7 +2,7 @@ # requires: unix-shell # A build program that succeeds and still says something. # -# ⚠️ THE SECOND BUILD IS THE TEST, NOT THE FIRST. +# THE SECOND BUILD IS THE TEST, NOT THE FIRST. # # mcpp prints what it captured from a build program only when that program # EXITS NON-ZERO, which is why `mcpp:warning=` had to exist at all. But the @@ -17,11 +17,11 @@ # So the assertions below are ordered: first build, then a SECOND build that # must be a cache hit and must still carry the same line. # -# ⚠️ This test was confirmed to FAIL before the fix — the cache-hit emission +# This test was confirmed to FAIL before the fix — the cache-hit emission # was removed and the second-build assertion went red while the first stayed # green. A test for a replay path that has never been observed failing cannot # distinguish "replay works" from "the cache never hit". -# ⚠️ `"$MCPP"`, NEVER A BARE `mcpp`. The harness passes the binary under test; +# `"$MCPP"`, NEVER A BARE `mcpp`. The harness passes the binary under test; # a bare name resolves through PATH to whichever engine happens to be installed, # and this test would then pass against an engine that does not have the feature. set -e @@ -61,7 +61,7 @@ EOF grep -q "no emulator found" first.log \ || { cat first.log; echo "FAIL: the advisory did not reach the output"; exit 1; } -# ⚠️ Prefixed with the package it came from. A workspace has several build +# Prefixed with the package it came from. A workspace has several build # programs and an unattributed sentence sends the reader to the wrong manifest. grep -q "advisory: no emulator found" first.log \ || { cat first.log; echo "FAIL: the advisory was not attributed to its package"; exit 1; } @@ -70,9 +70,9 @@ grep -q "advisory: no emulator found" first.log \ grep -qi "Finished" first.log \ || { cat first.log; echo "FAIL: the build did not finish"; exit 1; } -# ── 2. ⭐ THE SECOND BUILD. The program does not run; the advisory must ───── +# ── 2. THE SECOND BUILD. The program does not run; the advisory must ───── # -# ⚠️ `touch` FIRST, AND NOT AS A SUPERSTITION. A build with nothing at all to +# `touch` FIRST, AND NOT AS A SUPERSTITION. A build with nothing at all to # do takes a WHOLE-PROJECT fast path that prints one line and never reaches the # build.mcpp stage — so an unmodified second build exercises neither the run # path nor the replay path, and asserting against it would test the fast path diff --git a/tests/e2e/14_toolchain_fallback.sh b/tests/e2e/14_toolchain_fallback.sh index b219fed7..fde7562d 100755 --- a/tests/e2e/14_toolchain_fallback.sh +++ b/tests/e2e/14_toolchain_fallback.sh @@ -32,7 +32,7 @@ echo "$out" | grep -q 'mcpp toolchain install' || { # A project-level [toolchain] = "system" is REFUSED, and refused on its own # terms rather than falling through to "no toolchain configured". # -# ⚠️ THIS USED TO ASSERT THE OPPOSITE — that `system` "still works as escape +# THIS USED TO ASSERT THE OPPOSITE — that `system` "still works as escape # hatch" — and the assertion was only ever `grep -q 'no toolchain configured'` # being absent. That predicate stays satisfied by ANY other error, so when the # escape hatch became a refusal the test went on passing while its stated diff --git a/tests/e2e/161_xpkg_name_form.sh b/tests/e2e/161_xpkg_name_form.sh index 8cd4132f..3254e622 100755 --- a/tests/e2e/161_xpkg_name_form.sh +++ b/tests/e2e/161_xpkg_name_form.sh @@ -3,10 +3,10 @@ # 161_xpkg_name_form.sh — SPEC-001 §3.2: `package.name` is a SINGLE ATOMIC # SEGMENT; all hierarchy belongs in `package.namespace`. # -# ✅ namespace = "chriskohlhoff", name = "asio" -# ✅ namespace = "mcpplibs.capi", name = "lua" -# ✅ namespace = "compat", name = "compat.zlib" (legacy FQN, kept working) -# ❌ namespace = "mcpplibs", name = "capi.lua" (short name has a dot) +# accepted namespace = "chriskohlhoff", name = "asio" +# accepted namespace = "mcpplibs.capi", name = "lua" +# accepted namespace = "compat", name = "compat.zlib" (legacy FQN, kept working) +# refused namespace = "mcpplibs", name = "capi.lua" (short name has a dot) # # Why the last one is rejected rather than reinterpreted: identity is the pair # (namespace, name). A `name` carrying dots the namespace does not account for diff --git a/tests/e2e/188_build_actions.sh b/tests/e2e/188_build_actions.sh index ded74900..b5fee351 100755 --- a/tests/e2e/188_build_actions.sh +++ b/tests/e2e/188_build_actions.sh @@ -414,7 +414,7 @@ grep -q '1 passed' o6.log || { cat o6.log; echo "FAIL: the test did not run"; ex # command never runs. Saying nothing there is the same failure `[resources]` # reports as resources/no-image. # -# ⚠️ THE ONLY TARGET IS GATED OFF, and that is what makes this case exist at +# THE ONLY TARGET IS GATED OFF, and that is what makes this case exist at # all. A `kind = "lib"` target used to serve here, because a static library did # not take an object action's outputs; it does now (design C-6, e2e 608), so a # project with one is no longer a project with no consumer. A target behind an @@ -442,7 +442,7 @@ cp "$TMP/objtest/build.mcpp" build.mcpp grep -q 'produces no target to put its outputs into' o7.log || { cat o7.log; echo "FAIL: an object with no consumer must be reported"; exit 1; } -# ⭐ THE CONTROL: with the feature on, the target exists and the action is not +# THE CONTROL: with the feature on, the target exists and the action is not # reported. Without it the assertion above would pass on a build that had # stopped emitting the diagnostic for every project. "$MCPP" build --features gated > o7b.log 2>&1 || { diff --git a/tests/e2e/218_module_extensions_graph_shape.sh b/tests/e2e/218_module_extensions_graph_shape.sh index 0903960b..c95cb7c5 100755 --- a/tests/e2e/218_module_extensions_graph_shape.sh +++ b/tests/e2e/218_module_extensions_graph_shape.sh @@ -15,10 +15,10 @@ # anything. That is the worst failure mode in this area — silent, and it # surfaces later as an unrelated BMI error. # -# ⚠️ The edit below adds a real `import`. Do NOT reduce it to `touch`: an +# The edit below adds a real `import`. Do NOT reduce it to `touch`: an # mtime-only change is exactly what a correct implementation is also # allowed to ignore, so a touch-based test can pass with the bug present. -# ⚠️ And do NOT delete artifacts to force a rebuild: ninja then fails, the +# And do NOT delete artifacts to force a rebuild: ninja then fails, the # failure is read as a stale-graph signature, and the fast path falls back # to a full prepare for the wrong reason — the assertion below would hold # while proving nothing. @@ -56,7 +56,7 @@ printf 'import std;\nimport gshape.face;\nint main(){ std::println("{}", face()) # Ask mcpp for the fingerprint instead of inferring it from the build tree. # -# ⚠️ The obvious `find target -name build.ninja | head -1` is WRONG here and was +# The obvious `find target -name build.ninja | head -1` is WRONG here and was # flaky in exactly the way this test is meant to catch: after part 2 there are # TWO output dirs, and `head -1` picks whichever `find` happened to walk first. # It passed standalone and failed inside the suite. Assert on the value, not on diff --git a/tests/e2e/233_bench_matrix.sh b/tests/e2e/233_bench_matrix.sh index 301a1d42..beec75ae 100755 --- a/tests/e2e/233_bench_matrix.sh +++ b/tests/e2e/233_bench_matrix.sh @@ -19,7 +19,7 @@ # 4. the workflow reads the file instead of repeating it. set -e -# ⚠️ EVERY python read below MUST name its encoding, and this is what enforces it. +# EVERY python read below MUST name its encoding, and this is what enforces it. # # `open()`, `read_text()` and `subprocess(text=True)` decode with the LOCALE # encoding, which is UTF-8 on the Linux and macOS runners and cp1252 on the @@ -257,7 +257,7 @@ for t in ("cmake", "xmake", "bazel", "gcc", "llvm"): if not re.match(r"^\d+(\.\d+)+$", str(m.get("reference_mcpp", ""))): fail.append("reference_mcpp must be an exact released version — it is the old-vs-new column") -# ⚠️ WHAT IS DELIBERATELY *NOT* CHECKED HERE: that `reference_mcpp` equals the +# WHAT IS DELIBERATELY *NOT* CHECKED HERE: that `reference_mcpp` equals the # `.xlings.json` workspace pin. # # That check existed and was wrong. It read "the reference arm IS the binary CI @@ -399,7 +399,7 @@ PY # test. The numbers still print, they are just of something else, which is this # suite's entire failure mode in miniature. # -# ⚠️ ONE RUN, ONE FILE. This used to stitch three separate runs together and +# ONE RUN, ONE FILE. This used to stitch three separate runs together and # name each column with the file it came from, because no single run measured # every column. The standard set now measures all of them, so the check reads # ONE report — and a table that cannot be reproduced from one run is a table @@ -437,7 +437,7 @@ for doc, header in (("README.md", r"\| scenario \|"), print(f"FAIL: {doc}'s benchmark table did not parse — has its shape changed?") raise SystemExit(1) - # ⚠️ BOLD IS NOT PART OF THE GRAMMAR. This once required `**Ns**` in a fixed + # BOLD IS NOT PART OF THE GRAMMAR. This once required `**Ns**` in a fixed # column, and silently stopped matching when the bolding moved to whichever # column is actually faster: two of five rows dropped out and the check went # on printing a success line for the three that remained. Emphasis is @@ -450,7 +450,7 @@ for doc, header in (("README.md", r"\| scenario \|"), print(f"FAIL: {doc}: no engine columns in the table header") raise SystemExit(1) - # ⚠️ THE HEADER IS A SHORT NAME, THE DATA HOLDS THE FULL LABEL. + # THE HEADER IS A SHORT NAME, THE DATA HOLDS THE FULL LABEL. # # Three columns reading `mcpp@2026.8.13.1`, `mcpp@2026.8.13.1+schedule=on` # and `mcpp@2026.8.11.3` made the table wider than a README renders, so it @@ -480,7 +480,7 @@ for doc, header in (("README.md", r"\| scenario \|"), for line in body: cells = [c.strip() for c in line.strip().strip("|").split("|")] scenario = cells[0].strip("`") - # ⚠️ COUNT FROM THE RIGHT, not from a fixed offset. This was `cells[2:]` + # COUNT FROM THE RIGHT, not from a fixed offset. This was `cells[2:]` # ("after scenario and what-changed"), and dropping the what-changed # column to narrow the table shifted every value one place — which this # check would have reported as a row whose value count disagrees with the @@ -662,14 +662,14 @@ python3 - "$ROOT" <<'PY' || exit 1 import pathlib, re, sys root = pathlib.Path(sys.argv[1]) / "bench/src/engines" -# ⚠️ AN EMPTY GLOB PASSES THIS CHECK PERFECTLY. Rename the directory, move the +# AN EMPTY GLOB PASSES THIS CHECK PERFECTLY. Rename the directory, move the # adapters, and every assertion below iterates zero files and prints its success # line. That is the failure mode this whole test exists to prevent, so the check # must first prove it has something to check. Four is the number of adapters # today (mcpp, cmake, xmake, bazel) plus engine; the floor is deliberately low # so adding one does not require editing this. # -# ⚠️ AND THE IMPLEMENTATION UNITS ARE WHERE THE CODE IS. The adapters declare in +# AND THE IMPLEMENTATION UNITS ARE WHERE THE CODE IS. The adapters declare in # `.cppm` and define in `.cpp`; globbing only `.cppm` leaves this scanning # signatures, where a `compiler == "clang"` cannot appear — so the check would # have kept printing its success line while testing nothing at all. It nearly diff --git a/tests/e2e/234_bmi_schedule_on.sh b/tests/e2e/234_bmi_schedule_on.sh index 0af1f2c6..cd889fa2 100755 --- a/tests/e2e/234_bmi_schedule_on.sh +++ b/tests/e2e/234_bmi_schedule_on.sh @@ -13,7 +13,7 @@ # it is split. If the strategy silently fell back, the timings would look # like a regression in the feature rather than like the feature being off. # -# 2. ⚠️ STALE CONCURRENCY TOKENS MUST BE RECLAIMED. Real compiler concurrency is +# 2. STALE CONCURRENCY TOKENS MUST BE RECLAIMED. Real compiler concurrency is # bounded by a semaphore of directories under `/.mcpp-sched`, and a # token is removed by the supervisor holding it. A supervisor that never runs # its cleanup — Ctrl-C on the build, the OOM killer, a reboot — leaves its @@ -31,7 +31,7 @@ if [ -z "${MCPP:-}" ]; then MCPP="$(bash "$_root/.github/tools/newest_artifact.sh" "$_root" mcpp 2>/dev/null || true)" [ -n "$MCPP" ] || { echo "SKIP: no mcpp binary built yet — run \`mcpp build\` first"; exit 0; } fi -# ⚠️ ABSOLUTISE WHATEVER WE GOT, not just what we derived. This test `cd`s into a +# ABSOLUTISE WHATEVER WE GOT, not just what we derived. This test `cd`s into a # temp directory, so a RELATIVE MCPP — which is what `ls -t target/*/*/bin/mcpp` # hands you, and what a caller naturally exports — stops resolving the moment we # leave the repository, with @@ -46,7 +46,7 @@ TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT cd "$TMP" -# ⚠️ PIN PER PLATFORM, not just `default`. A bare `default = "gcc@16.1.0"` sends +# PIN PER PLATFORM, not just `default`. A bare `default = "gcc@16.1.0"` sends # macOS and Windows looking for a gcc payload that does not exist for them, and # the whole test dies at step 1 with `'xim:gcc@16.1.0' not in current index` — # reported as "the build with bmi_schedule=on failed", which is a completely @@ -153,7 +153,7 @@ printf '\nexport int core_extra() { return 1; }\n' >> src/core.cppm # A generous bound that is still far below the in-process 2h fallback: if the # reclaim is gone this hangs, and the timeout is what turns that into a failure # a CI log can explain. -# ⚠️ CAPTURE THE STATUS BEFORE ANY TEST TOUCHES IT. Inside `if ! cmd; then`, +# CAPTURE THE STATUS BEFORE ANY TEST TOUCHES IT. Inside `if ! cmd; then`, # `$?` is the status of `! cmd` — which is 0 exactly when cmd FAILED — so a real # failure got reported as "exited 0" and the timeout case could never be # distinguished from any other. Same shape as reading `$?` after `cmd | tail`. diff --git a/tests/e2e/235_std_object_only_when_needed.sh b/tests/e2e/235_std_object_only_when_needed.sh index 5908eef6..afeac331 100755 --- a/tests/e2e/235_std_object_only_when_needed.sh +++ b/tests/e2e/235_std_object_only_when_needed.sh @@ -7,7 +7,7 @@ # and SharedLibrary whenever the toolchain merely HAD a prebuilt std module, # regardless of whether anything in that unit imported it. # -# ⚠️ THE TEST THAT MATTERS IS THE TRANSITIVE ONE. A unit that never writes +# THE TEST THAT MATTERS IS THE TRANSITIVE ONE. A unit that never writes # `import std` itself still needs the initialiser when a module it imports does. # Checking only "does this unit's own source say import std" is the same # "the edge exists but nobody depends on it" mistake as #405, and it fails in diff --git a/tests/e2e/236_module_extensions_default_toolchain.sh b/tests/e2e/236_module_extensions_default_toolchain.sh index 1e3bd4e4..afe70da4 100755 --- a/tests/e2e/236_module_extensions_default_toolchain.sh +++ b/tests/e2e/236_module_extensions_default_toolchain.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # `[build] module_extensions` on the platform's DEFAULT toolchain (#412). # -# ⚠️ WHY THIS EXISTS SEPARATELY FROM 217. `217_module_extensions.sh` declares +# WHY THIS EXISTS SEPARATELY FROM 217. `217_module_extensions.sh` declares # `# requires: gcc`, and `run_all.sh` deliberately does not grant `gcc` on # Windows or macOS: # @@ -73,7 +73,7 @@ echo "== build ==" exit 1 } -# ⚠️ A BUILD THAT EXITS 0 IS NOT ENOUGH, and this is the exact trap the +# A BUILD THAT EXITS 0 IS NOT ENOUGH, and this is the exact trap the # extension work was full of: Clang hands an unrecognised `.ixx` to the LINKER, # warns, and exits 0 having produced NO BMI. So assert the interface really was # compiled as one — a BMI exists for the module. diff --git a/tests/e2e/237_default_subos_without_info.sh b/tests/e2e/237_default_subos_without_info.sh index 71a62d36..f5495619 100755 --- a/tests/e2e/237_default_subos_without_info.sh +++ b/tests/e2e/237_default_subos_without_info.sh @@ -20,7 +20,7 @@ # It is the index-floor rule and mcpp#221 again: DATA THAT IS MISSING OR NEWER # MUST NOT INVALIDATE THE PROGRAM THAT READS IT. # -# ⚠️ WHY `221_subos_without_info_still_builds.sh` DOES NOT COVER THIS. +# WHY `221_subos_without_info_still_builds.sh` DOES NOT COVER THIS. # # 221 creates a PROJECT-LOCAL SubOS (`[xlings] subos = "bare"` → # `/.mcpp/.xlings/subos/bare`). The fixup read a HARDCODED @@ -124,11 +124,11 @@ echo " absence: no fixup error (exit $rc)" # not a broken toolchain: same home, same payloads, same project — one JSON # block is the entire difference. # -# ⚠️ NOT `allow_host_libs = true`. That was the first version and it FAILED ON +# NOT `allow_host_libs = true`. That was the first version and it FAILED ON # CI while passing here: falling back to the host needs a host C runtime, and # the runners have no `crt1.o` / `libm` to fall back TO. The green half must not # depend on a fact about the machine — binding to the payload glibc does not. -# ⚠️ ASK THE REAL HOME WHICH GLIBC IT USES, do not take the first directory. +# ASK THE REAL HOME WHICH GLIBC IT USES, do not take the first directory. # A machine with two glibc payloads installed has one the toolchain was patched # against and one it was not, and `ls | head -1` picks by alphabetical order — # which would make this control fail for a reason that has nothing to do with diff --git a/tests/e2e/238_c_only_unit_links_with_c_driver.sh b/tests/e2e/238_c_only_unit_links_with_c_driver.sh index 3d109a76..3ac93eda 100755 --- a/tests/e2e/238_c_only_unit_links_with_c_driver.sh +++ b/tests/e2e/238_c_only_unit_links_with_c_driver.sh @@ -21,7 +21,7 @@ # deploying that .so has to ship a C++ runtime with it, and its runtime closure # is larger than the code justifies. # -# ⚠️ BOTH DIRECTIONS. Checking only that libstdc++ disappeared would pass an +# BOTH DIRECTIONS. Checking only that libstdc++ disappeared would pass an # implementation that linked EVERYTHING with the C driver, which turns every # C++ target into a pile of undefined `_ZSt…`. So a C++ target must still link, # run, and use the C++ rule. @@ -38,7 +38,7 @@ case "$MCPP" in /*) ;; *) MCPP="$_root/$MCPP" ;; esac TMP=$(mktemp -d) trap "rm -rf $TMP || true" EXIT -# ⚠️ THREE SEPARATE PROJECTS, not three targets in one. Targets in a single +# THREE SEPARATE PROJECTS, not three targets in one. Targets in a single # package share that package's objects — a `[targets.X] sources` list does not # fence them off — so a C++ target next door would put its object into the # pure-C library's link and the predicate under test would never see a C-only diff --git a/tests/e2e/239_msvc_managed_toolset.sh b/tests/e2e/239_msvc_managed_toolset.sh index e6fc9e97..a1716021 100755 --- a/tests/e2e/239_msvc_managed_toolset.sh +++ b/tests/e2e/239_msvc_managed_toolset.sh @@ -38,7 +38,7 @@ cd "$TMP" # 0) Decide SKIP here, from a positive check, and never again. # -# ⚠️ This used to be decided AFTER the install by pattern-matching the +# This used to be decided AFTER the install by pattern-matching the # failure text, and one of the patterns was `*"index"*`. Nearly every mcpp # command prints "package index" somewhere, so every genuine install # failure took the skip branch: the test could pass or skip, never fail. @@ -61,7 +61,7 @@ fi # 1) install it. Any failure from here on is a FAILURE. # -# ⚠️ STREAMED AS WELL AS CAPTURED, BECAUSE THIS STEP IS THE ONE THAT RUNS OUT +# STREAMED AS WELL AS CAPTURED, BECAUSE THIS STEP IS THE ONE THAT RUNS OUT # OF TIME. It fetches ~376 MB (xim:msvc ~85 MB + xim:windows-sdk ~291 MB), and # `out=$(...)` alone prints nothing while it does — so a run killed by the # per-test deadline leaves 600 seconds of silence and a harness line that can diff --git a/tests/e2e/240_pack_pe_zip_cross.sh b/tests/e2e/240_pack_pe_zip_cross.sh index 911be28f..314af001 100755 --- a/tests/e2e/240_pack_pe_zip_cross.sh +++ b/tests/e2e/240_pack_pe_zip_cross.sh @@ -39,7 +39,7 @@ cat > mcpp.toml <<'EOF' name = "winpack" version = "0.1.0" -# ⚠️ THIS TEST DROPS A FILE INTO THE BUILD TREE AND EXPECTS `pack` TO SEE IT, +# THIS TEST DROPS A FILE INTO THE BUILD TREE AND EXPECTS `pack` TO SEE IT, # so the two commands have to agree on WHICH build tree that is. # # `mcpp pack` builds with the `release` fallback (a packaged artifact leaves diff --git a/tests/e2e/243_pack_library_interface_closure.sh b/tests/e2e/243_pack_library_interface_closure.sh index 8f183579..d9baffaf 100755 --- a/tests/e2e/243_pack_library_interface_closure.sh +++ b/tests/e2e/243_pack_library_interface_closure.sh @@ -2,7 +2,7 @@ # requires: # (no capability: this must hold on every target.) # -# ⚠️ This test is also the regression for a scanner bug it uncovered. An +# This test is also the regression for a scanner bug it uncovered. An # IMPLEMENTATION PARTITION (`module M:part;`, no `export`) had no coverage # anywhere in mcpp, and the scanner recorded it as "requires M:part, provides # nothing" — so the file required its own name, the graph held no edge from the diff --git a/tests/e2e/245_pack_library_fat_target_selection.sh b/tests/e2e/245_pack_library_fat_target_selection.sh index b8a26240..b337aee7 100755 --- a/tests/e2e/245_pack_library_fat_target_selection.sh +++ b/tests/e2e/245_pack_library_fat_target_selection.sh @@ -12,7 +12,7 @@ # cannot express "gcc+musl OR msvc+mingw", and a host that has neither should # skip rather than half-run. # -# ⚠️ THE PREDICATE IS THE POINT. Each leg is selected by a generated +# THE PREDICATE IS THE POINT. Each leg is selected by a generated # `cfg(all(arch=…, os=…, env=…))` block and NOT by a bare `[target.'']` # key. The bare form only matched when `--target` was passed: a plain # `mcpp build` resolved the host and compared it against an empty string, so diff --git a/tests/e2e/252_pack_library_old_client.sh b/tests/e2e/252_pack_library_old_client.sh index 90ae2112..f220c7bc 100755 --- a/tests/e2e/252_pack_library_old_client.sh +++ b/tests/e2e/252_pack_library_old_client.sh @@ -92,7 +92,7 @@ EOF || { cat "$TMP/app/new.log"; echo "the PR binary could not consume its own package"; exit 1; } grep -q 'ok=42' "$TMP/app/new.log" || { cat "$TMP/app/new.log"; echo "wrong answer"; exit 1; } -# ⚠️ The boot entry each CI job bootstraps from is an xvm SHIM, and a shim +# The boot entry each CI job bootstraps from is an xvm SHIM, and a shim # resolves against the home it is asked in — under the e2e suite's environment # it answers `xlings: 'mcpp' is not installed` and prints NOTHING for # `--version`. The first version of this guard compared that empty string diff --git a/tests/e2e/253_pack_library_undetermined_partition.sh b/tests/e2e/253_pack_library_undetermined_partition.sh index 15d99b9f..2869412b 100755 --- a/tests/e2e/253_pack_library_undetermined_partition.sh +++ b/tests/e2e/253_pack_library_undetermined_partition.sh @@ -24,7 +24,7 @@ # silence, which is the one failure mode the whole closure design exists to # prevent. # -# ⚠️ PINNED FROM BOTH SIDES, on purpose. Asserting only that the override case +# PINNED FROM BOTH SIDES, on purpose. Asserting only that the override case # warns cannot distinguish "mcpp models three states" from "mcpp warns about # every partition it publishes". So the same fixture is packed twice — once # scanned, once overridden — and the two must produce DIFFERENT sentences: diff --git a/tests/e2e/254_pack_library_platform_coverage.sh b/tests/e2e/254_pack_library_platform_coverage.sh index c5e8e0b2..69e2f6b9 100755 --- a/tests/e2e/254_pack_library_platform_coverage.sh +++ b/tests/e2e/254_pack_library_platform_coverage.sh @@ -22,7 +22,7 @@ # # So the silence is as much the feature as the warning, and both are asserted. # -# ⚠️ THE PER-HOST PICKS BELOW MIRROR host_can_serve (registry.cppm:542-566). +# THE PER-HOST PICKS BELOW MIRROR host_can_serve (registry.cppm:542-566). # That is deliberate and it is a tripwire: if mcpp ever gains, say, a # macOS-hosted Windows toolchain, this test starts failing — which is the # correct outcome, because the table it encodes will have changed and the diff --git a/tests/e2e/255_pack_library_msvc_archiver.sh b/tests/e2e/255_pack_library_msvc_archiver.sh index 87e9339b..a92b53f0 100755 --- a/tests/e2e/255_pack_library_msvc_archiver.sh +++ b/tests/e2e/255_pack_library_msvc_archiver.sh @@ -105,7 +105,7 @@ if command -v lib &>/dev/null && members="$(lib /nologo /LIST "$(host_path "$arc echo " (verified against lib /LIST)" fi -# ⚠️ NO CONSUMER STEP, and the reason is a measured one rather than a +# NO CONSUMER STEP, and the reason is a measured one rather than a # simplification. The package is built with msvc@system, so its tag is # `…-msvc19-…`; the consumer would have to use the same toolchain or the ABI gate # refuses it — correctly: diff --git a/tests/e2e/256_pack_library_fat_windows.sh b/tests/e2e/256_pack_library_fat_windows.sh index 510af576..c32e1e99 100755 --- a/tests/e2e/256_pack_library_fat_windows.sh +++ b/tests/e2e/256_pack_library_fat_windows.sh @@ -14,7 +14,7 @@ # platform — a layout keyed by "windows" could hold only one of these, and the # one it held would link for exactly half of its consumers. # -# ⚠️ WHY IT DID NOT EXIST BEFORE, AND WHY THAT WAS THE WRONG CALL. This +# WHY IT DID NOT EXIST BEFORE, AND WHY THAT WAS THE WRONG CALL. This # combination was reported as "not possible on Windows". It is: host_can_serve # (registry.cppm:542-566) grants a Windows host `*-windows-msvc`, # `*-windows-gnu` AND host-arch `*-linux-musl` — three targets, so a fat package @@ -125,7 +125,7 @@ check() { # $1 = label, $2 = expected leg dir, $3.. = build args ( cd app && "$MCPP" build "$@" > "$TMP/$label.log" 2>&1 ) \ || { cat "$TMP/$label.log"; echo "$label build failed"; exit 1; } local nj; nj="$(find app/target -name build.ninja | head -1)" - # ⚠️ Separator-agnostic, and matched from the PACKAGE name rather than from + # Separator-agnostic, and matched from the PACKAGE name rather than from # `dist/`. A native mcpp.exe writes native separators into build.ninja, so # the first version of this — `grep -o "dist/mathkit-0.1.0/lib/…"` — matched # nothing at all on Windows and reported "saw more than one leg" over an diff --git a/tests/e2e/257_shared_library_pe.sh b/tests/e2e/257_shared_library_pe.sh index 925ef6a1..a138e561 100755 --- a/tests/e2e/257_shared_library_pe.sh +++ b/tests/e2e/257_shared_library_pe.sh @@ -23,7 +23,7 @@ # and prints the right answer under wine # 4. an MSVC-ABI shared target is still refused, and says why # -# ⚠️ WINE IS EVIDENCE, NOT PROOF. Wine maps the filesystem differently (Z:) and +# WINE IS EVIDENCE, NOT PROOF. Wine maps the filesystem differently (Z:) and # has previously passed things a real Windows failed. The Windows-native halves # of the same claims live in 242/255/256; what wine gives here is the loader # actually resolving the DLL, which no Linux-hosted check can show. @@ -66,7 +66,7 @@ imp="$(find target -name 'libmathkit.dll.a' | head -1)" echo "FAIL: no import library. The .dll alone is a library only mingw's ld" echo " will link, so the package would be unusable everywhere else." exit 1; } -# ⚠️ And no `-fPIC`. PE code is position independent by design, and clang +# And no `-fPIC`. PE code is position independent by design, and clang # targeting the MSVC ABI REJECTS the flag — `unsupported option '-fPIC' for # target 'x86_64-pc-windows-msvc'` — killing the build in clang-scan-deps before # anything compiles. The condition used to be the DIALECT rather than the target, diff --git a/tests/e2e/258_shared_library_msvc_auto_def.sh b/tests/e2e/258_shared_library_msvc_auto_def.sh index 98fe952a..d4892151 100755 --- a/tests/e2e/258_shared_library_msvc_auto_def.sh +++ b/tests/e2e/258_shared_library_msvc_auto_def.sh @@ -16,7 +16,7 @@ # only a real link can answer is whether link.exe accepts the result and whether # a consumer can then resolve a symbol through it. That is this test. # -# ⚠️ THE TWO LIMITS ARE ASSERTED, NOT ASSUMED. Auto-export cannot make a data +# THE TWO LIMITS ARE ASSERTED, NOT ASSUMED. Auto-export cannot make a data # symbol readable without `__declspec(dllimport)` on the consumer's declaration, # and CMake documents the same limit for the same mechanism. The test exercises a # FUNCTION across the boundary, which is what auto-export does cover, and says so @@ -166,7 +166,7 @@ anndef="$(find annotated/target -name 'annkit.def' | head -1)" # already marks variables `DATA`, so it is reasonable to wonder whether the limit # still bites. It does, and here is the difference. # -# ⚠️ The assertion is on the DIFFERENCE between the two spellings, not on any +# The assertion is on the DIFFERENCE between the two spellings, not on any # particular error text — which form the failure takes (a link error, or a # pointer read where a value was meant) depends on the toolset version, and # pinning one of them would make this test a hostage to that. diff --git a/tests/e2e/259_shared_library_macho.sh b/tests/e2e/259_shared_library_macho.sh index 9d1ad391..358b4ded 100755 --- a/tests/e2e/259_shared_library_macho.sh +++ b/tests/e2e/259_shared_library_macho.sh @@ -16,7 +16,7 @@ # `soname`, and to decide Mach-O-ness with `#if defined(__APPLE__)` on the HOST # rather than from the target. # -# ⚠️ AND WHY IT ASSERTS THE PATH IS *GONE*. Checking that the consumer runs in +# AND WHY IT ASSERTS THE PATH IS *GONE*. Checking that the consumer runs in # place proves nothing: the absolute install name still resolves while the build # directory exists. The package has to be consumed from a location the original # build path cannot satisfy — so the library is packed, the producer's whole diff --git a/tests/e2e/260_module_extension_is_configuration.sh b/tests/e2e/260_module_extension_is_configuration.sh index 9fa97053..3f4556f5 100755 --- a/tests/e2e/260_module_extension_is_configuration.sh +++ b/tests/e2e/260_module_extension_is_configuration.sh @@ -13,7 +13,7 @@ # 1. a DECLARED extension works with no further help; # 2. an UNDECLARED one is refused where the mistake is, not four steps later. # -# ⚠️ WHY (2) IS THE INTERESTING HALF. Before this, an undeclared `.ixx` matched +# WHY (2) IS THE INTERESTING HALF. Before this, an undeclared `.ixx` matched # by `sources` was accepted and produced a compile edge whose object NOTHING # LINKS — measured: # diff --git a/tests/e2e/261_pack_follows_the_module_extension.sh b/tests/e2e/261_pack_follows_the_module_extension.sh index f8267eb9..efed1354 100755 --- a/tests/e2e/261_pack_follows_the_module_extension.sh +++ b/tests/e2e/261_pack_follows_the_module_extension.sh @@ -10,7 +10,7 @@ # and consuming the package must both work with nothing further declared on # either side. Anything else makes the knob a knob plus two things to remember. # -# ⚠️ TWO DEFECTS SAT BEHIND THIS, and the first was silent in both directions. +# TWO DEFECTS SAT BEHIND THIS, and the first was silent in both directions. # # 1. The lib-root convention hard-coded `.cppm`: `src/.cppm`. For an # `.ixx` project that file does not exist, so the closure started nowhere: diff --git a/tests/e2e/262_pack_consumed_by_native_cl.sh b/tests/e2e/262_pack_consumed_by_native_cl.sh index b257e703..1d40a1ea 100755 --- a/tests/e2e/262_pack_consumed_by_native_cl.sh +++ b/tests/e2e/262_pack_consumed_by_native_cl.sh @@ -16,7 +16,7 @@ # ship because an older mcpp reads only the first and silently ignores the # second, so dropping the first would leave every older client with no link line. # -# ⚠️ WHY BOTH HALVES ARE ASSERTED. The renderer is unit tested +# WHY BOTH HALVES ARE ASSERTED. The renderer is unit tested # (tests/unit/test_link_intent_spelling.cpp) and only a real `cl.exe` can say # whether the result links. But a consumer that merely SUCCEEDS proves less than # it looks: if the neutral form were ignored and the ldflags applied instead, diff --git a/tests/e2e/263_lib_root_follows_the_extension.sh b/tests/e2e/263_lib_root_follows_the_extension.sh index a97e7897..13b33c82 100755 --- a/tests/e2e/263_lib_root_follows_the_extension.sh +++ b/tests/e2e/263_lib_root_follows_the_extension.sh @@ -5,7 +5,7 @@ # 263_lib_root_follows_the_extension.sh — the lib-root convention offers one # candidate per DECLARED module extension, at every call site. # -# ⚠️ WHY THIS EXISTS AS ITS OWN TEST. The previous round fixed the resolver the +# WHY THIS EXISTS AS ITS OWN TEST. The previous round fixed the resolver the # packer uses and left two other callers on the non-probing form, because the # path under test was the only one anybody looked at. Both were reachable and # both were wrong for an `.ixx` project: diff --git a/tests/e2e/264_pack_library_is_relocatable.sh b/tests/e2e/264_pack_library_is_relocatable.sh index ab14996a..5130020e 100755 --- a/tests/e2e/264_pack_library_is_relocatable.sh +++ b/tests/e2e/264_pack_library_is_relocatable.sh @@ -21,12 +21,12 @@ # # error while loading shared libraries: libstdc++.so.6: cannot open shared object file # -# ⚠️ AND WHY `$ORIGIN` IS NOT THE FIX. Measured: a DT_RUNPATH of `$ORIGIN`, and +# AND WHY `$ORIGIN` IS NOT THE FIX. Measured: a DT_RUNPATH of `$ORIGIN`, and # a DT_RUNPATH of the empty string, both fail exactly the same way. It is the # TAG's presence that disables inheritance, not its contents. The criterion is # therefore "there is no tag", never "the tag is relative". # -# ⚠️ AND WHY THIS TEST DELIBERATELY BREAKS THE PACKAGE HALF-WAY THROUGH. +# AND WHY THIS TEST DELIBERATELY BREAKS THE PACKAGE HALF-WAY THROUGH. # Asserting only that the fixed package runs cannot tell "the defect is fixed" # from "this machine happens to satisfy the stale path" — which is precisely # what made the pre-existing e2e (251) green throughout the bug's life: it diff --git a/tests/e2e/265_pack_strips_but_stays_usable.sh b/tests/e2e/265_pack_strips_but_stays_usable.sh index be5351ad..ccf8b2b6 100755 --- a/tests/e2e/265_pack_strips_but_stays_usable.sh +++ b/tests/e2e/265_pack_strips_but_stays_usable.sh @@ -3,7 +3,7 @@ # 265_pack_strips_but_stays_usable.sh — `mcpp pack` ships stripped artifacts, # and stripping must not make them unusable. # -# ⚠️ THE ONE THAT BITES: A STATIC ARCHIVE CANNOT BE `--strip-all`ed. +# THE ONE THAT BITES: A STATIC ARCHIVE CANNOT BE `--strip-all`ed. # # `strip` with no shape argument defaults to strip-all, and on a `.a` that # removes the ARCHIVE SYMBOL INDEX. The package then fails at the CONSUMER's diff --git a/tests/e2e/266_pack_refuses_a_macho_program.sh b/tests/e2e/266_pack_refuses_a_macho_program.sh index 110a1ac3..75c21a00 100755 --- a/tests/e2e/266_pack_refuses_a_macho_program.sh +++ b/tests/e2e/266_pack_refuses_a_macho_program.sh @@ -22,7 +22,7 @@ # the `pack` capability where `elf` + `patchelf` are both present — i.e. Linux. # So no job in this suite has ever run `mcpp pack` on a Mac. # -# ⚠️ BOTH SIDES, ON THE SAME HOST. Asserting only the refusal cannot tell "the +# BOTH SIDES, ON THE SAME HOST. Asserting only the refusal cannot tell "the # gate works" from "pack is broken on this machine". So the same run also packs # a LIBRARY target, which takes a different pipeline and must still succeed. set -e diff --git a/tests/e2e/267_target_requested_is_empty_for_a_native_build.sh b/tests/e2e/267_target_requested_is_empty_for_a_native_build.sh index 8189e65f..5b8ad3cb 100755 --- a/tests/e2e/267_target_requested_is_empty_for_a_native_build.sh +++ b/tests/e2e/267_target_requested_is_empty_for_a_native_build.sh @@ -3,7 +3,7 @@ # `MCPP_TARGET_REQUESTED` — the value a build program needs and `MCPP_TARGET` # cannot give it. # -# ⭐⭐ WHY THE TWO VARIABLES ARE NOT THE SAME QUESTION. +# WHY THE TWO VARIABLES ARE NOT THE SAME QUESTION. # # `MCPP_TARGET` answers "which machine is this for", and it is filled in with # the host when nobody named a target — which is right for that question and @@ -17,7 +17,7 @@ # can name one. A native build on the same machine gets the SDK and needs # nothing from the package. # -# ⚠️ Both readings of that question have already been measured wrong in +# Both readings of that question have already been measured wrong in # `openkal-macos`: # # from the host → right for the cross, and `library not found for -lSystem` @@ -29,7 +29,7 @@ # ⇒ The assertions below are the contract those two attempts needed: EMPTY for a # native build, and the requested triple otherwise. # -# ⚠️ `"$MCPP"`, never a bare `mcpp`: the harness passes the binary under test, +# `"$MCPP"`, never a bare `mcpp`: the harness passes the binary under test, # and a bare name resolves through PATH to whichever engine is installed. set -e @@ -45,7 +45,7 @@ version = "0.1.0" EOF printf 'int main() { return 0; }\n' > src/main.cpp -# ⚠️ The build program reports through a NON-ZERO exit, because mcpp prints what +# The build program reports through a NON-ZERO exit, because mcpp prints what # it captured only from a program that failed. A probe that returned zero would # have its output discarded and this test would assert nothing. cat > build.mcpp <<'EOF' @@ -72,7 +72,7 @@ echo "$native" | grep -qE 'requested=\[\]' || { echo "$native" | grep 'PROBE ' >&2 exit 1 } -# ⚠️ And the positive control on the same line: `MCPP_TARGET` must be filled in. +# And the positive control on the same line: `MCPP_TARGET` must be filled in. # Without this the assertion above would also pass if mcpp had stopped setting # any of them. echo "$native" | grep -qE 'target=\[[a-z0-9_]+-[a-z0-9-]+\]' || { @@ -83,14 +83,14 @@ echo "$native" | grep -qE 'target=\[[a-z0-9_]+-[a-z0-9-]+\]' || { # ── A build pointed at a target ──────────────────────────────────────────── # -# ⚠️ THE HOST'S OWN TRIPLE, NAMED EXPLICITLY — which is the case that +# THE HOST'S OWN TRIPLE, NAMED EXPLICITLY — which is the case that # distinguishes the two variables rather than merely one that differs from the # native run. `--target ` produces equal values for `MCPP_TARGET` and the # host triple, and `MCPP_TARGET_REQUESTED` is non-empty because a target was # named. A test that used a foreign triple would pass with a variable that # merely echoed `MCPP_TARGET`. # -# ⚠️ And it needs no payload. The first version of this test named +# And it needs no payload. The first version of this test named # `x86_64-linux-musl` on the grounds that every host could resolve it, which was # an assumption rather than a measurement: # diff --git a/tests/e2e/268_target_side_resolution.sh b/tests/e2e/268_target_side_resolution.sh index b752126e..d6bcfa8f 100755 --- a/tests/e2e/268_target_side_resolution.sh +++ b/tests/e2e/268_target_side_resolution.sh @@ -16,7 +16,7 @@ # stack, and a test that fetched an ecosystem over the network to assert a # string in a report would be slower and no more conclusive. # -# ⚠️ NO `sed -i` ANYWHERE, AND THAT IS NOT STYLE. BSD sed reads the argument +# NO `sed -i` ANYWHERE, AND THAT IS NOT STYLE. BSD sed reads the argument # after `-i` as a backup suffix, so an in-place edit written for GNU sed fails # on macOS. Measured on the macOS leg of this suite, in this very file: # @@ -74,7 +74,7 @@ printf 'int main() { return 0; }\n' > src/main.cpp # a single provider of two layers is a legitimate shape and a useful one to # assert: it proves the two lookups are independent. # -# ⚠️ `|| true`, AND THAT IS THE TEST'S SUBJECT RATHER THAN A CONCESSION. The +# `|| true`, AND THAT IS THE TEST'S SUBJECT RATHER THAN A CONCESSION. The # resolution is reported during planning, before a single object is compiled, so # what this file asserts is complete whether or not the link afterwards # succeeds. Requiring a successful link would additionally require a working C @@ -100,7 +100,7 @@ echo "$out" | grep -q 'graph' || { echo "$out" >&2; exit 1 } -# ⚠️ AND THE POSITIVE CONTROL ON THE SAME OUTPUT. Without this, the assertions +# AND THE POSITIVE CONTROL ON THE SAME OUTPUT. Without this, the assertions # above would also pass on an engine that printed the same three lines for # every build regardless of what the graph contained. # The label is the capability name — `c++-abi`, matching `mcpp:c++-abi` — so @@ -136,7 +136,7 @@ echo "$plain" | grep -qE 'kernel-abi .*payload' || { # turns one wrong letter into a behaviour that silently does not happen while # the build reports success. # -# ⚠️ IN A DEPENDENCY IT IS A WARNING RATHER THAN AN ERROR, AND THE CHANGE WAS +# IN A DEPENDENCY IT IS A WARNING RATHER THAN AN ERROR, AND THE CHANGE WAS # FORCED BY A MEASUREMENT. mcpp cannot distinguish a misspelling from a layer # named after this build tool was released, and the two mistakes cost different # amounts. Refusing meant a package declaring a NEW layer failed to load under diff --git a/tests/e2e/269_openkal_llvm_spelling_still_resolves.sh b/tests/e2e/269_openkal_llvm_spelling_still_resolves.sh index 4f7d41ae..09fe0869 100755 --- a/tests/e2e/269_openkal_llvm_spelling_still_resolves.sh +++ b/tests/e2e/269_openkal_llvm_spelling_still_resolves.sh @@ -62,7 +62,7 @@ old_spelling=$(driver_for "openkal-llvm@22.1.8") exit 1 } -# ⚠️ AND THE OLDER SPELLING MUST NOT STILL DECIDE ANYTHING. Both manifests here +# AND THE OLDER SPELLING MUST NOT STILL DECIDE ANYTHING. Both manifests here # have an empty dependency graph, so both must report a target side supplied # entirely by the payload. If the family name still carried the fact it used to, # the second would report `graph` somewhere and the first would not. diff --git a/tests/e2e/280_target_side_layers.sh b/tests/e2e/280_target_side_layers.sh index ad82af8b..725ac9fd 100755 --- a/tests/e2e/280_target_side_layers.sh +++ b/tests/e2e/280_target_side_layers.sh @@ -25,7 +25,7 @@ out="$("$MCPP" build 2>&1)" target_line="$(printf '%s\n' "$out" | grep -E '^\s+Target ' || true)" [[ -n "$target_line" ]] || { echo "no Target line at all:"; echo "$out"; exit 1; } -# ⚠️ The labels go through `grep -F`, not a pattern. `c++-abi` in an extended +# The labels go through `grep -F`, not a pattern. `c++-abi` in an extended # regular expression is a repetition operator applied to a repetition operator, # which GNU grep tolerates and BSD grep rejects outright — measured on macOS: # grep: repetition-operator operand invalid diff --git a/tests/e2e/281_target_side_rules.sh b/tests/e2e/281_target_side_rules.sh index 357419f8..e1ed164c 100755 --- a/tests/e2e/281_target_side_rules.sh +++ b/tests/e2e/281_target_side_rules.sh @@ -54,7 +54,7 @@ out="$(MCPP_TOOLCHAIN=gcc@16.1.0 "$MCPP" build 2>&1)" || rc=$? [[ "$rc" -ne 0 ]] || { echo "a requirement gcc does not meet was accepted:"; echo "$out"; exit 1; } grep -q "requires the compiler to be" <<< "$out" || { echo "the refusal does not name the requirement:"; echo "$out"; exit 1; } -# ⚠️ THE NEXT STEP MUST BE ONE THAT WOULD ACTUALLY WORK, AND UNTIL 2026.8.26.2 +# THE NEXT STEP MUST BE ONE THAT WOULD ACTUALLY WORK, AND UNTIL 2026.8.26.2 # THE FIRST ONE OFFERED WAS `mcpp toolchain default llvm`. # # That is a GLOBAL change — the default for every project on the machine — @@ -66,7 +66,7 @@ grep -q "requires the compiler to be" <<< "$out" || { # decided. grep -q "remove it" <<< "$out" || { echo "the refusal names no next step:"; echo "$out"; exit 1; } -# ⚠️ `if`, NOT `grep … && { … }`. Under `set -e` a trailing `&&` list whose +# `if`, NOT `grep … && { … }`. Under `set -e` a trailing `&&` list whose # left side fails takes the script down — and here grep FAILING is the passing # case. This repo has paid for that shape more than once. if grep -q "mcpp toolchain default" <<< "$out"; then @@ -150,7 +150,7 @@ grep -q "not a layer a package can supply" <<< "$out" || { # ── The layer vocabulary must be extensible by a published package ────────── # -# ⚠️ This one comes from a measurement, and the behaviour it replaces made the +# This one comes from a measurement, and the behaviour it replaces made the # vocabulary permanently un-extendable: # # error: dependency 'openkal-llvm-runtime': mcpp.toml: error: diff --git a/tests/e2e/282_target_is_a_request.sh b/tests/e2e/282_target_is_a_request.sh index ff9d3214..7e7fa5d0 100755 --- a/tests/e2e/282_target_is_a_request.sh +++ b/tests/e2e/282_target_is_a_request.sh @@ -47,7 +47,7 @@ cd app # ── 1. Declining to name a C library is not naming `gnu` ──────────────────── # -# ⚠️ `|| true`, and that is the subject rather than a concession: the stand-in +# `|| true`, and that is the subject rather than a concession: the stand-in # package declares the layer without supplying one, so the LINK afterwards has # no C library to find. What this file asserts — the report and the refusal — # is complete before a single object is compiled. @@ -63,7 +63,7 @@ grep -q "Target x86_64-linux-gnu" <<< "$out" && { # ── 2. Naming one the graph disagrees with is reported, not refused ───────── # -# ⚠️ The severity was decided by a measurement. Refusing is the clean answer — +# The severity was decided by a measurement. Refusing is the clean answer — # only one of the two names can describe the artifact — and it broke every # project and CI configuration spelling the host target `x86_64-linux-gnu`, # which is what `mcpp toolchain list` prints and therefore what people write. diff --git a/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh b/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh index ad631323..678f45d8 100755 --- a/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh +++ b/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh @@ -2,7 +2,7 @@ # requires: gcc # `mcpp run --target X` must not write X's build into the HOST's cache slot. # -# ⚠️ WHAT MAKES THIS FAIL SILENTLY RATHER THAN LOUDLY. +# WHAT MAKES THIS FAIL SILENTLY RATHER THAN LOUDLY. # # The build cache is keyed on the target triple, and `mcpp run`'s fast path # matches the entry whose key is EMPTY — empty means "built for this machine", @@ -23,7 +23,7 @@ # Running `target/riscv64-none-elf/…/bin/openkal-same-source` # exit=1 # -# ⭐ THE ASSERTION IS ON THE PATH, NOT ON THE EXIT CODE. A foreign-architecture +# THE ASSERTION IS ON THE PATH, NOT ON THE EXIT CODE. A foreign-architecture # artifact fails to exec, so a test checking only the status would pass for the # wrong reason there and MISS the defect wherever the foreign binary happens to # run — which is what a same-architecture cross build does. What is wrong is @@ -73,7 +73,7 @@ if ! out="$("$MCPP" run 2>&1)"; then exit 1 fi -# ⭐ The load-bearing line. `Running `…`` names the artifact's path, and the +# The load-bearing line. `Running `…`` names the artifact's path, and the # path names the target it was built for. ran="$(printf '%s\n' "$out" | sed -n 's/.*Running `\([^`]*\)`.*/\1/p' | head -1)" if [ -z "$ran" ]; then diff --git a/tests/e2e/284_env_segment_is_optional_everywhere.sh b/tests/e2e/284_env_segment_is_optional_everywhere.sh index 4c38712c..e58956ed 100755 --- a/tests/e2e/284_env_segment_is_optional_everywhere.sh +++ b/tests/e2e/284_env_segment_is_optional_everywhere.sh @@ -2,7 +2,7 @@ # requires: gcc # `arch-os` is a target on every platform, not only on Linux. # -# ⚠️ WHY THIS WAS ASYMMETRIC AND WHY THE ASYMMETRY COST SOMETHING. +# WHY THIS WAS ASYMMETRIC AND WHY THE ASYMMETRY COST SOMETHING. # # A target triple states a REQUEST, and a request must be able to say nothing. # `x86_64-linux` was accepted on that basis; `x86_64-windows` was rejected with @@ -14,7 +14,7 @@ # compiler is clang, the linker lld, the C library musl and the C++ runtime # libc++: nothing in the build is GNU. # -# ⭐ THE ASSERTION IS ON THE IDENTITY, NOT ONLY ON THE EXIT STATUS. Accepting the +# THE ASSERTION IS ON THE IDENTITY, NOT ONLY ON THE EXIT STATUS. Accepting the # short spelling is worth nothing if it produces a second output directory or a # second fingerprint: the project would then build twice, cache nothing, and the # two artefacts could drift. The identity must stay total — `x86_64-windows` IS @@ -63,7 +63,7 @@ check_pair() { fi fp_short="$(ls "target/$canonical" | head -1)" - # ⭐ The load-bearing step: the long spelling must land on the SAME + # The load-bearing step: the long spelling must land on the SAME # fingerprint, which is what makes the second build a cache hit rather than # a second full build. "$MCPP" build --target "$canonical" >/dev/null 2>&1 || { @@ -99,7 +99,7 @@ echo "OK: the env segment is optional on every platform, and declining it change # ── The Windows C-library axis has a name of its own ───────────────────────── # -# ⚠️ `x86_64-windows-musl` is a target mcpp names and LLVM cannot. Measured on +# `x86_64-windows-musl` is a target mcpp names and LLVM cannot. Measured on # llvm 22.1.8, handing `windows` with a `musl` environment to clang is not a # diagnostic but an ICE inside the COFF writer: # @@ -109,7 +109,7 @@ echo "OK: the env segment is optional on every platform, and declining it change # different strings — which they already are, either side of the arrow in the # build report. # -# ⭐ THE ASSERTION IS THAT IT IS A KNOWN TARGET WITH A CORRECT DIAGNOSIS, NOT +# THE ASSERTION IS THAT IT IS A KNOWN TARGET WITH A CORRECT DIAGNOSIS, NOT # THAT IT BUILDS. This project names no dependency, so nothing supplies the # target side and the build cannot succeed — correctly. What is under test is # that mcpp recognises the name and says the true thing about it, rather than @@ -131,7 +131,7 @@ case "$out" in exit 1 ;; esac -# ⭐ AND THE PIN IS ASSERTED, BECAUSE THE PIN IS WHAT THE ROW IS FOR. +# AND THE PIN IS ASSERTED, BECAUSE THE PIN IS WHAT THE ROW IS FOR. # # The row names `llvm` not as a preference but because a global default of gcc # would otherwise be carried onto a target no gcc can emit, and the error would @@ -142,7 +142,7 @@ esac # Resolved llvm@22.1.8 → x86_64-windows-musl → …/xim-x-llvm/22.1.8/bin/clang++ # target default for x86_64-windows-musl, replacing your gcc@16.1.0 # -# ⚠️ THIS WAS WRITTEN WITH AN `*) : ;;` FALLBACK, WHICH MADE IT UNFAILABLE. +# THIS WAS WRITTEN WITH AN `*) : ;;` FALLBACK, WHICH MADE IT UNFAILABLE. # A branch that accepts anything is not a check, and this file's whole subject # is a name that used to be accepted by nothing. The fallback is gone; if the # report's phrasing changes, this is meant to go red and be updated. diff --git a/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh b/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh index bf294df2..fe1f22e8 100755 --- a/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh +++ b/tests/e2e/285_kernel_abi_from_graph_keeps_the_payload_c_library.sh @@ -3,7 +3,7 @@ # A package may supply the kernel interface while the C library stays the # payload's, and the link line has to keep reaching the payload. # -# ⚠️ THE PREDICATE THAT DECIDED THIS WAS AN `OR` OVER TWO LAYERS. +# THE PREDICATE THAT DECIDED THIS WAS AN `OR` OVER TWO LAYERS. # # bool system_from_graph() const { # return kernelAbi.fromGraph() || cAbi.fromGraph(); @@ -21,14 +21,14 @@ # crti.o (bare name — the linker cannot resolve it) # crtn.o (bare name — the linker cannot resolve it) # -# ⭐ THE SHAPE IS NOT EXOTIC — IT IS HOW EVERY openkal BACKEND IS TESTED. +# THE SHAPE IS NOT EXOTIC — IT IS HOW EVERY openkal BACKEND IS TESTED. # openkal-linux, openkal-macos and openkal-windows each build their conformance # suite against the platform's own C library, because a backend implements # openkal ON TOP OF that platform. All three went red the moment their CI pin # moved onto the release carrying this, and stayed green before it — which is # the only reason the defect shipped. # -# ⚠️ AND NOTHING IN THIS SUITE HAD THIS SHAPE. 278 e2e scripts, and the +# AND NOTHING IN THIS SUITE HAD THIS SHAPE. 278 e2e scripts, and the # combination "kernel-abi from the graph, C library from the payload" appeared # in none of them. That is what this file is for. set -e @@ -43,7 +43,7 @@ cd "$work/app" # program naming it alone gets its kernel interface from the graph and # everything else — C library, C++ runtime, startup files — from the payload. # -# ⚠️ AND WITHOUT `features = ["standalone"]`, WHICH IS A DIFFERENT SHAPE. +# AND WITHOUT `features = ["standalone"]`, WHICH IS A DIFFERENT SHAPE. # That feature says this implementation is the whole of the program's # environment, so it supplies the entry point; asked for beside a C library # that supplies one too, the link ends in @@ -64,7 +64,7 @@ default = "gcc@16.1.0" [dependencies] openkal-linux = "0.5.4" TOML -# ⭐ THE PROGRAM USES THE C++ RUNTIME, NOT ONLY THE C LIBRARY. +# THE PROGRAM USES THE C++ RUNTIME, NOT ONLY THE C LIBRARY. # # `int main() { return 0; }` links against almost nothing and would pass while # a second defect of the same family was live: the contract table decided @@ -115,7 +115,7 @@ if [ "$rc" != 0 ]; then exit 1 fi -# ⭐ AND THE ARTEFACT, BECAUSE A LINK THAT SUCCEEDS IS NOT THE CLAIM. +# AND THE ARTEFACT, BECAUSE A LINK THAT SUCCEEDS IS NOT THE CLAIM. # The claim is that the payload's C runtime was reached; a program that links # without a C library at all would also exit 0 here. bin="$(find target -type f -name kabi | head -1)" diff --git a/tests/e2e/286_the_openkal_stack_still_builds.sh b/tests/e2e/286_the_openkal_stack_still_builds.sh index 29d14208..5888540d 100755 --- a/tests/e2e/286_the_openkal_stack_still_builds.sh +++ b/tests/e2e/286_the_openkal_stack_still_builds.sh @@ -2,7 +2,7 @@ # requires: llvm unix-shell # The whole target side from packages: kernel interface, C library, C++ runtime. # -# ⚠️ WHY THIS FILE EXISTS, AND WHAT IT COST NOT TO HAVE IT. +# WHY THIS FILE EXISTS, AND WHAT IT COST NOT TO HAVE IT. # # mcpp and openkal are separate projects, and the engine names no # implementation — that separation is the point of `mcpp:` capabilities. @@ -21,7 +21,7 @@ # manifest had the shape. See `285_…` for the narrow case; this file covers the # arrangement the ecosystem actually ships. # -# ⭐ THE ASSERTIONS ARE ABOUT THE ARTEFACT, NOT ABOUT EXIT CODES. A build that +# THE ASSERTIONS ARE ABOUT THE ARTEFACT, NOT ABOUT EXIT CODES. A build that # resolves the wrong C library still exits 0; what it cannot do is produce a # static image with no interpreter and no reference to the host's loader. set -e @@ -105,12 +105,12 @@ case "$desc" in echo " $desc"; exit 1 ;; esac -# ⭐ NO INTERPRETER. `statically linked` from `file` is a summary; the program +# NO INTERPRETER. `statically linked` from `file` is a summary; the program # header is the fact. A dynamic image names the host's loader here, and that is # a path the target machine has no reason to have. if command -v readelf > /dev/null 2>&1; then phdrs="$(readelf -l "$bin" 2>/dev/null || true)" - # ⚠️ THIS ASSERTS AN ABSENCE, SO IT MUST FIRST ESTABLISH THAT SOMETHING WAS + # THIS ASSERTS AN ABSENCE, SO IT MUST FIRST ESTABLISH THAT SOMETHING WAS # READ. `readelf -l` on a file it cannot parse prints zero lines and exits # quietly; `grep -q INTERP` then finds nothing, which reads exactly like a # static image. e2e 287 shipped that mistake with a disassembler and CI diff --git a/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh b/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh index 0d6b8118..c7413f6a 100755 --- a/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh +++ b/tests/e2e/287_the_openkal_stack_crosses_to_aarch64.sh @@ -2,18 +2,18 @@ # requires: llvm unix-shell # The same stack, for a machine this one is not. # -# ⚠️ `aarch64-linux-musl` APPEARED IN NO e2e SCRIPT UNTIL THIS ONE. It is a +# `aarch64-linux-musl` APPEARED IN NO e2e SCRIPT UNTIL THIS ONE. It is a # `verified` row of the target table and the acceptance target named in # mcpp-community/mcpp#492, and 278 scripts mentioned it zero times. What # verified it was a hand-written probe run once — which says the target worked # on the day someone looked, and nothing about tomorrow. # -# ⭐ CROSSING IS WHERE A TARGET SIDE FROM THE GRAPH EARNS ITS KEEP, AND WHERE +# CROSSING IS WHERE A TARGET SIDE FROM THE GRAPH EARNS ITS KEEP, AND WHERE # ITS MISTAKES ARE VISIBLE. A native build that quietly takes the payload's C # library still runs; a cross that does produces an artefact for the wrong # machine, and `file` says so in one line. # -# ⚠️ AND THE HELPERS ARE ASSERTED, NOT JUST THE ARCHITECTURE. clang turns on +# AND THE HELPERS ARE ASSERTED, NOT JUST THE ARCHITECTURE. clang turns on # `+outline-atomics` for aarch64 whenever the compiler runtime is compiler-rt, # and the `__aarch64_*` helpers that feature calls live in compiler-rt rather # than anywhere the engine could supply. A build that links without them is not @@ -97,7 +97,7 @@ if [ -n "$nm" ]; then fi fi -# ⚠️⚠️ A DISASSEMBLER THAT CANNOT READ THIS FILE ANSWERS "NO LSE", AND THE +# A DISASSEMBLER THAT CANNOT READ THIS FILE ANSWERS "NO LSE", AND THE # FIRST VERSION OF THIS BELIEVED IT. # # `command -v objdump` on a Linux runner finds GNU binutils, whose BFD is built @@ -115,7 +115,7 @@ fi # where /usr/bin/llvm-objdump happens to exist and is picked first, and failed # in CI where it does not — the first time this test had ever run there. # -# ⭐ SO THE TOOL IS TAKEN FROM THE TOOLCHAIN THAT BUILT THE BINARY. mcpp +# SO THE TOOL IS TAKEN FROM THE TOOLCHAIN THAT BUILT THE BINARY. mcpp # installed llvm to compile this; its llvm-objdump reads every target clang # emits, by construction. objdump="" @@ -127,7 +127,7 @@ done if [ -n "$objdump" ]; then disasm="$("$objdump" -d "$bin" 2>/dev/null || true)" - # ⭐ THE GUARD THAT SEPARATES THE TWO ANSWERS. Zero instruction lines means + # THE GUARD THAT SEPARATES THE TWO ANSWERS. Zero instruction lines means # the tool could not read the file; it is not evidence about LSE, and # reporting it as such is how a green suite hides a broken measurement. insns="$(printf '%s\n' "$disasm" | grep -cE '^\s*[0-9a-f]+:' || true)" diff --git a/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh b/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh index e3528d6d..b7aced54 100755 --- a/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh +++ b/tests/e2e/288_the_openkal_stack_on_a_machine_with_no_os.sh @@ -2,7 +2,7 @@ # requires: llvm unix-shell qemu-riscv # openkal where there is no operating system, and no C library either. # -# ⚠️ THE FREESTANDING SHAPE IS THE ONE THE ENGINE MODELS MOST AND TESTS LEAST. +# THE FREESTANDING SHAPE IS THE ONE THE ENGINE MODELS MOST AND TESTS LEAST. # `riscv64-none-elf` appears in ten e2e scripts and in none of them does a real # openkal package supply the platform: they build a program with no dependency # at all, which exercises the freestanding link path and not the seam this @@ -10,13 +10,13 @@ # by openkal-opensbi, which answers to the SBI a machine's firmware provides — # while `cAbi` is genuinely absent. # -# ⭐ `cAbi.absent()` IS A DISTINCT ORIGIN, AND THE LINK LINE BRANCHES ON IT. +# `cAbi.absent()` IS A DISTINCT ORIGIN, AND THE LINK LINE BRANCHES ON IT. # A hosted target whose C library comes from a package and one that has no C # library at all take different flags (`-nostdlib -static` for the latter), and # the predicate that chooses between them was, until 2026-08-25, an OR over two # layers. Four origins, four behaviours; this covers the one no other e2e does. # -# ⚠️ AND THE PROGRAM RUNS UNDER QEMU RATHER THAN MERELY LINKING. A freestanding +# AND THE PROGRAM RUNS UNDER QEMU RATHER THAN MERELY LINKING. A freestanding # image that links is not evidence: the entry point, the linker script and the # ordering of the startup objects are BSP facts, and the only check that reaches # them is the machine printing what the program told it to. @@ -37,7 +37,7 @@ version = "0.1.0" [toolchain] default = "llvm@22.1.8" -# ⚠️ `sysroot = ""` IS WHAT MAKES THIS THE NO-C-LIBRARY SHAPE, AND THE TARGET +# `sysroot = ""` IS WHAT MAKES THIS THE NO-C-LIBRARY SHAPE, AND THE TARGET # NAME DOES NOT IMPLY IT. # # `riscv64-none-elf` names a machine with no operating system; whether the @@ -66,7 +66,7 @@ import openkal.abort; // No : there is no C library here. The only way out of this program is // the interface the platform package implements. -// ⚠️ `extern "C"`, AND THE THREE-ARGUMENT SIGNATURE THE ENTRY POINT DECLARES. +// `extern "C"`, AND THE THREE-ARGUMENT SIGNATURE THE ENTRY POINT DECLARES. // `-ffreestanding` removes the compiler's special handling of `main`, so the // name is an ordinary symbol and has to match what calls it. The platform // package's `standalone` feature runs the initialisers and then calls @@ -99,9 +99,9 @@ case "$out" in *) echo "FAIL: the platform did not come from the graph" printf '%s\n' "$out" | grep -E 'abi' | sed 's/^/ /'; exit 1 ;; esac -# ⭐ AND THE `c-abi` LINE NAMES NOTHING. +# AND THE `c-abi` LINE NAMES NOTHING. # -# ⚠️ THE FIRST VERSION OF THIS ASSERTED THE LINE WAS ABSENT, WHICH IS THE WRONG +# THE FIRST VERSION OF THIS ASSERTED THE LINE WAS ABSENT, WHICH IS THE WRONG # CRITERION AND WOULD HAVE FAILED ON A CORRECT BUILD. The report prints one row # per layer and says what each resolved to; a layer that resolved to nothing is # reported as nothing: @@ -132,7 +132,7 @@ case "$desc" in esac # ── It boots on a machine whose firmware provides the SBI ────────────────── -# ⚠️ THE PAYLOAD'S COPY FIRST, AND `command -v` ONLY AS A FALLBACK. +# THE PAYLOAD'S COPY FIRST, AND `command -v` ONLY AS A FALLBACK. # # On a machine with xlings there is a `qemu-system-riscv64` on PATH that is a # shim, and asking it to run anything answers: @@ -160,7 +160,7 @@ else wait $pid 2>/dev/null || true kill $guard 2>/dev/null || true - # ⚠️ The emulated console ends its lines with CRLF, so a comparison that + # The emulated console ends its lines with CRLF, so a comparison that # does not strip them fails on a line that is correct. if tr -d '\r' < "$log" | grep -q 'okbare alive'; then echo " ok it boots and prints through openkal.stream" diff --git a/tests/e2e/289_one_host_reaches_every_openkal_target.sh b/tests/e2e/289_one_host_reaches_every_openkal_target.sh index 0673477c..cbd81c4c 100755 --- a/tests/e2e/289_one_host_reaches_every_openkal_target.sh +++ b/tests/e2e/289_one_host_reaches_every_openkal_target.sh @@ -2,7 +2,7 @@ # requires: llvm unix-shell # One source, one host, every machine the ecosystem serves. # -# ⭐⭐ THIS IS CHEAP BECAUSE OF WHAT THE ECOSYSTEM IS, AND THAT IS THE POINT. +# THIS IS CHEAP BECAUSE OF WHAT THE ECOSYSTEM IS, AND THAT IS THE POINT. # # A traditional stack needs a macOS runner to test macOS and a Windows runner # to test Windows, because the target side comes from a payload that only @@ -11,13 +11,13 @@ # every target, and the shape of that artefact is checkable without leaving it. # Coverage that would cost six runners costs one loop. # -# ⚠️ WHAT THIS FILE IS FOR IS BREADTH, NOT DEPTH. 286, 287 and 288 each go deep +# WHAT THIS FILE IS FOR IS BREADTH, NOT DEPTH. 286, 287 and 288 each go deep # on one arrangement — the native stack, the aarch64 cross with its # outline-atomics helpers, the machine with no operating system. This asserts # the one property they cannot: that a change to the engine did not silently # stop serving a target nobody happened to build that day. # -# ⚠️ A TARGET THAT CANNOT BE BUILT HERE IS SKIPPED WITH ITS REASON, NEVER +# A TARGET THAT CANNOT BE BUILT HERE IS SKIPPED WITH ITS REASON, NEVER # PASSED OVER SILENTLY. A sweep whose failure mode is "produced no output" # would report success on a run that swept nothing. set -e @@ -48,7 +48,7 @@ while IFS='|' read -r target deps want_fmt want_link; do { printf '[package]\nname = "sweep"\nversion = "0.1.0"\n\n' printf '[toolchain]\ndefault = "llvm@22.1.8"\n\n' - # ⚠️ `sysroot = ""` IS HOW A PROJECT SAYS "NO C LIBRARY", AND IT IS NOT + # `sysroot = ""` IS HOW A PROJECT SAYS "NO C LIBRARY", AND IT IS NOT # IMPLIED BY THE TARGET. `riscv64-none-elf` names a machine with no # operating system; whether the program has a C library is a separate # statement, and the ecosystem's own bare-metal example makes it. Left @@ -65,7 +65,7 @@ while IFS='|' read -r target deps want_fmt want_link; do printf "$deps\n" } > mcpp.toml - # ⚠️ TWO SOURCES, BECAUSE TWO OF THESE TARGETS HAVE NO C LIBRARY TO PRINT + # TWO SOURCES, BECAUSE TWO OF THESE TARGETS HAVE NO C LIBRARY TO PRINT # WITH. The bare-metal row reaches the platform interface directly; every # other row is an ordinary hosted program. Writing one source that works # everywhere would mean writing to the lowest common denominator, which is @@ -138,7 +138,7 @@ EOF printf '\n built %d, skipped %d, failed %d\n' "$built" "$skipped" "$failed" [ "$failed" = 0 ] || exit 1 -# ⭐ AND A SWEEP THAT SWEPT NOTHING IS NOT A PASS. Every row skipping is the +# AND A SWEEP THAT SWEPT NOTHING IS NOT A PASS. Every row skipping is the # signature of a machine that cannot reach the index at all, which is worth # reporting as a skip of the whole file rather than as success. if [ "$built" = 0 ]; then diff --git a/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh b/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh index ba33ec69..2389458c 100755 --- a/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh +++ b/tests/e2e/290_the_declaration_puts_an_environment_in_front.sh @@ -2,7 +2,7 @@ # requires: gcc unix-shell # A build program looks up a tool and finds the one the PROJECT declared. # -# ⚠️ IT USED TO FIND WHATEVER THE MACHINE HAD, AND `command -v` CANNOT TELL THE +# IT USED TO FIND WHATEVER THE MACHINE HAD, AND `command -v` CANNOT TELL THE # DIFFERENCE. Measured 2026-08-25 with a build program that printed its own # PATH: the project's declared environment appeared nowhere in it, and # `command -v qemu-system-riscv64` returned a shim that answers, when run, @@ -12,14 +12,14 @@ # — found, reported present, unable to execute, while a working copy sat in an # environment the build could not reach. # -# ⭐⭐ THIS FILE ASSERTS BOTH DIRECTIONS, BECAUSE ONLY ONE OF THEM IS THE +# THIS FILE ASSERTS BOTH DIRECTIONS, BECAUSE ONLY ONE OF THEM IS THE # FEATURE. Prepending unconditionally would pass the "declared" half, and that # is the design that was withdrawn: a shared directory in front of every # project makes what a build sees depend on what else was installed on that # machine. The declaration is what puts it there, so a project that declares # nothing must come out unchanged. # -# ⚠️⚠️ AND "UNCHANGED" IS COMPARED AGAINST THE INHERITED VALUE, NOT AGAINST A +# AND "UNCHANGED" IS COMPARED AGAINST THE INHERITED VALUE, NOT AGAINST A # PATTERN. The first version of this half rejected a first entry matching # `*/subos/*/bin` — and CI's own PATH already begins with one, because the # runner activates an xlings environment to get mcpp at all: @@ -34,7 +34,7 @@ MCPP="${MCPP:-mcpp}" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT -# ⚠️ A NON-ZERO EXIT, BECAUSE THAT IS WHAT MAKES THE OUTPUT VISIBLE. mcpp +# A NON-ZERO EXIT, BECAUSE THAT IS WHAT MAKES THE OUTPUT VISIBLE. mcpp # prints a build program's stdout only when it fails — a probe that succeeds # says nothing, which is a property of the protocol and not of this test. probe='import std; @@ -110,7 +110,7 @@ case "$first" in exit 1 ;; esac -# ⭐ AND THE INHERITED VALUE IS STILL THERE, WHOLE, BEHIND IT. Prefixing means +# AND THE INHERITED VALUE IS STILL THERE, WHOLE, BEHIND IT. Prefixing means # the rest is untouched; a test that only checked the first entry would pass on # a PATH that had thrown everything else away, which would break every build # program that calls `git`, `python3` or a shell. diff --git a/tests/e2e/291_dynamic_linkage_is_refused_only_when_the_c_library_is_the_graphs.sh b/tests/e2e/291_dynamic_linkage_is_refused_only_when_the_c_library_is_the_graphs.sh index 2f208631..f2b427d3 100755 --- a/tests/e2e/291_dynamic_linkage_is_refused_only_when_the_c_library_is_the_graphs.sh +++ b/tests/e2e/291_dynamic_linkage_is_refused_only_when_the_c_library_is_the_graphs.sh @@ -2,7 +2,7 @@ # requires: gcc llvm elf unix-shell # `linkage = "dynamic"` is reported as ineffective only when it actually is. # -# ⚠️ THE PREDICATE USED TO SPAN TWO LAYERS AND THE REASON SPANS ONE. The +# THE PREDICATE USED TO SPAN TWO LAYERS AND THE REASON SPANS ONE. The # warning's own justification — "those packages are compiled into this build as # objects, and there is no shared object to link against" — is a property of # the C LIBRARY. A backend that runs ON a platform takes its kernel interface @@ -15,7 +15,7 @@ # $ file …/dynprobe → dynamically linked # $ readelf -d … → NEEDED libm.so.6, libgcc_s.so.1, libc.so.6 # -# ⭐⭐ BOTH DIRECTIONS, BECAUSE ONLY ONE OF THEM IS THE FIX. Deleting the +# BOTH DIRECTIONS, BECAUSE ONLY ONE OF THEM IS THE FIX. Deleting the # warning outright also stops it lying, and that would lose the diagnostic the # directive needs when the C library really does come from the graph — which is # why the second half builds that arrangement and requires the warning to @@ -26,7 +26,7 @@ MCPP="${MCPP:-mcpp}" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT -# ⚠️ AN EXPLICIT `--target` IS LOAD-BEARING. `[target.]` applies to the +# AN EXPLICIT `--target` IS LOAD-BEARING. `[target.]` applies to the # target that was REQUESTED; a bare `mcpp build` requests none, the row never # applies, `linkage` stays empty, and both halves of this test would pass # without exercising anything. @@ -82,11 +82,11 @@ if warned "$out"; then fi echo " ok no warning when the C library is the payload's" -# ⭐ AND THE ARTIFACT AGREES. The warning's claim is "The artifact is static"; +# AND THE ARTIFACT AGREES. The warning's claim is "The artifact is static"; # a test that only checked for the absence of the text would pass on a build # that silently produced a static binary anyway. dyn="$(readelf -d "$bin" 2>&1 || true)" -# ⚠️ "no dynamic section" AND "readelf said nothing" BOTH COUNT ZERO `NEEDED`. +# "no dynamic section" AND "readelf said nothing" BOTH COUNT ZERO `NEEDED`. # The first is the answer a static binary gives and the second is the answer a # tool that could not read the file gives, and only one of them is about this # build. `readelf -d` on a static ELF says so in words; on a file it cannot diff --git a/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh b/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh index 463da7c5..7f4f12df 100755 --- a/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh +++ b/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh @@ -3,7 +3,7 @@ # Declaring which layer a package supplies must not change which compiler can # emit the target. # -# ⚠️ MEASURED 2026-08-25, ON A THREE-LINE MANIFEST. Adding one `provides` line +# MEASURED 2026-08-25, ON A THREE-LINE MANIFEST. Adding one `provides` line # to a project moved a bare-metal RISC-V build onto the host's g++: # # provides = ["mcpp:kernel-abi=openkal"] @@ -19,7 +19,7 @@ # target at all: `clang`/`lld` are cross-compilers by construction and a host # g++ cannot produce riscv64-none-elf whatever the graph contains. # -# ⭐⭐ BOTH DIRECTIONS, BECAUSE ONLY ONE OF THEM IS THE FIX. Never cancelling +# BOTH DIRECTIONS, BECAUSE ONLY ONE OF THEM IS THE FIX. Never cancelling # the pin also stops this failing, and it would restore the substitution the # predicate was added to prevent — a hosted project whose C library comes from # its graph having its chosen toolchain silently replaced. The second half diff --git a/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh b/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh index e795b5da..3aa312e0 100755 --- a/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh +++ b/tests/e2e/293_the_requested_target_and_the_resolved_one_name_one_os.sh @@ -2,7 +2,7 @@ # requires: gcc unix-shell # A build for one operating system is never quietly performed for another. # -# ⚠️ MEASURED 2026-08-25 IN CI. On a machine that had installed only a native +# MEASURED 2026-08-25 IN CI. On a machine that had installed only a native # gcc, the cross payload was absent, resolution fell back to the host compiler, # and the report said so on one line while the build carried on: # @@ -15,7 +15,7 @@ # function — a symbol, not the decision that produced it. openkal-uefi reached # the same fallback at the linker: `ld: unrecognized option '--subsystem'`. # -# ⭐⭐ BOTH DIRECTIONS, BECAUSE A REFUSAL THAT FIRES TOO OFTEN IS WORSE THAN THE +# BOTH DIRECTIONS, BECAUSE A REFUSAL THAT FIRES TOO OFTEN IS WORSE THAN THE # DEFECT. `x86_64-windows-gnu → x86_64-w64-windows-gnu` differs in vendor and # spelling and is exactly right; refusing on anything but the OS would reject # every correct cross build. The second half is a sweep that must not refuse. @@ -38,7 +38,7 @@ make_project() { # dir target-section # Windows target is the one arrangement that reproduces CI's fallback without # uninstalling anything. # -# ⚠️ AND SINCE THE SYSTEM TOOLCHAIN IS REFUSED, THAT REFUSAL IS ALSO AN ANSWER +# AND SINCE THE SYSTEM TOOLCHAIN IS REFUSED, THAT REFUSAL IS ALSO AN ANSWER # TO THIS TEST'S QUESTION. mcpp builds only with toolchains it manages # (`msvc@system` excepted), and the refusal fires before target resolution — so # a Windows target can no longer reach a Linux host compiler through this door @@ -49,7 +49,7 @@ make_project "$work/mismatch" '[target.x86_64-windows-gnu] toolchain = "system"' out="$(cd "$work/mismatch" && "$MCPP" build --target x86_64-windows-gnu 2>&1 || true)" -# ⚠️⚠️ AND A SKIP HERE HAS TO BE EARNED, OR THE TEST CANNOT SEE A REVERT. +# AND A SKIP HERE HAS TO BE EARNED, OR THE TEST CANNOT SEE A REVERT. # The first draft took the skip branch whenever no refusal appeared — which is # precisely what the unfixed build does, so reverting the fix turned this test # green-by-silence rather than red. The arrangement either reproduced (the @@ -63,7 +63,7 @@ case "$out" in # The stronger refusal: the arrangement cannot be expressed any more, so a # Windows target never reaches a Linux compiler through this door. # - # ⚠️ AND IT MUST NOT `exit 0` HERE. Half two is independent of half one and + # AND IT MUST NOT `exit 0` HERE. Half two is independent of half one and # asserts that correct cross builds still go through; leaving early skips # it, and the ecosystem job that runs this file checks that each test "ran # to its conclusion" precisely so an early exit cannot masquerade as a @@ -79,7 +79,7 @@ case "$out" in && printf '%s' "$resolved" | grep -q 'linux'; then echo "FAIL: a Windows target resolved to a Linux toolchain and the build went on" echo " $reported" - # ⚠️ THE WHOLE OUTPUT, because the one line above says WHAT happened and + # THE WHOLE OUTPUT, because the one line above says WHAT happened and # not which decision produced it. This failure first appeared only in # CI, where the arrangement differs from any machine it was written on, # and a one-line report cannot be read backwards into a cause. @@ -92,7 +92,7 @@ case "$out" in exit 0 ;; esac -# ⭐ AND THE MESSAGE NAMES BOTH. A refusal that does not say what it resolved +# AND THE MESSAGE NAMES BOTH. A refusal that does not say what it resolved # to leaves the reader with the same question the report used to answer. # # Only asked of the OS-mismatch refusal. The toolchain refusal is a different @@ -113,7 +113,7 @@ fi # ── Half two: every correct cross build still goes through ──────────────── # -# ⚠️ A sweep that swept nothing is a SKIP, not a pass. +# A sweep that swept nothing is a SKIP, not a pass. built=0; skipped=0 for t in x86_64-linux-gnu x86_64-linux-musl aarch64-linux-musl x86_64-windows-gnu; do make_project "$work/ok-$t" "" diff --git a/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh b/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh index 401f0f64..333d33c0 100755 --- a/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh +++ b/tests/e2e/294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh @@ -3,7 +3,7 @@ # `toolchain list` names the targets this host can build for, including the ones # whose system has to come from a dependency graph. # -# ⚠️ IT USED TO FILTER ON `host_can_serve`, WHICH ANSWERS A NARROWER QUESTION — +# IT USED TO FILTER ON `host_can_serve`, WHICH ANSWERS A NARROWER QUESTION — # "can a payload here serve this target" — and presented it as "can this be # built". Measured 2026-08-25 on Linux: `x86_64-windows-musl` was absent from # the list while the same machine produced a real artefact for it, @@ -15,7 +15,7 @@ # # because its system came from the graph. # -# ⭐⭐ AND THE SECOND HALF IS THE POINT. Listing every vocabulary row would also +# AND THE SECOND HALF IS THE POINT. Listing every vocabulary row would also # make the first half pass, and it would tell a Linux user they can build # `x86_64-windows-msvc` — which needs MSVC, or `aarch64-macos`, which needs the # macOS SDK. Neither is something a dependency can supply. A list that @@ -47,7 +47,7 @@ esac wm="$(row x86_64-windows-musl)" if [ -z "$wm" ]; then echo "FAIL: x86_64-windows-musl is absent, and this host can build it" - # ⚠️ WHICH BINARY ANSWERED. A list missing a row and a list produced by an + # WHICH BINARY ANSWERED. A list missing a row and a list produced by an # older mcpp look identical, and the first CI failure of this test could not # tell them apart — so the evidence names the program as well as its output. echo " asked: ${MCPP} ($("$MCPP" --version 2>&1 | head -1))" @@ -67,7 +67,7 @@ esac # ── Half two: what a graph cannot supply stays out ──────────────────────── # -# ⚠️ ASSERTS THE ROW IS ABSENT, so it first establishes that rows are being +# ASSERTS THE ROW IS ABSENT, so it first establishes that rows are being # printed at all — an empty section would pass this trivially. n="$(printf '%s\n' "$targets" | grep -cE '^\s+\*?\s*[a-z0-9_]+-' || true)" if [ "${n:-0}" -lt 3 ]; then diff --git a/tests/e2e/295_naming_the_host_target_changes_nothing.sh b/tests/e2e/295_naming_the_host_target_changes_nothing.sh index acdcb0c6..a9f858d3 100755 --- a/tests/e2e/295_naming_the_host_target_changes_nothing.sh +++ b/tests/e2e/295_naming_the_host_target_changes_nothing.sh @@ -2,12 +2,12 @@ # requires: gcc unix-shell jq # Spelling out the target this machine already builds for changes nothing. # -# ⭐⭐ THIS IS AN IDENTITY, NOT A THRESHOLD. `mcpp build` and +# THIS IS AN IDENTITY, NOT A THRESHOLD. `mcpp build` and # `mcpp build --target ` describe the same build for the # same machine, so the link line either is the same string or something decided # on the spelling rather than on the build. # -# ⚠️ MEASURED 2026-08-26, ON THE MACHINE THIS WAS WRITTEN ON: +# MEASURED 2026-08-26, ON THE MACHINE THIS WAS WRITTEN ON: # # $ mcpp build → ELF 64-bit LSB pie executable # $ mcpp build --target x86_64-linux-gnu → hermetic link check failed @@ -17,13 +17,13 @@ # -stdlib=libc++ --rtlib=compiler-rt --unwindlib=libunwind # -Wl,--push-state,--as-needed -latomic # -# ⭐ THE CAUSE IS ONE CONDITION. `flags.cppm` asks `!crossTarget.empty()` — is +# THE CAUSE IS ONE CONDITION. `flags.cppm` asks `!crossTarget.empty()` — is # there a `--target=` on the command line — and its own comment says what it # meant to ask: "THE TARGET SIDE COMES FROM THE GRAPH". Those are different # questions, and a project that names its host target while using no # dependencies at all answers yes to the first and no to the second. # -# ⭐ NO EXPECTED VALUES. This asserts a relation between two runs, so it holds +# NO EXPECTED VALUES. This asserts a relation between two runs, so it holds # on every host and needs no table to compare against — which is why it is the # first thing to land. set -e @@ -38,7 +38,7 @@ printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cp # The target this host builds for when nothing is said. Taken from mcpp itself # rather than assembled from `uname`: the point is to name the target mcpp would # have chosen, and deriving it a second way would test the derivation instead. -# ⚠️⚠️ THE TARGET IS PER TOOLCHAIN, NOT PER MACHINE — AND THREE DRAFTS ASSUMED +# THE TARGET IS PER TOOLCHAIN, NOT PER MACHINE — AND THREE DRAFTS ASSUMED # OTHERWISE. # # The identity is "naming the target this build would use anyway changes @@ -52,14 +52,14 @@ printf '#include \nint main() { std::printf("ok\\n"); }\n' > src/main.cp # # — correct behaviour. gcc cannot emit `-msvc`, and the test had asked it to. # -# ⭐ So ask the query what THIS toolchain resolves to with no target named, and +# So ask the query what THIS toolchain resolves to with no target named, and # then name that. Exact on every host, and it needs no table. implicit_target() { # toolchain spec → the triple it would use anyway "$MCPP" why toolchain --toolchain "$1" --format json 2>/dev/null \ | jq -r '.data.triple.toolchain // empty' | tr -d '\r' } -# ⚠️⚠️ TWO LINES, AND FOR A LONG TIME THIS TEST READ ONLY ONE. +# TWO LINES, AND FOR A LONG TIME THIS TEST READ ONLY ONE. # # The identity is about THE BUILD, and a build has a compile line as well as a # link line. `2026.8.26.1` corrected the link side; the compile side kept @@ -82,7 +82,7 @@ line_of() { # channel, extra args… → that line of build.ninja, or nothing rm -rf target "$MCPP" build "$@" >/dev/null 2>&1 || true local f; f="$(find target -name build.ninja 2>/dev/null | head -1)" - # ⚠️ AN EXPLICIT `return 0`, BECAUSE THIS FUNCTION IS ALLOWED TO FIND + # AN EXPLICIT `return 0`, BECAUSE THIS FUNCTION IS ALLOWED TO FIND # NOTHING. The first draft ended on `[ -n "$f" ] && grep …`, whose exit # status under `set -e` is the test's — so the one case this test exists to # examine, a build that produced no link line, killed the script before it @@ -98,7 +98,7 @@ line_of() { # channel, extra args… → that line of build.ninja, or nothing # directory, which differs because the fingerprint does; that is the mechanism # working, not a difference in what is compiled. # -# ⚠️ NOT ANCHORED WITH `^`, AND WINDOWS IS WHY. The ninja channel QUOTES a token +# NOT ANCHORED WITH `^`, AND WINDOWS IS WHY. The ninja channel QUOTES a token # whose path needs it, so the same flag arrives as # `"-fprebuilt-module-path=C$:\Users\..."` — an anchored pattern misses it and # the comparison then fails on the one token this filter exists to remove. @@ -114,9 +114,9 @@ fail=0 checked=0 for tc in gcc llvm; do - # ⚠️ THE TOOLCHAIN IS NAMED, because the defect lives on one of them and a + # THE TOOLCHAIN IS NAMED, because the defect lives on one of them and a # run that silently used the other would pass while proving nothing. - # ⭐ ONE FIELD, NOT A COLUMN. Earlier drafts read `$NF` — which is + # ONE FIELD, NOT A COLUMN. Earlier drafts read `$NF` — which is # `(default)` on exactly the row most likely to be picked — and then # `grep -oP`, which does not exist on macOS and would have turned this half # into a silent skip there. @@ -141,7 +141,7 @@ for tc in gcc llvm; do explicit_cxx="$(line_of cxxflags --target "$ht")" if [ -z "$implicit" ] || [ -z "$explicit" ]; then - # ⚠️ EARNED, NOT ASSUMED. One of the two produced no build.ninja at all, + # EARNED, NOT ASSUMED. One of the two produced no build.ninja at all, # which is itself the asymmetry this test is about — so it is only a # skip when NEITHER produced one. if [ -z "$implicit" ] && [ -z "$explicit" ]; then @@ -164,7 +164,7 @@ for tc in gcc llvm; do else lhs="$implicit_cxx"; rhs="$explicit_cxx" fi - # ⚠️ BOTH SIDES MUST HAVE CONTENT. Two empty strings compare equal, and + # BOTH SIDES MUST HAVE CONTENT. Two empty strings compare equal, and # a comparison that passes on nothing is the false green this file's # other guard already exists for. if [ -z "$lhs" ] || [ -z "$rhs" ]; then diff --git a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh index 7450f48e..e67ba035 100755 --- a/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh +++ b/tests/e2e/296_what_the_report_names_is_what_the_link_line_uses.sh @@ -2,20 +2,20 @@ # requires: gcc unix-shell jq # The layer the report names is the layer the link line reaches for. # -# ⭐⭐ TWO RELATIONS, NO EXPECTED VALUES. Like e2e 295, this compares two things +# TWO RELATIONS, NO EXPECTED VALUES. Like e2e 295, this compares two things # mcpp itself produced rather than checking them against a table, so it holds on # every host and needs nothing installed beyond a working toolchain: # # c-abi (payload) ⇒ the link line must reach into that payload # c-abi (graph) ⇒ the link line must NOT reach into this host's C library # -# ⚠️ THE FIRST ONE IS THE SHAPE THIS RELEASE KEEPS PAYING FOR. `xim:glibc` is +# THE FIRST ONE IS THE SHAPE THIS RELEASE KEEPS PAYING FOR. `xim:glibc` is # installed, carries Scrt1.o/crti.o/crtn.o, and the report says # `c-abi glibc (payload)` — while llvm's link line contained no reference to it # at all and the build failed on startup objects resolved from /lib. The report # and the link line disagreed, and only the report was read. # -# ⚠️ AND THE SECOND ONE IS ITS MIRROR. A graph-supplied C library that still +# AND THE SECOND ONE IS ITS MIRROR. A graph-supplied C library that still # carried this host's loader produced, measured 2026-08-23: # # ld64.lld: error: unknown argument @@ -41,7 +41,7 @@ build_and_read() { # extra args… → sets $report and $ldflags if [ -n "$f" ]; then ldflags="$(grep -m1 '^ldflags' "$f" || true)" fi - # ⚠️⚠️ AN EXPLICIT `return 0`, BECAUSE THIS FUNCTION IS ALLOWED TO FIND + # AN EXPLICIT `return 0`, BECAUSE THIS FUNCTION IS ALLOWED TO FIND # NOTHING — AND THE SAME LINE HAS NOW BEEN WRONG TWICE. # # Written as `[ -n "$f" ] && ldflags=…`, the whole chain returns 1 when @@ -49,17 +49,17 @@ build_and_read() { # extra args… → sets $report and $ldflags # under `set -e` the CALLER dies — silently, mid-script, for exactly the # case the function exists to handle. # - # ⚠️ Measured on windows-2022: relation one printed both its `ok` lines and + # Measured on windows-2022: relation one printed both its `ok` lines and # the script then stopped. No relation two, no conclusion, no error. The # graph build produced no link line there, which is a legitimate outcome # and should have reached the `SKIP` branch twenty lines below. # - # ⭐ 295's `ldflags_of` had the identical line and was fixed with the + # 295's `ldflags_of` had the identical line and was fixed with the # identical comment earlier in the same session. Fixing one instance of a # shape is not fixing the shape: the sibling was three files away and # nobody looked. # - # ⚠️ AND THE SHAPE IS NARROWER THAN IT LOOKS — measured, not reasoned. A + # AND THE SHAPE IS NARROWER THAN IT LOOKS — measured, not reasoned. A # FUNCTION ending in `[ … ] && …` kills its caller; the same list ending a # `{ …; } > file` group does NOT, because errexit exempts a `&&` list whose # failure came from a non-final component. So 290/292/293, which all end @@ -76,10 +76,10 @@ fail=0; checked=0 # ── Relation zero: the machine interface must round-trip ────────────────── # -# ⭐⭐ A CONSUMER READS `family` AND `version`, JOINS THEM, AND HANDS THE RESULT +# A CONSUMER READS `family` AND `version`, JOINS THEM, AND HANDS THE RESULT # BACK. That has to work, and for one entry it did not. # -# ⚠️ Measured on windows-2022: `toolchain list --format json` reported +# Measured on windows-2022: `toolchain list --format json` reported # `msvc@19.44.35228` — cl.exe's version — and `--toolchain msvc@19.44.35228` is # refused by design ("names a COMPILER version, not a toolset"). Every msvc cell # in the target matrix came back `build-failed`, with mcpp rejecting its own @@ -99,7 +99,7 @@ done # ── Relation one: a payload C library must be on the link line ───────────── for tc in gcc llvm; do - # ⭐ From the machine interface: `grep -oP` does not exist on macOS, and its + # From the machine interface: `grep -oP` does not exist on macOS, and its # failure mode here was a skip rather than a red. ver="$("$MCPP" toolchain list --format json 2>/dev/null \ | jq -r --arg t "$tc" '[.data.toolchains[] | select(.family==$t) | .version][0] // empty' | tr -d '\r')" @@ -115,13 +115,13 @@ for tc in gcc llvm; do [ -n "$ldflags" ] || { echo " SKIP $tc@$ver produced no link line"; continue; } checked=$((checked+1)) - # ⭐⭐ THE QUERY NAMES THE DIRECTORY, AND THE LINK LINE MUST CONTAIN IT. + # THE QUERY NAMES THE DIRECTORY, AND THE LINK LINE MUST CONTAIN IT. # # `cLibrary.path` is what `resolve_link_model` decided — the same function # the flag emitter calls — so this asserts that what mcpp SAYS it will pass # is what mcpp passes. Nothing is hardcoded and nothing is per-platform. # - # ⚠️ THE FIRST VERSION HARDCODED `registry/data/xpkgs` AND WAS WRONG ON + # THE FIRST VERSION HARDCODED `registry/data/xpkgs` AND WAS WRONG ON # macOS. Measured on macos-14: # # c-abi libSystem (payload) @@ -133,15 +133,15 @@ for tc in gcc llvm; do want="$("$MCPP" why toolchain --toolchain "$tc@$ver" --format json 2>/dev/null \ | jq -r '.data.cLibrary.path // ""' | tr -d '\r')" if [ -z "$want" ]; then - # ⭐⭐ NO PATH IS ALSO A CLAIM, AND IT IS CHECKABLE. The query says mcpp + # NO PATH IS ALSO A CLAIM, AND IT IS CHECKABLE. The query says mcpp # passes no C-library path — the self-contained arrangement, where the # driver carries its own (mingw g++, MSVC). The assertion is then the # other direction: nothing from OUTSIDE mcpp's store may appear either. # - # ⚠️ THE FIRST VERSION SKIPPED HERE, and on windows-2022 both toolchains + # THE FIRST VERSION SKIPPED HERE, and on windows-2022 both toolchains # took that branch — so relation one had no coverage on that host at all # and the test did not reach its conclusion. - # ⚠️⚠️ THE FLAG AND ITS PATH ARE JOINED FIRST, AND BOTH SPELLINGS EXIST. + # THE FLAG AND ITS PATH ARE JOINED FIRST, AND BOTH SPELLINGS EXIST. # # `-B/usr/lib` is one word and `-B /usr/lib` is two. Splitting on spaces # and matching `^-B` flags the bare `-B` of the second form as a leak @@ -151,7 +151,7 @@ for tc in gcc llvm; do # -B /usr/lib/gcc → [-B] (false positive) # -B /usr/lib/gcc → [] (false negative) # - # ⭐ A false negative is the worse one here — this branch exists to + # A false negative is the worse one here — this branch exists to # catch a link line reaching outside mcpp's store — so the two forms are # made one before anything is decided. leak="$(printf '%s\n' "$ldflags" \ diff --git a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh index 558284d4..aa978bfb 100755 --- a/tests/e2e/297_a_capability_pin_is_not_a_preference.sh +++ b/tests/e2e/297_a_capability_pin_is_not_a_preference.sh @@ -2,7 +2,7 @@ # requires: gcc unix-shell jq # A declared toolchain overrides a convention. It does not override a capability. # -# ⭐⭐ THE TARGET TABLE'S PIN MEANS TWO DIFFERENT THINGS AND ONLY ONE OF THEM IS +# THE TARGET TABLE'S PIN MEANS TWO DIFFERENT THINGS AND ONLY ONE OF THEM IS # A PREFERENCE. # # hosted row `x86_64-linux-musl → gcc@16.1.0` @@ -15,7 +15,7 @@ # because clang/lld are cross-compilers by construction". # A host g++ does not emit riscv64 whatever anyone declares. # -# ⚠️ MEASURED 2026-08-26, before this file existed: +# MEASURED 2026-08-26, before this file existed: # # [toolchain] default = "gcc@16.1.0" # $ mcpp build --target riscv64-none-elf @@ -24,7 +24,7 @@ # # — a message about an option, for a decision made a hundred lines earlier. # -# ⭐⭐ BOTH DIRECTIONS, BECAUSE REFUSING EVERYTHING ALSO STOPS THE BAD MESSAGE. +# BOTH DIRECTIONS, BECAUSE REFUSING EVERYTHING ALSO STOPS THE BAD MESSAGE. # Half two declares gcc for a HOSTED target and requires it to be honoured; a # guard that refused there would take away the escape hatch the whole # convention/preference distinction exists to protect. @@ -37,7 +37,7 @@ mkdir -p "$work/src" cd "$work" printf 'extern "C" int main(int, char**, char**) { return 0; }\n' > src/main.cpp -# ⭐ From mcpp's machine interface rather than from the table it prints for +# From mcpp's machine interface rather than from the table it prints for # people: a column position is not a contract, and two earlier drafts of the # neighbouring test disagreed about which column held the version. gccver="$("$MCPP" toolchain list --format json 2>/dev/null \ @@ -53,13 +53,13 @@ printf '[package]\nname = "capprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefau rm -rf target out="$(cd "$work" && "$MCPP" build --target riscv64-none-elf 2>&1 || true)" -# ⭐⭐ THE CLASSIFICATION COMES FROM THE MACHINE INTERFACE, THE WORDING FROM THE +# THE CLASSIFICATION COMES FROM THE MACHINE INTERFACE, THE WORDING FROM THE # MESSAGE. `data.reason` is a finite token, so "did it refuse, and under which # rule" survives any rewording; the assertions below still require the sentence # to name the target, the reason and the way out, because that is a promise the # code cannot keep on its own. # -# ⚠️ MEASURED COST OF NOT SPLITTING THEM: this file asserted `cannot emit it`, +# MEASURED COST OF NOT SPLITTING THEM: this file asserted `cannot emit it`, # the message was reworded to `cannot be emitted by` in the same session, and # the assertion then passed by matching nothing at all. reason="$(cd "$work" && "$MCPP" why toolchain --target riscv64-none-elf \ @@ -70,14 +70,14 @@ case "$reason" in capability-pin) echo " ok a bare-metal target refuses a compiler that cannot emit it" ;; none) - # ⚠️ THE OLD BEHAVIOUR EXACTLY: resolution succeeded, gcc was handed a + # THE OLD BEHAVIOUR EXACTLY: resolution succeeded, gcc was handed a # riscv64 target, and the complaint arrived a hundred lines later as # `unrecognized argument in option '-mabi=lp64d'`. echo "FAIL: the declaration was honoured and the build was left to fail later" printf '%s\n' "$out" | grep -iE 'error|note' | head -3 | sed 's/^/ /' exit 1 ;; *) - # ⚠️ NEITHER OUTCOME MEANS THIS MACHINE CANNOT RUN THE TEST. A build that + # NEITHER OUTCOME MEANS THIS MACHINE CANNOT RUN THE TEST. A build that # SUCCEEDED with gcc would be a third answer entirely, and one worth # failing on: it would mean a host g++ emitted riscv64. if printf '%s\n' "$out" | grep -q 'Finished'; then @@ -89,7 +89,7 @@ case "$reason" in exit 0 ;; esac -# ⭐ AND THE MESSAGE POINTS AT THE DECISION. A refusal that does not name the +# AND THE MESSAGE POINTS AT THE DECISION. A refusal that does not name the # target and the way out leaves the reader where the `-mabi` message did. ok=1 printf '%s\n' "$out" | grep -q 'riscv64-none-elf' || ok=0 @@ -105,7 +105,7 @@ fi # ── Half two: a hosted target still honours the declaration ─────────────── # -# ⚠️⚠️ THE TARGET COMES FROM THE QUERY, NOT FROM A LITERAL. This half used to +# THE TARGET COMES FROM THE QUERY, NOT FROM A LITERAL. This half used to # name `x86_64-linux-gnu`, which is a hosted row on Linux and a CROSS row on # Windows — where the host target is `x86_64-windows-msvc` and gcc does not # serve it. Measured on windows-2022, the query for that combination produced @@ -136,7 +136,7 @@ case "$hostedReason" in printf '%s\n' "$hosted" | head -4 | sed 's/^/ /' exit 1 ;; none) - # ⭐ AND IT MUST ALSO BUILD. "Not refused" alone would pass on a machine + # AND IT MUST ALSO BUILD. "Not refused" alone would pass on a machine # where resolution succeeded and the link then failed. case "$hosted" in *"Finished"*) echo " ok a hosted target still honours the declared toolchain" ;; diff --git a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh index 787506eb..004e877c 100644 --- a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh +++ b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh @@ -3,7 +3,7 @@ # Naming your own compiler for a pinned target is allowed. Naming it and # supplying nothing in place of what the pin supplied is not. # -# ⭐⭐ THE TWO CASES ARE THE SAME MANIFEST MINUS ONE LINE. +# THE TWO CASES ARE THE SAME MANIFEST MINUS ONE LINE. # # [toolchain] default = "llvm@22.1.8" → refused # @@ -15,7 +15,7 @@ # use for it — which is `examples/06-openkal-cross`. With neither, clang is a # retargetable compiler holding no C library at all. # -# ⚠️ MEASURED 2026-08-26, before this file existed. Both spellings ran the whole +# MEASURED 2026-08-26, before this file existed. Both spellings ran the whole # build and failed at the link: # # --target x86_64-linux-musl @@ -29,7 +29,7 @@ # system mingw. A machine without one fails differently, which is the other # reason the answer must not come from the link. # -# ⚠️⚠️ HALF TWO IS NOT DECORATION. A guard that refused every declared toolchain +# HALF TWO IS NOT DECORATION. A guard that refused every declared toolchain # would pass half one and take away the escape hatch — and the openkal ecosystem # is built entirely on it. set -e @@ -55,7 +55,7 @@ fail=0 # The target is read from the machine rather than hardcoded: any row whose pin # names a family other than llvm will do, and which rows exist is the target # table's business, not this test's. -# ⭐⭐ `pin`, NOT `toolchain` — AND THE DIFFERENCE COST A RED CI RUN. +# `pin`, NOT `toolchain` — AND THE DIFFERENCE COST A RED CI RUN. # # `toolchain` is what the row is associated with: the installed payload on an # installed row, the convention on a vocabulary row. A row can have the first @@ -69,11 +69,11 @@ fail=0 # The status filter matters for the same reason in the other direction: a # `planned` row refuses under a different rule, and half one would pass while # testing nothing. -# ⚠️ AND THE HOST'S OWN ARCHITECTURE FIRST. Selecting alphabetically picked +# AND THE HOST'S OWN ARCHITECTURE FIRST. Selecting alphabetically picked # `aarch64-linux-musl` on an x86_64 runner — the same rule, exercised on a # machine that cannot run the result. # -# ⚠️ THE FIRST VERSION OF THIS COMMENT SAID "because the cross target is +# THE FIRST VERSION OF THIS COMMENT SAID "because the cross target is # expensive to build". Measured on ubuntu-24.04 before writing it down: the # whole invariants step is 85 seconds either way, because openkal's packages # arrive prebuilt. The reason is representativeness, not cost, and a reason @@ -101,7 +101,7 @@ printf '[package]\nname = "convprobe"\nversion = "0.1.0"\n\n[toolchain]\ndefa rm -rf target out="$("$MCPP" build --target "$pinned" 2>&1 || true)" -# ⭐⭐ CLASSIFY FROM THE MACHINE INTERFACE, ASSERT THE WORDING FROM THE MESSAGE. +# CLASSIFY FROM THE MACHINE INTERFACE, ASSERT THE WORDING FROM THE MESSAGE. # `data.reason` is a finite token and survives any rewording of the sentence; # the sentence is still checked below, because naming the target, the convention # and the replacement is a promise no code can keep on its own. @@ -112,7 +112,7 @@ case "$reason" in convention-unreplaced) echo " ok overriding a convention with nothing in its place is refused" ;; none) - # ⚠️ TWO OUTCOMES SHARE THIS BRANCH AND ONLY ONE IS A REGRESSION. If the + # TWO OUTCOMES SHARE THIS BRANCH AND ONLY ONE IS A REGRESSION. If the # build also succeeded, an llvm payload now supplies this target's C library # and the refusal has outlived its reason — the table changed and this test # is the thing that says so. If it failed, the old behaviour is back: the @@ -130,7 +130,7 @@ case "$reason" in exit 0 ;; esac -# ⭐ AND IT POINTS AT THE WAY OUT. A refusal that names no replacement leaves +# AND IT POINTS AT THE WAY OUT. A refusal that names no replacement leaves # the reader exactly where `crtbeginT.o (bare name)` left them. if [ "$fail" = 0 ]; then ok=1 @@ -148,7 +148,7 @@ fi # ── Half two: the same declaration, with a graph that supplies the C library ── # -# ⚠️ THE DEPENDENCY IS THE ONLY DIFFERENCE. Same compiler, same target, same +# THE DEPENDENCY IS THE ONLY DIFFERENCE. Same compiler, same target, same # source. printf '[package]\nname = "convprobe"\nversion = "0.1.0"\n\n[dependencies]\nopenkal-llvm-runtime = "0.1.3"\n\n[toolchain]\ndefault = "llvm@%s"\n' \ "$llvmver" > mcpp.toml @@ -156,7 +156,7 @@ printf 'import std;\nint main() { std::println("ok"); }\n' > src/main.cpp rm -rf target graph="$("$MCPP" build --target "$pinned" --verbose 2>&1 || true)" -# ⭐⭐ THE CLAIM IS THAT THE GUARD STOOD ASIDE, NOT THAT openkal COMPILES ON THIS +# THE CLAIM IS THAT THE GUARD STOOD ASIDE, NOT THAT openkal COMPILES ON THIS # ARCHITECTURE — AND THE FIRST VERSION ASSERTED THE SECOND. # # It required `Finished`, so a dependency failing for its own reasons turned @@ -186,7 +186,7 @@ if [ "$graphReason" = convention-unreplaced ]; then elif [ "$graphCabi" = graph ]; then echo " ok the same declaration is honoured when the graph supplies the C library" else - # ⚠️ NOT A PASS, AND NOT A SKIP. "Not refused" on its own is satisfied by a + # NOT A PASS, AND NOT A SKIP. "Not refused" on its own is satisfied by a # dependency that never resolved, so the second condition is what makes the # first one mean anything. echo "FAIL: c-abi came from '$graphCabi', not the graph — half two proves nothing" diff --git a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh index e04963e5..1d000937 100755 --- a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh +++ b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh @@ -2,14 +2,14 @@ # requires: unix-shell jq # A target request that declines to name a C library resolves to a SUPPORTED row. # -# ⭐⭐ `parse` FILLS THE ENV SEGMENT LEXICALLY, AND THE TIER GATE USED TO ASK +# `parse` FILLS THE ENV SEGMENT LEXICALLY, AND THE TIER GATE USED TO ASK # ABOUT THE FILLED VALUE RATHER THAN ABOUT THE REQUEST. # # The fill exists so the IDENTITY stays total: `x86_64-linux` IS # `x86_64-linux-gnu`, that is the output directory and the cache key, and a unit # test pins it. What it is not is an answer to "does mcpp support this". # -# ⚠️ MEASURED ON 2026.8.26.1, same machine, same graph, two spellings: +# MEASURED ON 2026.8.26.1, same machine, same graph, two spellings: # # $ mcpp build --target aarch64-linux # error: target 'aarch64-linux-gnu' is registered but not yet supported @@ -21,7 +21,7 @@ # `examples/06-openkal-cross` teaches the short spelling for three platforms; # the fourth was the one that could not be written. # -# ⭐⭐ BOTH DIRECTIONS, BECAUSE "SEND EVERY BARE -linux TO musl" ALSO FIXES +# BOTH DIRECTIONS, BECAUSE "SEND EVERY BARE -linux TO musl" ALSO FIXES # aarch64 AND WOULD BREAK EVERY PROJECT ON THE PLANET. Half two is the control: # `x86_64-linux` must still be gnu, because gnu is a supported row there. set -e @@ -34,7 +34,7 @@ cd "$work" printf '[package]\nname = "reqprobe"\nversion = "0.1.0"\n' > mcpp.toml printf 'extern "C" int main(int, char**, char**) { return 0; }\n' > src/main.cpp -# ⭐ CLASSIFICATION FROM THE MACHINE INTERFACE. `data.reason` is a finite token; +# CLASSIFICATION FROM THE MACHINE INTERFACE. `data.reason` is a finite token; # a substring search over prose stops asserting the moment the prose is reworded # — measured in this repo, twice, in one session. reason_for() { @@ -42,7 +42,7 @@ reason_for() { | jq -r '.data.reason // "-"' | tr -d '\r' } -# ⚠️⚠️ THE CLAIM IS ABOUT COMPLETION, NOT ABOUT SERVABILITY, AND ONLY ONE OF +# THE CLAIM IS ABOUT COMPLETION, NOT ABOUT SERVABILITY, AND ONLY ONE OF # THOSE IS THE SAME ON EVERY BUILD HOST. # # `aarch64-linux-musl` pins the musl-gcc payload. macOS has no gcc payload at @@ -54,7 +54,7 @@ reason_for() { # visible either way: on success in `data.triple`, and under a refusal in the # first line of the message, which names its subject. # -# ⚠️ THE SUBJECT, NOT THE DOCUMENT. Grepping the whole JSON was the first draft +# THE SUBJECT, NOT THE DOCUMENT. Grepping the whole JSON was the first draft # and it is contaminated: `host-cannot-serve` lists every target this host CAN # serve, so `x86_64-linux-musl` appears in a message that is about something # else entirely. The list is an answer to a different question sitting in the @@ -91,7 +91,7 @@ case "$r" in exit 1 ;; esac -# ⚠️ AND THE ROW IS THE IDENTITY, NOT THE SPELLING. The output directory is the +# AND THE ROW IS THE IDENTITY, NOT THE SPELLING. The output directory is the # identity. Asserting only "it did not refuse with tier-planned" would stay # green in a world where the completion picked some other row entirely. case "$row" in diff --git a/tests/e2e/300_a_registered_family_is_not_reported_unknown.sh b/tests/e2e/300_a_registered_family_is_not_reported_unknown.sh index b815a028..2e207b19 100755 --- a/tests/e2e/300_a_registered_family_is_not_reported_unknown.sh +++ b/tests/e2e/300_a_registered_family_is_not_reported_unknown.sh @@ -3,7 +3,7 @@ # "unknown target" is a claim about the vocabulary, and it was false for a whole # arch+os family. # -# ⚠️ MEASURED ON 2026.8.26.1: +# MEASURED ON 2026.8.26.1: # # $ mcpp why toolchain --target riscv64-linux --format json # unknown target 'riscv64-linux' @@ -15,7 +15,7 @@ # the refusal carried no code at all: `other` is what the machine interface # prints for a branch nobody named, and this branch had a perfectly good name. # -# ⭐ TWO ASSERTIONS, BECAUSE THE WORD AND THE CODE FAIL SEPARATELY. A message +# TWO ASSERTIONS, BECAUSE THE WORD AND THE CODE FAIL SEPARATELY. A message # fixed without a code still reports `other`; a code added without fixing the # message still tells the reader their target does not exist. set -e @@ -42,7 +42,7 @@ if [ "$reason" != tier-planned ]; then fi echo " ok a registered family refuses as planned, not as unknown" -# ⭐ AND THE MESSAGE NAMES A ROW THAT EXISTS. Telling someone their target is +# AND THE MESSAGE NAMES A ROW THAT EXISTS. Telling someone their target is # planned is only actionable once they can see which spelling is the registered # one — the same rule that made `x86_64-linux` have to work before the # name/fact warning was worth printing. @@ -54,7 +54,7 @@ else exit 1 fi -# ⚠️ AND IT DOES NOT QUOTE A TRIPLE THE READER NEVER TYPED. The old message was +# AND IT DOES NOT QUOTE A TRIPLE THE READER NEVER TYPED. The old message was # about `riscv64-linux-gnu`, a string that appears nowhere in the command and # nowhere in the vocabulary. if printf '%s\n' "$msg" | grep -q 'riscv64-linux-gnu'; then diff --git a/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh b/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh index a4984bea..6285b30c 100755 --- a/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh +++ b/tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh @@ -3,12 +3,12 @@ # A compiler the dependency graph requires is USED, not merely checked — and # selecting it writes no configuration. # -# ⭐⭐ `provides` AND `requires` ARE TWO HALVES OF ONE VOCABULARY AND THEY WERE +# `provides` AND `requires` ARE TWO HALVES OF ONE VOCABULARY AND THEY WERE # READ AT OPPOSITE ENDS OF `prepare_build`. The block that decides the toolchain # once the graph exists consulted the first; the second was collected a thousand # lines later and used only to reject the outcome. # -# ⚠️ MEASURED ON 2026.8.26.1, three-line manifest, llvm already installed: +# MEASURED ON 2026.8.26.1, three-line manifest, llvm already installed: # # $ mcpp build # global default gcc@16.1.0 # error: `openkal-llvm-runtime@0.1.3` requires the compiler to be `llvm`. @@ -19,20 +19,20 @@ # Nothing was missing. The remedy printed was a GLOBAL change — the default for # every project on the machine — made because ONE project's dependency asked. # -# ⭐ AND THE SECOND HALF IS THE POINT OF THE FIRST. `resolve_target_toolchain` +# AND THE SECOND HALF IS THE POINT OF THE FIRST. `resolve_target_toolchain` # has two call sites and both are downstream of the graph, so every # `write_default_toolchain` lives on a branch this selection no longer enters. # "Touch no configuration" is not a rule someone has to remember; it is where # the decision sits. The sha256 is what enforces it. # -# ⚠️ THE CRITERION IS A HASH, NOT "THE BUILD SUCCEEDED". A build that succeeds +# THE CRITERION IS A HASH, NOT "THE BUILD SUCCEEDED". A build that succeeds # and rewrites the user's default is exactly the behaviour being removed, and # the two are indistinguishable from the exit code. set -e MCPP="${MCPP:-mcpp}" -# ⭐ THE REQUIRED FAMILY IS CHOSEN AGAINST THIS MACHINE, NOT HARDCODED. The +# THE REQUIRED FAMILY IS CHOSEN AGAINST THIS MACHINE, NOT HARDCODED. The # claim is "a requirement that differs from mcpp's own answer is applied", so # the test needs a family that (a) is installed here and (b) is not the one # already resolving. Hardcoding `llvm` would silently assert nothing on a box @@ -47,7 +47,7 @@ printf 'extern "C" int main(int, char**, char**) { return 0; }\n' > "$work/app/s printf 'export module needs_it;\nexport int needs_it() { return 0; }\n' \ > "$work/needs/src/needs_it.cppm" -# ⚠️⚠️ THE BASELINE IS MEASURED IN THE PROJECT UNDER TEST, AND MEASURING IT +# THE BASELINE IS MEASURED IN THE PROJECT UNDER TEST, AND MEASURING IT # ANYWHERE ELSE MAKES THIS FILE ASSERT NOTHING. # # The first draft asked `why toolchain` from whatever directory the runner @@ -81,7 +81,7 @@ printf '[package]\nname = "needs-%s"\nversion = "0.1.0"\nrequires = ["mcpp: printf '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nneeds-%s = { path = "../needs" }\n' \ "$want" > "$work/app/mcpp.toml" -# ⚠️⚠️ A HASHER THAT DOES NOT EXIST MAKES THIS CRITERION PASS BY MEASURING +# A HASHER THAT DOES NOT EXIST MAKES THIS CRITERION PASS BY MEASURING # NOTHING. macOS has no `sha256sum` (it is `shasum -a 256`), and the first draft # would then have compared two empty strings — which are equal. The whole point # of this half is that a build can succeed AND rewrite the configuration; a @@ -121,7 +121,7 @@ echo " ok the graph required '$want' and mcpp took it (was '$current')" # ── And it changed no configuration ────────────────────────────────────── after="" [ -f "$cfg" ] && after="$(hash_of "$cfg")" -# ⚠️ AND BOTH SIDES MUST BE NON-EMPTY. `"" = ""` is the shape this half exists +# AND BOTH SIDES MUST BE NON-EMPTY. `"" = ""` is the shape this half exists # to refuse, whatever produced the emptiness. if [ -z "$before" ] || [ -z "$after" ]; then echo "FAIL: one side of the comparison is empty (before='$before' after='$after')," @@ -139,12 +139,12 @@ fi # ── And the query says who asked ───────────────────────────────────────── # -# ⚠️ A COMPILER THE USER DID NOT NAME, REPORTED WITHOUT ITS REASON, IS A RULE +# A COMPILER THE USER DID NOT NAME, REPORTED WITHOUT ITS REASON, IS A RULE # THAT CAN ONLY BE LEARNED BY EXPERIMENT. The same argument that put a reason on # the target row's substitution applies here, and more so: this one overrides a # value the user set with `mcpp toolchain default`. # -# ⭐ FROM `compiler.chosenBy`, NOT FROM THE PROSE. A consumer asking "why this +# FROM `compiler.chosenBy`, NOT FROM THE PROSE. A consumer asking "why this # compiler" reading the status line would be doing the substring matching the # machine interface exists to remove — and so would this test. by="$(printf '%s' "$j" | jq -r '.data.compiler.chosenBy.requiredBy // ""' | tr -d '\r')" @@ -163,7 +163,7 @@ else exit 1 fi -# ⭐ AND THE HUMAN-FACING LINE STILL CARRIES IT. The token is for programs; the +# AND THE HUMAN-FACING LINE STILL CARRIES IT. The token is for programs; the # person running the build reads the status line, and a decision reported there # without its reason is the rule learned by experiment. out="$("$MCPP" build 2>&1 || true)" diff --git a/tests/e2e/302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh b/tests/e2e/302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh index b2e4073b..e03184e8 100755 --- a/tests/e2e/302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh +++ b/tests/e2e/302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh @@ -3,7 +3,7 @@ # The graph's compiler requirement is applied where mcpp's own answer was # revisable — and only there. # -# ⭐⭐ THE RANK IS NOT NEW. `TcOrigin` already sorted these and +# THE RANK IS NOT NEW. `TcOrigin` already sorted these and # `tc_origin_is_user_explicit` already answered "may mcpp revise this"; the # defect the previous test covers was that nobody asked. This file is the other # side: the one case that must still refuse, and the one that cannot be resolved @@ -12,7 +12,7 @@ # half one `[toolchain] default = ` the project stated it → refuse # half two two packages, two families no compiler satisfies both # -# ⚠️ AND THE ADVICE IS PART OF THE CLAIM. Until 2026.8.26.2 the first remedy +# AND THE ADVICE IS PART OF THE CLAIM. Until 2026.8.26.2 the first remedy # offered was `mcpp toolchain default ` — global, and in the only case # that still reaches here it does not even work, because a project-level # statement is what decided. A remedy that cannot fix the failure it is printed @@ -54,7 +54,7 @@ if [ -n "$gccspec" ] && [ -n "$llvmspec" ]; then fi echo " ok a compiler the project stated is not revised" - # ⭐ AND THE REMEDY POINTS AT THE STATEMENT THAT DECIDED. + # AND THE REMEDY POINTS AT THE STATEMENT THAT DECIDED. ok=1 printf '%s\n' "$msg" | grep -q '\[toolchain\]' || ok=0 printf '%s\n' "$msg" | grep -q 'mcpp.toml' || ok=0 @@ -66,7 +66,7 @@ if [ -n "$gccspec" ] && [ -n "$llvmspec" ]; then exit 1 fi - # ⚠️ AND IT DOES NOT SEND THE READER TO A GLOBAL SETTING. Changing + # AND IT DOES NOT SEND THE READER TO A GLOBAL SETTING. Changing # `mcpp toolchain default` here fixes nothing: the project's own statement # is what is being used. if printf '%s\n' "$msg" | grep -q 'mcpp toolchain default'; then @@ -81,7 +81,7 @@ fi # ── Half two: two packages, two families ───────────────────────────────── # -# ⚠️ ONE SUPPLIER PER LAYER, AND TWO IS AN ERROR RATHER THAN A PICK — the rule +# ONE SUPPLIER PER LAYER, AND TWO IS AN ERROR RATHER THAN A PICK — the rule # `provides` already follows. Resolving by graph-traversal order would make the # answer depend on an order the author neither writes nor can predict, and would # silently satisfy one package while failing the other inside a header. @@ -111,7 +111,7 @@ fi # ── Half three: a capability row's remedy is not a convention row's ────── # -# ⚠️⚠️ THE TWO ROWS REFUSE UNDER ONE RULE AND FOR TWO REASONS, AND ONE REMEDY +# THE TWO ROWS REFUSE UNDER ONE RULE AND FOR TWO REASONS, AND ONE REMEDY # DOES NOT SERVE BOTH. # # A convention pin is cancelled by a graph that supplies the target's system, so @@ -120,10 +120,10 @@ fi # emits the target. Printed there, that remedy is an instruction the sentence # directly above it has already ruled out. # -# ⭐ FOUND BY READING THE MESSAGE, NOT BY A FAILING BUILD. This half exists so +# FOUND BY READING THE MESSAGE, NOT BY A FAILING BUILD. This half exists so # the next rewording cannot put it back. # -# ⚠️ The refusal is decided from the VOCABULARY (the row's pin) before any +# The refusal is decided from the VOCABULARY (the row's pin) before any # payload is resolved, so this half is host-independent and needs nothing # installed. printf '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nneeds-gcc = { path = "../needs-gcc" }\n' \ diff --git a/tests/e2e/303_the_query_gives_one_answer_for_the_c_library.sh b/tests/e2e/303_the_query_gives_one_answer_for_the_c_library.sh index 4e986df9..561b50a8 100755 --- a/tests/e2e/303_the_query_gives_one_answer_for_the_c_library.sh +++ b/tests/e2e/303_the_query_gives_one_answer_for_the_c_library.sh @@ -2,7 +2,7 @@ # requires: unix-shell jq # `mcpp why toolchain --format json` answers "which C library" once. # -# ⚠️⚠️ IT USED TO ANSWER TWICE AND DIFFERENTLY, IN ONE DOCUMENT. +# IT USED TO ANSWER TWICE AND DIFFERENTLY, IN ONE DOCUMENT. # # Measured on 2026.8.26.1 over an openkal project: # @@ -16,7 +16,7 @@ # `layers[].c-abi` describes the BUILD. A consumer had no way to tell which one # governed, which is a machine interface contradicting itself. # -# ⭐ ONE FIELD ADDED, NONE CHANGED. docs/11 §6 promises that fields are added +# ONE FIELD ADDED, NONE CHANGED. docs/11 §6 promises that fields are added # and never removed and that a field's meaning never changes; renaming # `cLibrary` or widening `mode` would break that for a document whose whole # point is to be depended on. @@ -37,7 +37,7 @@ probe() { # → " " # ── Half one: the field exists at all ──────────────────────────────────── # -# ⚠️ A `null` HERE IS NOT A FAILING ASSERTION, IT IS AN ABSENT ONE. Without +# A `null` HERE IS NOT A FAILING ASSERTION, IT IS AN ABSENT ONE. Without # this check the comparisons below would read "null" on both sides and agree. printf '[package]\nname = "clibprobe"\nversion = "0.1.0"\n' > mcpp.toml base="$("$MCPP" why toolchain --format json 2>/dev/null | tr -d '\r')" @@ -47,7 +47,7 @@ if [ "$basereason" != none ]; then printf '%s' "$base" | jq -r '.diagnostics[].message' | sed 's/^/ /' exit 1 fi -# ⚠️⚠️ `has`, NOT `// "MISSING"`. jq's `//` returns its right side when the left +# `has`, NOT `// "MISSING"`. jq's `//` returns its right side when the left # is null OR FALSE — so `"suppliesTarget": false`, which is a perfectly good # answer, read as absent. Measured on windows-x86_64: the field was there and # this check reported it missing. @@ -62,7 +62,7 @@ fi # ── Half two: the control — with no graph, the payload is not denied ───── # -# ⚠️ THE CLAIM IS SELF-CONSISTENCY, NOT A PARTICULAR VALUE. `suppliesTarget` is +# THE CLAIM IS SELF-CONSISTENCY, NOT A PARTICULAR VALUE. `suppliesTarget` is # also false when there is no payload C-library model at all (`mode: none`), # which is a legitimate state on some hosts. What must never happen is # `suppliesTarget: false` while the c-abi layer says the payload supplied it. @@ -83,7 +83,7 @@ echo " ok with no dependency graph the two answers agree (mode '$mode', c-abi # ── Half three: with a graph, it says so ───────────────────────────────── # -# ⭐ THE ROW IS BUILT FROM A LOCAL PACKAGE, NOT FROM THE INDEX. A test whose +# THE ROW IS BUILT FROM A LOCAL PACKAGE, NOT FROM THE INDEX. A test whose # subject is "the document agrees with itself" must not also depend on a # network fetch: the skip that produces is indistinguishable from a pass. mkdir -p "$work/libc/src" @@ -91,26 +91,26 @@ printf '[package]\nname = "fake-libc"\nversion = "0.1.0"\nprovides = ["mcpp > "$work/libc/mcpp.toml" printf 'export module fake_libc;\nexport int fl() { return 0; }\n' \ > "$work/libc/src/fake_libc.cppm" -# ⚠️ `libc`, NOT `../libc`. The root package IS `$work`, so `../libc` points +# `libc`, NOT `../libc`. The root package IS `$work`, so `../libc` points # outside the fixture — measured, the dependency did not resolve, `data.layers` # came back null, and the skip below reported "no contradiction to check". printf '[package]\nname = "clibprobe"\nversion = "0.1.0"\n\n[dependencies]\nfake-libc = { path = "libc" }\n' \ > mcpp.toml -# ⚠️⚠️ A REFUSAL MUST NOT BE READ AS "NOTHING TO CHECK". The first draft went +# A REFUSAL MUST NOT BE READ AS "NOTHING TO CHECK". The first draft went # straight to the skip when `origin2` was empty — and empty is what a FAILED # query produces, not only an inapplicable one. A criterion whose "no" and whose # "could not measure" print the same line asserts nothing. doc2="$("$MCPP" why toolchain --format json 2>/dev/null | tr -d '\r')" reason2="$(printf '%s' "$doc2" | jq -r '.data.reason // "QUERY-FAILED"')" if [ "$reason2" != none ]; then - # ⚠️ NAMED, NOT SILENT. A musl C library over this host's own target is not + # NAMED, NOT SILENT. A musl C library over this host's own target is not # a combination every host can stack — on an MSVC-ABI host the layering # check answers first, and that is a different question correctly answered. # Printing the reason is what keeps this distinguishable from a defect; # a bare `exit 0` here would make the file's conclusion unreachable and # unremarked. - # ⚠️⚠️ AND IT DOES NOT PRINT THE CONCLUSION LINE. The skip means the central + # AND IT DOES NOT PRINT THE CONCLUSION LINE. The skip means the central # claim was NOT checked here; emitting `OK:` anyway would make CI's # "did it reach its conclusion" step accept a file that asserted two thirds # of itself. The skip is granted by REASON in the workflow, and diff --git a/tests/e2e/304_the_target_side_reaches_every_unit.sh b/tests/e2e/304_the_target_side_reaches_every_unit.sh index de1b95f3..e62290d6 100755 --- a/tests/e2e/304_the_target_side_reaches_every_unit.sh +++ b/tests/e2e/304_the_target_side_reaches_every_unit.sh @@ -3,7 +3,7 @@ # A layer the GRAPH supplies is beneath every unit, including a sibling # dependency package that has never heard of it. # -# ⭐⭐ THE SET WAS ALREADY COMPUTED AND REACHED ONE TRANSLATION UNIT. +# THE SET WAS ALREADY COMPUTED AND REACHED ONE TRANSLATION UNIT. # # A package that supplies a target-side layer publishes the headers the whole # target is built against. Those travelled as an ordinary `publicUsage`, which @@ -21,11 +21,11 @@ # # mcpp#514 §A. # -# ⚠️ NO openkal, NO CROSS, NO NETWORK. A path package declaring one capability +# NO openkal, NO CROSS, NO NETWORK. A path package declaring one capability # and one `include_dirs` entry reproduces it, which is the point: this is not a # property of openkal but of how a target side was modelled. # -# ⭐ THE CRITERION IS THE CDB ROW, NOT A GREP OVER THE TREE. `compile_commands.json` +# THE CRITERION IS THE CDB ROW, NOT A GREP OVER THE TREE. `compile_commands.json` # says what each unit is actually compiled with; a grep over `build.ninja` would # also match the global rule and pass for the wrong reason. set -e @@ -40,7 +40,7 @@ mkdir -p src abi/src abi/abi-include abi/abi-internal dep/src dep/dep-include # The provider: supplies a target-side layer, publishes one directory, and # keeps one to itself. # -# ⭐⭐ THE TWO KEYS OF THIS RELEASE MEET IN THIS ONE PACKAGE, AND THAT IS THE +# THE TWO KEYS OF THIS RELEASE MEET IN THIS ONE PACKAGE, AND THAT IS THE # REAL-WORLD SHAPE. `openkal-musl` declares `provides = ["mcpp:c-abi=musl"]` # AND carries an internal header overlay. A layer's directories reach EVERY # unit, so publishing the overlay by mistake would put musl's `hidden`/`weak` @@ -101,7 +101,7 @@ EOF [ -s compile_commands.json ] || { echo "FAIL: no compile_commands.json"; exit 1; } -# ⚠️ A DENOMINATOR. A CDB with no sibling row would make every assertion below +# A DENOMINATOR. A CDB with no sibling row would make every assertion below # vacuously true, which is the false green this criterion has to rule out. siblings="$(jq -r '[.[] | select(.file | test("/dep/src/"))] | length' compile_commands.json)" if [ "$siblings" -lt 1 ]; then @@ -140,7 +140,7 @@ if [ -n "$leaked" ]; then exit 1 fi -# ⭐⭐ AND THE LAYER'S *PRIVATE* DIRECTORY REACHES NOBODY BUT ITSELF. +# AND THE LAYER'S *PRIVATE* DIRECTORY REACHES NOBODY BUT ITSELF. # # A layer reaches every unit, so publishing an internal overlay by mistake is # wider here than anywhere else: it would land on every translation unit of the diff --git a/tests/e2e/305_built_from_is_not_published.sh b/tests/e2e/305_built_from_is_not_published.sh index 427e5cbe..d0373d50 100755 --- a/tests/e2e/305_built_from_is_not_published.sh +++ b/tests/e2e/305_built_from_is_not_published.sh @@ -2,7 +2,7 @@ # requires: unix-shell jq # A package can say which of its include directories stop at its own boundary. # -# ⭐⭐ `publicUsage` TOOK `privateBuild`'s DIRECTORIES ENTIRE, so a package was +# `publicUsage` TOOK `privateBuild`'s DIRECTORIES ENTIRE, so a package was # built from exactly the set it published. For almost every package those are # the same set; for one that vendors a library with an internal header overlay # they are not, and the difference reaches every consumer. @@ -19,7 +19,7 @@ # Publishing that directory hands those macros to every consumer; which # consumer breaks on which name was discovered one at a time. # -# ⭐ THE CRITERION IS THE DIRECTORY, NOT THE SYMPTOM. Asserting that `hidden` +# THE CRITERION IS THE DIRECTORY, NOT THE SYMPTOM. Asserting that `hidden` # no longer collides would go green again the moment the package patched that # one macro, while the leak stayed. This asserts that the directory is not on # the consumer's command line — and, separately, that a consumer using the name @@ -39,12 +39,12 @@ name = "vendored" version = "0.1.0" [build] -# ⚠️ ORDER IS LOAD-BEARING and is why `private_include_dirs` is a SUBSET of +# ORDER IS LOAD-BEARING and is why `private_include_dirs` is a SUBSET of # this list rather than a second list: the internal overlay must precede the # public headers for the package's OWN build, and two TOML arrays cannot # express one order. # -# ⭐ `gen/*` IS A GLOB ON BOTH LINES. The filter is applied AFTER expansion, so +# `gen/*` IS A GLOB ON BOTH LINES. The filter is applied AFTER expansion, so # the glob withholds exactly the directories it expands to. Comparing the # unexpanded spellings would be the obvious implementation and would publish # every one of them, because `gen/*` is not literally equal to `gen/one`. @@ -86,7 +86,7 @@ fi [ -s compile_commands.json ] || { echo "FAIL: no compile_commands.json"; exit 1; } -# ⚠️ DENOMINATORS ON BOTH SIDES. With no provider row or no consumer row the +# DENOMINATORS ON BOTH SIDES. With no provider row or no consumer row the # assertions below are vacuously true. prov="$(jq -r '[.[] | select(.file | test("/lib/src/"))] | length' compile_commands.json)" cons="$(jq -r '[.[] | select(.file | test("/src/main"))] | length' compile_commands.json)" @@ -114,7 +114,7 @@ fail=0 [ "$(has_dir '/src/main' '/lib/internal')" = false ] || { echo "FAIL: the private directory leaked to the consumer"; fail=1; } -# ⭐ AND THE GLOB WITHHOLDS WHAT IT EXPANDS TO — both directories, by name. +# AND THE GLOB WITHHOLDS WHAT IT EXPANDS TO — both directories, by name. # Checking `gen` alone would pass for an implementation that matched the # unexpanded spelling and published `gen/one` and `gen/two` anyway. for g in one two; do diff --git a/tests/e2e/306_dependency_linkage_axis.sh b/tests/e2e/306_dependency_linkage_axis.sh index e7627e15..23f18aa8 100755 --- a/tests/e2e/306_dependency_linkage_axis.sh +++ b/tests/e2e/306_dependency_linkage_axis.sh @@ -59,7 +59,7 @@ cd app # ── 1. the default is the old behaviour ──────────────────────────────────── "$MCPP" build > static.log 2>&1 || { cat static.log; exit 1; } -# ⚠️ The FINGERPRINT directory, `target///`, not the triple one. +# The FINGERPRINT directory, `target///`, not the triple one. # `ls target/*/ -d` yields the triple, and comparing a fingerprint against it # in step 3 would pass no matter what — a criterion aimed at the wrong object # does not report an error, it quietly answers a different question. @@ -124,7 +124,7 @@ done # ── 4. the bundle a shared build produces must actually start ────────────── # -# ⚠️ A `.so` that is built and then not shipped is the worst outcome of this +# A `.so` that is built and then not shipped is the worst outcome of this # axis: everything is green locally, the archive uploads, and it fails on the # user's machine with `error while loading shared libraries`. The closure is # resolved by RUNNING the artifact, and the library is found through `$ORIGIN` diff --git a/tests/e2e/307_symbol_provision.sh b/tests/e2e/307_symbol_provision.sh index ccdec3fc..c6267891 100755 --- a/tests/e2e/307_symbol_provision.sh +++ b/tests/e2e/307_symbol_provision.sh @@ -2,7 +2,7 @@ # requires: elf # 307_symbol_provision.sh — one library, one provider (issue #519). # -# ⚠️ THE SILENT CASE IS THE IMPORTANT ONE, and it is why this test has two +# THE SILENT CASE IS THE IMPORTANT ONE, and it is why this test has two # halves that differ by a single package. # # mcpp's own `kind = "shared"` mechanism produces the exact shape the @@ -156,7 +156,7 @@ EOF "$MCPP" build > conflict.log 2>&1 || { cat conflict.log; exit 1; } -# ⚠️ `target//` ACCUMULATES one directory per fingerprint, and adding a +# `target//` ACCUMULATES one directory per fingerprint, and adding a # dependency changed the fingerprint — so half 1's directory is still there # with its own `app` and its own clean verdict. Taking "the first" of either # would answer about the build that is not under test. Everything below is diff --git a/tests/e2e/309_host_module_identity.sh b/tests/e2e/309_host_module_identity.sh index 59c588f9..6640d159 100755 --- a/tests/e2e/309_host_module_identity.sh +++ b/tests/e2e/309_host_module_identity.sh @@ -80,7 +80,7 @@ out="$("$MCPP" run 2>&1 | grep '^DIVERGENT=' | tail -1)" [[ "$out" == "DIVERGENT=1" ]] || { echo "FAIL: the divergently-named rule's directive did not land: $out"; exit 1; } -# ⚠️ THE IMPORT ABOVE DOES NOT DISCRIMINATE ON GCC, AND THAT IS THE WHOLE +# THE IMPORT ABOVE DOES NOT DISCRIMINATE ON GCC, AND THAT IS THE WHOLE # POINT OF THIS FEATURE. Measured: with the pre-I1 code — which registered the # PACKAGE name — this build still succeeded under GCC, because GCC's BMIs are # implicit under gcm.cache and keyed by the name the SOURCE declares, so diff --git a/tests/e2e/311_rule_build_dependencies.sh b/tests/e2e/311_rule_build_dependencies.sh index e8056138..03fb4b77 100755 --- a/tests/e2e/311_rule_build_dependencies.sh +++ b/tests/e2e/311_rule_build_dependencies.sh @@ -143,7 +143,7 @@ out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" # moves; the refusal is reached only because it is asked BEFORE the cache is # consulted. # -# ⚠️ This assertion does not prove that placement on its own. Measured: with +# This assertion does not prove that placement on its own. Measured: with # the check below the fast path it still fired here, because `ctxHash` happens # to change as well. The placement is what makes the property hold; this pins # the behaviour, not the reason. diff --git a/tests/e2e/312_build_rules_example.sh b/tests/e2e/312_build_rules_example.sh index 82a3818a..8b9e4975 100755 --- a/tests/e2e/312_build_rules_example.sh +++ b/tests/e2e/312_build_rules_example.sh @@ -65,7 +65,7 @@ out="$("$MCPP" run 2>&1 | tail -1)" # and answers with an exit code, which is what clang-tidy does. mcpp creates # the stamp when the command succeeds. # -# ⚠️ ASSERT ON THE STAMP AND ON THE RE-RUN, NOT ON THE BUILD SUCCEEDING. +# ASSERT ON THE STAMP AND ON THE RE-RUN, NOT ON THE BUILD SUCCEEDING. # Measured with the wrapper removed: ninja does NOT fail when a declared output # goes unproduced. It leaves the file missing and re-runs that edge on every # build, forever — so the build stays green, this script's earlier assertions diff --git a/tests/e2e/313_check_stamp_on_every_platform.sh b/tests/e2e/313_check_stamp_on_every_platform.sh index 2e0a08c4..654e06f1 100755 --- a/tests/e2e/313_check_stamp_on_every_platform.sh +++ b/tests/e2e/313_check_stamp_on_every_platform.sh @@ -2,7 +2,7 @@ # 313_check_stamp_on_every_platform.sh — a `role = "check"` whose command never # touches its stamp, on EVERY platform. # -# ⚠️ WHY THIS EXISTS SEPARATELY FROM 188 AND 312. +# WHY THIS EXISTS SEPARATELY FROM 188 AND 312. # # The engine writes a check's stamp so that an analyser -- which answers with an # exit code and writes nothing -- can be a check without a wrapper script. The @@ -67,7 +67,7 @@ out="$("$MCPP" run 2>&1 | tail -1)" # declared output goes unproduced -- it leaves the file absent and re-runs that # edge on every build afterwards, so a build with no stamp mechanism at all # stays green and merely redoes work. -# ⚠️ NOT `mapfile`. macOS ships bash 3.2, which does not have it, and this file +# NOT `mapfile`. macOS ships bash 3.2, which does not have it, and this file # runs there — `47_cdb_prebuilt_module_path_abs.sh` already says so in its own # comment, and every other user of `mapfile` in this suite declares # `# requires: gcc`, which macOS never satisfies. So the constraint was written diff --git a/tests/e2e/314_dependency_action_generated_header.sh b/tests/e2e/314_dependency_action_generated_header.sh index 146eb743..30a84eec 100755 --- a/tests/e2e/314_dependency_action_generated_header.sh +++ b/tests/e2e/314_dependency_action_generated_header.sh @@ -12,7 +12,7 @@ # are deliberately excluded), not in the goal phony (objects and link outputs # only), and consumed by no edge. # -# ⚠️ THE ASSERTION IS ON CONTENT, NOT EXISTENCE. `prepare_actions` used to +# THE ASSERTION IS ON CONTENT, NOT EXISTENCE. `prepare_actions` used to # write a zero-byte placeholder for every Source output including headers, so # the file was on disk whether or not the generator ran — which is exactly why # mcpp#534 was filed as an intermittent race when it was a deterministic diff --git a/tests/e2e/315_blocking_check_gates_compilation.sh b/tests/e2e/315_blocking_check_gates_compilation.sh index 158145a8..be91eb69 100755 --- a/tests/e2e/315_blocking_check_gates_compilation.sh +++ b/tests/e2e/315_blocking_check_gates_compilation.sh @@ -12,7 +12,7 @@ # marked blocking ran alongside compilation exactly like a non-blocking one, # and the flag was a no-op with a paper trail. # -# ⚠️ ASSERTED ON THE ARTIFACT, NOT ON A LOG LINE. "the check failed" appears in +# ASSERTED ON THE ARTIFACT, NOT ON A LOG LINE. "the check failed" appears in # the output either way; what distinguishes blocking from non-blocking is # whether the object was PRODUCED. A grep over stderr would pass against the # defect. diff --git a/tests/e2e/316_link_unit_with_no_inputs_is_refused.sh b/tests/e2e/316_link_unit_with_no_inputs_is_refused.sh index 6bb1f484..a7cb25ef 100755 --- a/tests/e2e/316_link_unit_with_no_inputs_is_refused.sh +++ b/tests/e2e/316_link_unit_with_no_inputs_is_refused.sh @@ -15,7 +15,7 @@ # package with no sources has neither, so the variable expanded to nothing and # the shell was handed `-shared` as a program name.) # -# ⚠️ THE STATIC CASE IS THE ONE THAT MATTERED. `ar rcs libfoo.a` with no +# THE STATIC CASE IS THE ONE THAT MATTERED. `ar rcs libfoo.a` with no # members exits 0 and writes an 8-byte archive, so before this the build # REPORTED SUCCESS and every consumer failed later with undefined symbols. Both # kinds are asserted here, and the static one is why the check is at plan time @@ -50,7 +50,7 @@ EOF grep -q "$2" b.log || { echo "FAIL($1): the error does not say it is a $2:"; cat b.log; exit 1; } - # ⭐ THE NEGATIVE HALF. This is the assertion that catches a regression back + # THE NEGATIVE HALF. This is the assertion that catches a regression back # to the reported symptom: if the refusal is ever removed, the build reaches # the linker again and this is what comes out. if grep -q -- '-shared: not found' b.log; then diff --git a/tests/e2e/327_declared_xlings_deps_report_their_own_failure.sh b/tests/e2e/327_declared_xlings_deps_report_their_own_failure.sh index aa70f8bc..25edb92d 100755 --- a/tests/e2e/327_declared_xlings_deps_report_their_own_failure.sh +++ b/tests/e2e/327_declared_xlings_deps_report_their_own_failure.sh @@ -47,7 +47,7 @@ echo 'int main() { return 0; }' > proj/src/main.cpp cd proj # ── (1) MCPP_NO_AUTO_INSTALL: refuse, and name THAT knob ──────────────────── -# ⚠️ The knob that fired, not a fixed one. Telling someone who exported +# The knob that fired, not a fixed one. Telling someone who exported # MCPP_NO_AUTO_INSTALL to drop --offline sends them looking for a variable they # never set; the toolchain gate solved this years ago and this path is copying # its shape, so the test holds the shape too. @@ -77,7 +77,7 @@ echo " ok MCPP_OFFLINE refuses and names the offline knob" # ── (3) A declared package that cannot be provisioned fails the build ─────── # …and, the half a single run cannot show, is not recorded as done. # -# ⚠️ THE DENOMINATOR IS TWO INVOCATIONS. The stamp is what made the original +# THE DENOMINATOR IS TWO INVOCATIONS. The stamp is what made the original # defect permanent, and a stamp written for a failure is invisible in the run # that wrote it — the build that mattered already reported success. Only the # SECOND build distinguishes "failed" from "failed and remembered as done". @@ -111,7 +111,7 @@ echo " ok a failed provisioning fails the build, twice — it is not stamped a # with auto-install off, leg (1) above would refuse a build that worked # yesterday, for packages that are in fact installed. # -# ⚠️ THE LEGACY STAMP IS EVIDENCE ONLY HERE. The release that wrote it did not +# THE LEGACY STAMP IS EVIDENCE ONLY HERE. The release that wrote it did not # read the provisioning result, so it means "attempted", not "succeeded" — # trusting it anywhere else would carry that defect across the upgrade that # fixes it. This leg pins the one place it is allowed to count. diff --git a/tests/e2e/328_layer_predicates_apply_and_unknown_ones_say_so.sh b/tests/e2e/328_layer_predicates_apply_and_unknown_ones_say_so.sh index 80b0655e..7a271874 100755 --- a/tests/e2e/328_layer_predicates_apply_and_unknown_ones_say_so.sh +++ b/tests/e2e/328_layer_predicates_apply_and_unknown_ones_say_so.sh @@ -30,7 +30,7 @@ trap "rm -rf $TMP" EXIT cd "$TMP" # The resolved c-abi for this build, read from mcpp's own report rather than -# assumed. ⚠️ The report prints layers whose origin is not the compiler payload +# assumed. The report prints layers whose origin is not the compiler payload # by default, so the zero-config case needs MCPP_VERBOSE to list all five. mkdir -p probe/src cat > probe/mcpp.toml <<'EOF' @@ -45,21 +45,21 @@ CABI=$(sed -n 's/.*c-abi *\([A-Za-z0-9_+-]*\).*/\1/p' probe/report.log | head - [ -n "$CABI" ] || { cat probe/report.log; echo "FAIL: could not read the resolved c-abi"; exit 1; } echo " .. this host resolves c-abi = '$CABI'" -# ⚠️ A NAME NO LAYER CAN TAKE, for the negative leg. Deriving it (rather than +# A NAME NO LAYER CAN TAKE, for the negative leg. Deriving it (rather than # hardcoding "musl") keeps the leg meaningful on a musl host, where a hardcoded # counter-example would be the TRUE case and the test would assert nothing. NOTCABI="not-${CABI}" # ── (1) + (2) a matching layer predicate applies; a non-matching one does not ── # -# ⚠️ THE TRIPLE HALF OF THE COMBINED PREDICATE IS any(unix, windows), NOT unix. +# THE TRIPLE HALF OF THE COMBINED PREDICATE IS any(unix, windows), NOT unix. # The point of that leg is that a triple key and a layer key COMBINE, so its # triple half has to be true everywhere this test runs. Written as bare unix it # was true on Linux and macOS and false on Windows, where all(...) then # correctly evaluated false and the fixture's #error fired. CI caught it; the # feature was right and the test was not. # -# ⚠️ AND THESE NOTES LIVE OUTSIDE THE HEREDOCS. The fixtures below interpolate +# AND THESE NOTES LIVE OUTSIDE THE HEREDOCS. The fixtures below interpolate # $CABI, so their heredocs are unquoted — which makes backticks in a comment # command substitution. The first version of this note sat inside one and the # suite printed `syntax error: unexpected end of file` while still passing. @@ -94,7 +94,7 @@ EOF cat layers/b.log; echo "FAIL: layer predicates did not behave"; exit 1; } echo " ok a matching layer predicate applies, a non-matching one does not" -# ⚠️ EXACTLY ONCE, NOT AT LEAST ONCE. The pass that evaluates layer predicates +# EXACTLY ONCE, NOT AT LEAST ONCE. The pass that evaluates layer predicates # runs AFTER the triple-only merge, and `append()` is additive — so a predicate # with a triple leg (`any(unix, c-abi = ...)`) would be matched by both passes # and contribute twice. Counting the flag is the only way to see that; a @@ -121,7 +121,7 @@ echo " ok a predicate naming both a triple key and a layer applies exactly onc # ── (2b) …and it reaches a DEPENDENCY, which is the motivating case ───────── # -# ⚠️ EVERY LEG ABOVE IS SATISFIED BY A PASS THAT ONLY PATCHES packages[0]. +# EVERY LEG ABOVE IS SATISFIED BY A PASS THAT ONLY PATCHES packages[0]. # docs/14 writes this feature for "a package supplying a layer [that] frequently # supports several implementations of the layer beneath it" — a LIBRARY, reached # as someone's dependency. The build.mcpp tail that shares this pass's window @@ -202,7 +202,7 @@ echo " ok --strict makes it an error" # pass runs, dependency resolution is over, so such a section would otherwise be # dropped in exactly the silence this whole test exists to end. # -# ⚠️ AND THE BUILD INPUTS UNDER THE SAME PREDICATE MUST STILL APPLY. A warning +# AND THE BUILD INPUTS UNDER THE SAME PREDICATE MUST STILL APPLY. A warning # that quietly disabled the rest of the section would trade one silent drop for # another. mkdir -p depcond/src diff --git a/tests/e2e/329_exit_codes_match_the_contract.sh b/tests/e2e/329_exit_codes_match_the_contract.sh index 2bffcc9c..8e86f3ab 100755 --- a/tests/e2e/329_exit_codes_match_the_contract.sh +++ b/tests/e2e/329_exit_codes_match_the_contract.sh @@ -11,7 +11,7 @@ # # docs/spec/exit-codes.md is now the contract. This holds it. # -# ⚠️ ASSERTS THE CODE, NOT "STDERR IS NON-EMPTY". A criterion whose "no" is also +# ASSERTS THE CODE, NOT "STDERR IS NON-EMPTY". A criterion whose "no" is also # what a crash produces cannot tell the two apart — and the whole point of the # table is that a client can distinguish a rejection from a broken mcpp. # diff --git a/tests/e2e/332_cortex_m_builds_and_boots.sh b/tests/e2e/332_cortex_m_builds_and_boots.sh index 8671debf..1f81d41c 100755 --- a/tests/e2e/332_cortex_m_builds_and_boots.sh +++ b/tests/e2e/332_cortex_m_builds_and_boots.sh @@ -2,7 +2,7 @@ # requires: llvm unix-shell qemu-arm # Cortex-M: the M-profile rows build, boot, and collect what nothing calls. # -# ⚠️ THE ROWS THAT MATTER HERE ARE THE ONES A REASONED TABLE WOULD HAVE GOT +# THE ROWS THAT MATTER HERE ARE THE ONES A REASONED TABLE WOULD HAVE GOT # WRONG. Two properties are asserted that a build alone cannot show: # # * `--gc-sections` reaches a bare-metal link. Without it a C library that @@ -19,7 +19,7 @@ # a clean link. The row carries `-mfpu=none` for this, and the assertion is # an instruction count. # -# ⚠️ AND THE IMAGE IS RUN, NOT INSPECTED. A freestanding image that links is not +# AND THE IMAGE IS RUN, NOT INSPECTED. A freestanding image that links is not # evidence: the entry point and the ordering of the vector table are only # exercised by a machine that fetches from address zero. set -e @@ -58,7 +58,7 @@ name = "mcu" version = "0.1.0" TOML -# ⚠️ `volatile` on the operands. Without it the multiply is constant-folded and +# `volatile` on the operands. Without it the multiply is constant-folded and # the FPU assertion below passes for a reason unrelated to the flag. cat > src/main.cpp <<'CPP' namespace { @@ -128,7 +128,7 @@ boot_row() { # triple machine cpuflag flash_org flash_len ram_org r fi local out rc - # ⚠️⚠️ NOT `qemu | head`, AND THE EXIT STATUS IS A SECOND ASSERTION. + # NOT `qemu | head`, AND THE EXIT STATUS IS A SECOND ASSERTION. # # `$?` after a pipeline is the LAST command's status, so piping into `head` # would read head's 0 and the check below would be vacuous. It is not a @@ -159,18 +159,18 @@ boot_row thumbv7m-none-eabi mps2-an385 "-cpu cortex-m3" 0x00000000 4M 0 boot_row thumbv7em-none-eabihf mps2-an386 "-cpu cortex-m4" 0x00000000 4M 0x20000000 4M boot_row thumbv8m.main-none-eabi mps2-an505 "" 0x10000000 4M 0x38000000 512K -# ⚠️ A COUNT, BECAUSE A LOOP THAT RAN ZERO TIMES ALSO REACHES THIS LINE. +# A COUNT, BECAUSE A LOOP THAT RAN ZERO TIMES ALSO REACHES THIS LINE. [ "$ran" = "4" ] || { echo "FAIL: expected 4 rows to boot, got $ran"; exit 1; } # ── The float ABI, asserted on both sides of the pair ────────────────────── # -# ⚠️ A SEPARATE PROJECT, AND THAT SEPARATION IS ITSELF A MEASUREMENT. Float +# A SEPARATE PROJECT, AND THAT SEPARATION IS ITSELF A MEASUREMENT. Float # arithmetic cannot live in the fixture above: on a soft-float row it lowers # onto `__aeabi_fmul`, and this tier has no C library and no builtins to resolve # it against, so every boot row would fail to link. The boot fixture is # therefore integer-only and the float question is asked here. # -# ⚠️ AND ONE SIDE ALONE PROVES NOTHING. "The soft row emitted no FPU +# AND ONE SIDE ALONE PROVES NOTHING. "The soft row emitted no FPU # instruction" is equally true of a build that emitted no float code, so the # hard row is measured from the same source as the control. mkdir -p "$work/fp/src" @@ -196,7 +196,7 @@ hard=$("$OBJDUMP" -d "$(find target -type f -name fp | head -1)" \ echo "FAIL: thumbv7em-none-eabihf emitted no FPU instruction — the control is vacuous"; exit 1; } echo " ok thumbv7em-none-eabihf uses the FPU ($hard instructions)" -# ⭐ The soft row's proof is its LINK FAILURE, and that is the strongest form +# The soft row's proof is its LINK FAILURE, and that is the strongest form # available at this tier. `-mfpu=none` makes the compiler lower the multiply # onto `__aeabi_fmul` rather than `vmul.f32`; with no C library and no builtins # there is nothing for that call to resolve against. A link that fails naming diff --git a/tests/e2e/333_named_runners_locked_and_sbom.sh b/tests/e2e/333_named_runners_locked_and_sbom.sh index 18b590db..2b658805 100755 --- a/tests/e2e/333_named_runners_locked_and_sbom.sh +++ b/tests/e2e/333_named_runners_locked_and_sbom.sh @@ -4,7 +4,7 @@ # artefact reaches a device, whether the build is reproducible, and what went # into it. # -# ⚠️ NONE OF THESE NEEDS A DEVICE, AND THAT IS DELIBERATE. `flash`, `monitor` +# NONE OF THESE NEEDS A DEVICE, AND THAT IS DELIBERATE. `flash`, `monitor` # and `debug` perform an argv the board supplied; what this script asserts is # that mcpp resolves the right slot, reports an override, and refuses clearly # when nothing is declared. Standing in a shell script for the tool means the @@ -42,7 +42,7 @@ out=$("$MCPP" run --runner monitor 2>&1) || { echo "FAIL: run --runner monitor e case "$out" in *MONITORED*) ;; *) echo "FAIL: monitor did not perform its own slot"; exit 1 ;; esac echo " ok run --runner monitor performs its own entry, not flash's" -# ⚠️ THE SLOT THAT IS NOT DECLARED MUST BE REFUSED BY NAME. An engine that fell +# THE SLOT THAT IS NOT DECLARED MUST BE REFUSED BY NAME. An engine that fell # back to executing the artefact would "succeed" at flashing by running the # program on the build host, which is the failure the slot exists to prevent. if out=$("$MCPP" run --runner debug 2>&1); then @@ -80,7 +80,7 @@ test -f mcpp.lock || { echo "FAIL: no mcpp.lock after a build with a dependency" || { echo "FAIL: --locked rejected a matching lock"; exit 1; } echo " ok --locked passes when the resolution matches" -# ⚠️⚠️ AND THE FAILING DIRECTION IS THE ONE THAT MATTERS. Measured while writing +# AND THE FAILING DIRECTION IS THE ONE THAT MATTERS. Measured while writing # this: with the fast path still enabled, a corrupted lock passed `--locked` and # printed "Finished" — the flag was accepted and the check never ran. sed -i.bak 's/version = "0.0.1"/version = "9.9.9"/' mcpp.lock @@ -112,7 +112,7 @@ root = d["metadata"]["component"] assert root["name"] == "q", root names = [c["name"] for c in d["components"]] assert "cmdline" in names, names -# ⚠️ A component with no licence must SAY so rather than omit the field: an +# A component with no licence must SAY so rather than omit the field: an # absent key reads as "not examined" and is the shape a reviewer cannot filter. for c in d["components"]: assert "licenses" in c, c["name"] @@ -122,7 +122,7 @@ for c in d["components"]: print(" ok sbom is valid CycloneDX, names every component and its licence field") PY -# ⭐ AND IT REPORTS WHAT WAS RECORDED, NOT WHAT WOULD RESOLVE NOW. This is the +# AND IT REPORTS WHAT WAS RECORDED, NOT WHAT WOULD RESOLVE NOW. This is the # one property an SBOM must have, so it is asserted rather than assumed. sed -i.bak 's/version = "0.0.1"/version = "7.7.7"/' mcpp.lock "$MCPP" emit sbom -o sbom2.json >/dev/null 2>&1 || { echo "FAIL: mcpp emit sbom (2)"; exit 1; } @@ -137,12 +137,12 @@ mv mcpp.lock.bak mcpp.lock # ── B: one board package, two environments, chosen by the consumer ───────── # -# ⭐⭐ THE EMULATOR/HARDWARE AXIS IS A FEATURE, NOT A FORK. A board reached +# THE EMULATOR/HARDWARE AXIS IS A FEATURE, NOT A FORK. A board reached # through QEMU and the same board reached through a debug probe differ only in # the argv of their device slots. Publishing two packages would duplicate the # linker script, the startup code and the module surface to vary four strings. # -# ⚠️ AND THE SITE THIS CATCHES IS A REAL ONE. Dependency-supplied RunGlobal +# AND THE SITE THIS CATCHES IS A REAL ONE. Dependency-supplied RunGlobal # entries reach the root through a DIFFERENT code path from a package's own; # wiring only the latter left `mcpp flash` reporting "no flash is configured" # while `mcpp run` found the runner the same build program emitted beside it. @@ -193,7 +193,7 @@ echo " ok plain run uses the dependency's default runner" consumer_manifest ', features = ["hardware"]' rm -rf target -# ⭐⭐ THE 80% CASE: THE COMMAND DOES NOT CHANGE. On hardware "run" means +# THE 80% CASE: THE COMMAND DOES NOT CHANGE. On hardware "run" means # flash-and-go, so the feature redefines the DEFAULT runner rather than adding # a named one. A design requiring `--runner flash` here would have made the # most common action the one needing an extra argument. diff --git a/tests/e2e/334_dependency_declared_tool_is_found.sh b/tests/e2e/334_dependency_declared_tool_is_found.sh index 87a8f518..37ff097e 100755 --- a/tests/e2e/334_dependency_declared_tool_is_found.sh +++ b/tests/e2e/334_dependency_declared_tool_is_found.sh @@ -2,7 +2,7 @@ # requires: gcc unix-shell # A runner may name its program by bare name when a DEPENDENCY declared it. # -# ⚠️⚠️ THE CASE THIS COVERS IS THE ONE THE FEATURE EXISTS FOR, AND IT WAS THE +# THE CASE THIS COVERS IS THE ONE THE FEATURE EXISTS FOR, AND IT WAS THE # ONE THAT DID NOT WORK. # # `mcpp.build.runner_lookup` lets a runner name a program without writing a @@ -68,7 +68,7 @@ cat > src/main.cpp <<'CPP' int main() { return 0; } CPP -# ⚠️ THE TOOL IS ON PATH HERE ONLY VIA THE STAND-IN PAYLOAD'S bin/, WHICH IS +# THE TOOL IS ON PATH HERE ONLY VIA THE STAND-IN PAYLOAD'S bin/, WHICH IS # WHAT MAKES THE ASSERTION MEAN SOMETHING. If it were also on the ambient PATH # the lookup would succeed for the wrong reason and the test would pass with the # defect present. @@ -97,7 +97,7 @@ case "$out" in esac echo " ok a dependency's bare-name runner is resolved, and its absence is named" -# ⭐ AND THE FALLBACK IS REFUSED RATHER THAN TAKEN. Executing the artifact when +# AND THE FALLBACK IS REFUSED RATHER THAN TAKEN. Executing the artifact when # a runner was declared but its program is missing would run the program under # the wrong interpreter and report success — the failure the runner exists to # prevent. diff --git a/tests/e2e/335_xlings_tool_tiers.sh b/tests/e2e/335_xlings_tool_tiers.sh index 561a73f4..64ca4be7 100755 --- a/tests/e2e/335_xlings_tool_tiers.sh +++ b/tests/e2e/335_xlings_tool_tiers.sh @@ -2,7 +2,7 @@ # requires: gcc unix-shell # `[xlings.workspace]` entries carry a tier, and a verb installs only its own. # -# ⭐⭐ THE ASSERTION IS ON WHAT mcpp ASKS FOR, NOT ON WHAT GOT INSTALLED. +# THE ASSERTION IS ON WHAT mcpp ASKS FOR, NOT ON WHAT GOT INSTALLED. # # Verifying the tier by installing would need a clean machine and a network, # and neither is available here. It would also be the wrong object: what the @@ -10,7 +10,7 @@ # single download — `MCPP_NO_AUTO_INSTALL=1` refuses to provision and names # exactly what it would have provisioned. # -# ⚠️ AND THE CRITERION CARRIES ITS OWN DENOMINATOR. "The run-tier tool is +# AND THE CRITERION CARRIES ITS OWN DENOMINATOR. "The run-tier tool is # absent from `mcpp build`" is satisfied by an mcpp that provisions nothing at # all, or by a manifest whose entry never parsed. The pair of commands is the # test: the same project, the same package, present under `run` and absent diff --git a/tests/e2e/336_armv7a_builds_and_boots.sh b/tests/e2e/336_armv7a_builds_and_boots.sh index 21ef05e4..9542e2da 100755 --- a/tests/e2e/336_armv7a_builds_and_boots.sh +++ b/tests/e2e/336_armv7a_builds_and_boots.sh @@ -2,13 +2,13 @@ # requires: llvm unix-shell qemu-arm # ARMv7-A: the first 32-bit row in the table with a memory management unit. # -# ⚠️ THE ROW IS NOT A SECOND SPELLING OF THE M ROWS, AND THIS TEST IS WHERE THAT +# THE ROW IS NOT A SECOND SPELLING OF THE M ROWS, AND THIS TEST IS WHERE THAT # BECOMES CHECKABLE. Every other 32-bit target here is M-profile — an MPU that # describes regions by base and limit, with no page-table entry at all — so it # is the one machine class on which an address-space abstraction has never been # asked what a 32-bit entry looks like. # -# ⚠️ AND THE SEMIHOSTING EXIT CALL IS SPELLED DIFFERENTLY FROM M-PROFILE. +# AND THE SEMIHOSTING EXIT CALL IS SPELLED DIFFERENTLY FROM M-PROFILE. # `SYS_EXIT` (0x18) on AArch32 takes the reason code in `r1` DIRECTLY; the # `{reason, code}` block a Cortex-M board passes is `SYS_EXIT_EXTENDED` (0x20). # Measured: passing the block to 0x18 prints correctly and then reports the @@ -103,7 +103,7 @@ boot_row() { echo "FAIL: $triple kept a function nothing calls (--gc-sections not applied)"; exit 1 fi local out rc - # ⚠️ NOT `qemu | head`, AND THAT IS THE WHOLE POINT OF THE STATUS CHECK. + # NOT `qemu | head`, AND THAT IS THE WHOLE POINT OF THE STATUS CHECK. # `$?` after a pipeline is the LAST command's status, so piping into `head` # would read head's 0 and the exit assertion below would be vacuous — this # repository has shipped that shape before. The output goes to a file and @@ -118,7 +118,7 @@ boot_row() { *"armv7a ok"*) ;; *) echo "FAIL: $triple did not boot; got: $out"; exit 1 ;; esac - # ⭐ THE EXIT STATUS, NOT ONLY THE OUTPUT. A program that prints and then + # THE EXIT STATUS, NOT ONLY THE OUTPUT. A program that prints and then # gets its exit call wrong is exactly what this row's semihosting note is # about, and only the status tells the two apart. [ "$rc" = "0" ] || { echo "FAIL: $triple booted but exited $rc"; exit 1; } @@ -132,7 +132,7 @@ boot_row armv7a-none-eabihf # ── The float ABI, both sides, on this architecture ──────────────────────── # -# ⚠️ MEASURED HERE RATHER THAN CARRIED OVER FROM M-PROFILE. `armv7-a` is a +# MEASURED HERE RATHER THAN CARRIED OVER FROM M-PROFILE. `armv7-a` is a # different architecture from `thumbv7em`; that the soft ABI still reaches the # FPU there says nothing about here. It does — measured — and the row carries # `-mfpu=none` for it. diff --git a/tests/e2e/337_run_takes_features_and_profile.sh b/tests/e2e/337_run_takes_features_and_profile.sh index ba2869f4..4ff15970 100755 --- a/tests/e2e/337_run_takes_features_and_profile.sh +++ b/tests/e2e/337_run_takes_features_and_profile.sh @@ -2,14 +2,14 @@ # requires: gcc unix-shell # `mcpp run` takes `--features` and `--profile`, the axes `build` and `test` do. # -# ⚠️⚠️ WITHOUT THEM `run` COULD ONLY EXECUTE WHATEVER A PREVIOUS `build` LEFT +# WITHOUT THEM `run` COULD ONLY EXECUTE WHATEVER A PREVIOUS `build` LEFT # BEHIND. There was no spelling of `mcpp run` that ran a release artefact, or # one built with a feature on — and a board-support package expresses its two # environments (an emulator, a debug probe) AS features, so # `mcpp run --features hardware` is precisely the command the device surface was # designed around. It did not exist. # -# ⚠️ AND THE FAST PATH HAD TO LEARN ABOUT THEM. It reuses the cached artefact, +# AND THE FAST PATH HAD TO LEARN ABOUT THEM. It reuses the cached artefact, # which was built under the previous feature set and profile; taking it here # would accept the flag and ignore it, which is worse than refusing it. set -e @@ -55,7 +55,7 @@ plain="$("$MCPP" run 2>&1)" want "$plain" "quiet" "a plain run should not have the feature" want "$plain" "dev" "a plain run should be the dev profile" -# ⚠️ THE SECOND RUN IS THE ONE THAT MATTERS: the first populated the build +# THE SECOND RUN IS THE ONE THAT MATTERS: the first populated the build # cache, so a fast path that ignored --features would now answer "quiet". loud="$("$MCPP" run --features loud 2>&1)" want "$loud" "LOUD" "--features was accepted and ignored (the fast path took a stale entry)" @@ -76,7 +76,7 @@ want "$again" "dev" "a plain run inherited the previous --release" # ── And the same defect on `mcpp build`, which is where it came from ─────── # -# ⚠️⚠️ THIS WAS PRE-EXISTING AND IS THE REASON THE RUN SIDE WAS BROKEN. The +# THIS WAS PRE-EXISTING AND IS THE REASON THE RUN SIDE WAS BROKEN. The # build cache entry is keyed on (target, profile, cache mode) while the OUTPUT # DIRECTORY is keyed on a fingerprint that includes the features. So an entry # written by `mcpp build --features loud` pointed at the loud directory, and the @@ -85,7 +85,7 @@ want "$again" "dev" "a plain run inherited the previous --release" # # Measured before the fix: three builds of one project printed # `quiet`, `LOUD`, `LOUD`. -# ⚠️⚠️ THE ASSERTION IS ON WHAT THE CACHE ENTRY RECORDS, NOT ON WHICH FILE A +# THE ASSERTION IS ON WHAT THE CACHE ENTRY RECORDS, NOT ON WHICH FILE A # `find` HAPPENS TO RETURN FIRST. # # The first version of this block ran `find target -name featrun | head -1`. @@ -109,7 +109,7 @@ rm -rf target [ "$(mru_features)" = "loud" ] || { echo "FAIL: --features loud recorded '$(mru_features)', expected loud"; exit 1; } -# ⚠️ THE ONE THAT CAUGHT THE PRE-EXISTING DEFECT. Before the entry carried a +# THE ONE THAT CAUGHT THE PRE-EXISTING DEFECT. Before the entry carried a # feature set, this plain build matched the entry `--features loud` had written, # reported success in 0.00s and left the loud artefact in place. Measured: three # consecutive builds of one project printed `quiet`, `LOUD`, `LOUD`. diff --git a/tests/e2e/338_cortex_m_picolibc_sysroot.sh b/tests/e2e/338_cortex_m_picolibc_sysroot.sh index 7bd58213..13247bed 100755 --- a/tests/e2e/338_cortex_m_picolibc_sysroot.sh +++ b/tests/e2e/338_cortex_m_picolibc_sysroot.sh @@ -2,12 +2,12 @@ # requires: llvm unix-shell qemu-arm # A Cortex-M project opts into a C library, and gets the RIGHT multilib. # -# ⭐⭐ THE ZERO-LIBC TIER IS THE DEFAULT AND THIS IS THE OPT-IN. Every +# THE ZERO-LIBC TIER IS THE DEFAULT AND THIS IS THE OPT-IN. Every # `thumb*-none-eabi*` row carries an empty C-library column, so a project # targeting one begins with no libc unless it says otherwise. One line says # otherwise, and `libdir` is what makes that line find anything. # -# ⚠️⚠️ AND THE ASSERTION IS THE FLOAT ABI, NOT THAT IT LINKS. +# AND THE ASSERTION IS THE FLOAT ABI, NOT THAT IT LINKS. # # `libdir` names the sub-directory a multilib C library uses. On riscv # `/` separates every profile because `mabi` there IS the float @@ -44,10 +44,10 @@ llvm_tool() { } READELF="$(llvm_tool llvm-readelf)" || { echo "SKIP: llvm-readelf not found"; exit 0; } -# ⚠️ The payload has to be present. Installing it here rather than skipping +# The payload has to be present. Installing it here rather than skipping # would make the test about the install; skipping when it is absent keeps the # criterion about the ENGINE, which is what this suite tests. -# ⚠️ `xim-x-picolibc-arm` AND NOT `*-x-picolibc-arm`. The engine resolves the +# `xim-x-picolibc-arm` AND NOT `*-x-picolibc-arm`. The engine resolves the # name `xim:picolibc-arm`, so a copy installed under any other namespace — a # LOCAL index entry, say — satisfies a glob and not the engine. Measured: with # the loose pattern this test proceeded against a `local-x-` payload and failed @@ -83,12 +83,12 @@ main = "src/main.c" sysroot = "xim:picolibc-arm@1.8.12" TOML -# ⭐ LOCATION IS A TARGET FACT; SELECTION IS A BOARD FACT. The engine resolved +# LOCATION IS A TARGET FACT; SELECTION IS A BOARD FACT. The engine resolved # WHERE the C library is and which multilib profile applies. WHICH startup # object, WHICH linker script and WHICH libraries are decisions a board makes, # and this fixture stands in for a board package. # -# ⚠️ `target_libc_profile()` IS THE `libdir` COLUMN, AND WITHOUT IT NONE OF THIS +# `target_libc_profile()` IS THE `libdir` COLUMN, AND WITHOUT IT NONE OF THIS # CAN BE WRITTEN. It is empty when the column is, and the board is then reduced # to guessing a directory name — which is the thing that goes wrong silently. cat > build.mcpp <<'BUILD' @@ -129,14 +129,14 @@ C elf=$(find target -type f -name fw | head -1) [ -n "$elf" ] || { echo "FAIL: no artefact"; exit 1; } -# ⚠️⚠️ AN EMPTY IMAGE IS NOT A PASS, AND IT IS WHAT A MISSING BOARD SELECTION +# AN EMPTY IMAGE IS NOT A PASS, AND IT IS WHAT A MISSING BOARD SELECTION # PRODUCES. Measured while writing this: without the crt0 the link SUCCEEDED and # mcpp reported `Size fw text 0 data 0` — a well-formed ELF containing # nothing. Every check that only looked for "Finished" would have passed. size=$(wc -c < "$elf") [ "$size" -gt 4096 ] || { echo "FAIL: the artefact is $size bytes — an empty link"; exit 1; } -# ⭐ THE ASSERTION THAT CATCHES THE WRONG MULTILIB. A hard-float libc linked +# THE ASSERTION THAT CATCHES THE WRONG MULTILIB. A hard-float libc linked # into a soft-float image leaves `Tag_ABI_HardFP_use` in the attributes. hard=$("$READELF" -A "$elf" 2>/dev/null | grep -c 'ABI_HardFP_use' || true) [ "$hard" = "0" ] || { diff --git a/tests/e2e/33_multi_version_mangling.sh b/tests/e2e/33_multi_version_mangling.sh index f84495cb..a489a706 100755 --- a/tests/e2e/33_multi_version_mangling.sh +++ b/tests/e2e/33_multi_version_mangling.sh @@ -73,7 +73,7 @@ import mcpplibs.cmdline; // resolver rewrites this to the mangled secondary import std; export int libB_v() { return libB_impl_v(); } EOF -# ⭐ A PRIVATE HEADER BESIDE A SOURCE, WHICH IS WHAT STAGING USED TO DROP. +# A PRIVATE HEADER BESIDE A SOURCE, WHICH IS WHAT STAGING USED TO DROP. # # libB is the copy the resolver stages under `target/.mangled/`, and a quoted # include is resolved relative to the directory of the file holding it — so diff --git a/tests/e2e/603_version_floor.sh b/tests/e2e/603_version_floor.sh index 60dc9d4d..985942cc 100755 --- a/tests/e2e/603_version_floor.sh +++ b/tests/e2e/603_version_floor.sh @@ -7,7 +7,7 @@ # Measured on the development machine, a CUDA 13.3 build against a driver # serving 12.4 does exactly that. # -# ⚠️ Nothing here mentions a vendor. The engine reads a name, a relation and a +# Nothing here mentions a vendor. The engine reads a name, a relation and a # version; `cuda.driver` is data passing through. That is asserted directly in # the third case with a name no backend uses. set -e diff --git a/tests/e2e/605_probe_channel_and_accel_reach_the_build_program.sh b/tests/e2e/605_probe_channel_and_accel_reach_the_build_program.sh index 1270c197..ce5080f5 100755 --- a/tests/e2e/605_probe_channel_and_accel_reach_the_build_program.sh +++ b/tests/e2e/605_probe_channel_and_accel_reach_the_build_program.sh @@ -111,7 +111,7 @@ grep -q "WIDGET_ON is missing" none.log || { cat none.log; echo "FAIL: the build failed for another reason than the layer"; exit 1; } echo "PASS: --no-accel leaves the accelerator layer empty" -# ⚠️ AND THE VARIABLE, WHICH THIS SECTION CLAIMED AND DID NOT MEASURE. +# AND THE VARIABLE, WHICH THIS SECTION CLAIMED AND DID NOT MEASURE. # `accel_str` prints `(none)` for an empty set so an ABI tag reads as a # sentence, and handing that spelling on made `MCPP_ACCEL=(none)` reach every # build program that asked for no accelerator — including every project that diff --git a/tests/e2e/608_object_actions_reach_a_static_library.sh b/tests/e2e/608_object_actions_reach_a_static_library.sh index ff1483bb..bb9d383b 100755 --- a/tests/e2e/608_object_actions_reach_a_static_library.sh +++ b/tests/e2e/608_object_actions_reach_a_static_library.sh @@ -65,7 +65,7 @@ grep -q "produces no target" build.log && { lib=$(find target -name 'libarchived.a' | head -1) [ -n "$lib" ] || { echo "FAIL: no static library was produced"; exit 1; } -# ⭐ THE MEMBER LIST IS THE ASSERTION, not the exit status: an `ar` that was +# THE MEMBER LIST IS THE ASSERTION, not the exit status: an `ar` that was # handed nothing still writes a well-formed archive and reports success. members=$(ar t "$lib" 2>/dev/null | tr '\n' ' ') case "$members" in diff --git a/tests/e2e/609_shader_sources_are_device_sources.sh b/tests/e2e/609_shader_sources_are_device_sources.sh new file mode 100755 index 00000000..ffa04b85 --- /dev/null +++ b/tests/e2e/609_shader_sources_are_device_sources.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# requires: gcc +# A shader is a device source, and the classification table says so for every +# language a separate compiler consumes -- not only for NVIDIA's two. +# +# THE DEFECT THIS MEASURES. `SourceKind::Device` is documented as a graph role, +# "compiled by a device compiler mcpp does not drive", explicitly so that the +# table does not grow a row per vendor. The extension list nonetheless held +# `.cu` and `.hip` alone, so a shader in a constrained glob was refused: +# +# 'scale.comp' is listed in [build] sources, and mcpp has no role for the +# extension '.comp'. +# +# and the rule package that exists to compile it was told, in the same run, +# that there were no device sources -- a warning and an error contradicting +# each other about the same file. +# +# Four sections, and the last two are what make the first two a measurement +# rather than a demonstration. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +"$MCPP" new shaders > /dev/null; cd shaders +rm -f src/*.cppm +cat > src/main.cpp <<'EOF' +int main() { return 0; } +EOF +mkdir -p shaders + +# The build program prints what it was handed, delimited, so a section can +# assert on a whole list rather than on a substring of one name. +# MCPP_DEVICE_SOURCES is newline-separated and the advisory channel is one +# line per message, so the list is flattened before it is printed. Delimited, +# so an empty list is `device=[]` and not the absence of a line -- which is +# what section four asserts on. +cat > build.mcpp <<'EOF' +import std; +import mcpp; +int main() { + std::string flat(mcpp::device_sources()); + for (auto& c : flat) if (c == '\n') c = ' '; + mcpp::warning(("device=[" + flat + "]").c_str()); + return 0; +} +EOF + +write_manifest() { # $1 = accel line, $2 = sources line + cat > mcpp.toml < "shaders/s.$e"; done + +write_manifest 'accel = "widget9+{w1}"' '"src/*.cpp", { glob = "shaders/*", accel = "widget9+{w1}" }' +"$MCPP" build > all.log 2>&1 || { cat all.log; echo "FAIL: a project with shader sources failed to build"; exit 1; } +missing="" +for e in $exts; do + grep -q "shaders/s.$e" all.log || missing="$missing $e" +done +[ -z "$missing" ] || { cat all.log; echo "FAIL: not handed to the build program:$missing"; exit 1; } +echo "PASS: all 18 shader and kernel extensions reach the build program as device sources" + +# ── Two: and none of them is compiled ───────────────────────────────────── +# +# The complement of section one. A device source that ALSO went to the C++ +# compiler would satisfy section one and produce an object nothing can link. +"$MCPP" build -v > verbose.log 2>&1 +for e in $exts; do + if grep -q "s\.$e -o\|s\.$e\.o" verbose.log; then + cat verbose.log; echo "FAIL: s.$e was compiled"; exit 1 + fi +done +echo "PASS: no shader is offered to the C++ compiler" + +# ── Three: an extension that is NOT in the table is still refused ───────── +# +# Without this the first section would also pass on a table that classified +# every unknown extension as a device source, which is the opposite of what +# the table is for. +printf '// wgsl\n' > shaders/s.wgsl +write_manifest 'accel = "widget9+{w1}"' '"src/*.cpp", { glob = "shaders/s.wgsl", accel = "widget9+{w1}" }' +if "$MCPP" build > unknown.log 2>&1; then + cat unknown.log; echo "FAIL: an extension with no role was accepted"; exit 1 +fi +grep -q "no role for the extension" unknown.log || { + cat unknown.log; echo "FAIL: refusal does not say the extension has no role"; exit 1; } +grep -q "\.wgsl" unknown.log || { cat unknown.log; echo "FAIL: refusal does not name the extension"; exit 1; } +rm -f shaders/s.wgsl +echo "PASS: an extension the table does not name is still refused, by name" + +# ── Four: the default source globs did not widen ────────────────────────── +# +# The compatibility promise the whole change rests on. A package that vendors +# shaders it builds elsewhere must not start handing them to a build program +# on upgrade, so a project with NO `sources` entry must see none of them. +write_manifest '' '"src/*.cpp"' +mkdir -p src/shaders && cp shaders/s.comp src/shaders/ +rm -f mcpp.toml +cat > mcpp.toml <<'EOF' +[package] +name = "shaders" +version = "0.1.0" +[language] +standard = "c++23" + +[targets.shaders] +kind = "bin" +main = "src/main.cpp" +EOF +"$MCPP" build > default.log 2>&1 || { cat default.log; echo "FAIL: the default-glob build failed"; exit 1; } +grep -q "device=\[\]" default.log || { + cat default.log; echo "FAIL: a shader under src/ reached a build program with no sources entry"; exit 1; } +echo "PASS: the default source globs still exclude every device extension" + +echo "PASS: shader sources are device sources" diff --git a/tests/e2e/610_feature_controlled_rule_collection.sh b/tests/e2e/610_feature_controlled_rule_collection.sh new file mode 100755 index 00000000..55df8481 --- /dev/null +++ b/tests/e2e/610_feature_controlled_rule_collection.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# requires: gcc +# 610_feature_controlled_rule_collection.sh — one package, several rules, the +# set chosen by the consumer's feature request (mcpp 2026.9.5.3+). +# +# Before this, a host-module package contributed exactly one module: the unit +# at its lib root. A collection such as `mcpp:plugins` -- `mcpp.rules.cuda`, +# `mcpp.rules.spirv`, later `mcpp.tools.*` -- therefore needed one package per +# member, and a consumer could not say "these two" in the one place it says +# everything else about a dependency. Now every module INTERFACE unit among the +# package's resolved sources is a host module of its own, and `[features.] +# sources` is what puts a unit into that set. +# +# Pinned here: +# 1. a feature the consumer activates makes its unit importable, under the +# name the unit declares, and that unit may import the lib root; +# 2. a unit whose feature is NOT activated is not compiled and cannot be +# imported -- the module set is the feature set, not the file set; +# 3. two activated features give two modules; +# 4. a collection in the `mcpp` namespace draws no reserved-prefix warning, +# and the same collection under another namespace draws one per unit; +# 5. a package with neither a lib root nor a listed interface unit keeps the +# diagnostic it had. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +write_collection() { # $1 = namespace + rm -rf plugins + mkdir -p plugins/src plugins/rules + cat > plugins/mcpp.toml < plugins/src/plugins.cppm <<'EOT' +export module mcpp.plugins; +import std; +export namespace mcpp::plugins { +inline constexpr std::string_view version = "0.1.0"; +} +EOT + cat > plugins/rules/a.cppm <<'EOT' +export module mcpp.rules.a; +import std; +import mcpp; +import mcpp.plugins; // the lib root precedes every feature unit +export namespace mcpp::rules::a { +inline void apply() { + mcpp::cxxflag("-DRULE_A=1"); + // Unquoted on purpose: the token is stringized by the consumer, and a + // quoted value would have to survive two layers of shell quoting. + mcpp::cxxflag(std::format("-DPLUGINS_VERSION={}", mcpp::plugins::version).c_str()); +} +} +EOT + cat > plugins/rules/b.cppm <<'EOT' +export module mcpp.rules.b; +import std; +import mcpp; +export namespace mcpp::rules::b { +inline void apply() { mcpp::cxxflag("-DRULE_B=1"); } +} +EOT +} + +write_app() { # $1 = namespace, $2 = features (TOML list body), $3 = imports (space-separated module names) + rm -rf app + mkdir -p app/src + cat > app/mcpp.toml < app/build.mcpp + cat > app/src/main.cpp <<'EOT' +#include +#ifndef RULE_A +#define RULE_A 0 +#endif +#ifndef RULE_B +#define RULE_B 0 +#endif +#ifndef PLUGINS_VERSION +#define PLUGINS_VERSION none +#endif +#define STR_(x) #x +#define STR(x) STR_(x) +int main() { std::printf("A=%d B=%d V=%s\n", RULE_A, RULE_B, STR(PLUGINS_VERSION)); } +EOT +} + +# ── 1. one feature, one module, and the unit imports the lib root ─────────── +write_collection mcpp +write_app mcpp '"rules-a"' 'mcpp.rules.a' +cd app +"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: a feature-selected rule did not build"; exit 1; } +out="$("$MCPP" run 2>&1 | grep '^A=' | tail -1)" +[[ "$out" == 'A=1 B=0 V=0.1.0' ]] || { echo "FAIL: expected A=1 B=0 V=0.1.0, got: $out"; exit 1; } +echo "PASS: a feature the consumer activates makes its unit importable under its declared name" +if grep -q 'prefix is reserved for rules maintained by the mcpp project' b1.log; then + echo "FAIL: the mcpp namespace drew a reserved-prefix warning"; cat b1.log; exit 1; fi +echo "PASS: a collection in the mcpp namespace draws no reserved-prefix warning" +cd .. + +# ── 2. a unit whose feature is not activated is not importable ─────────────── +write_app mcpp '"rules-a"' 'mcpp.rules.b' +cd app +if "$MCPP" build > b2.log 2>&1; then + echo "FAIL: a rule whose feature is not active was importable"; cat b2.log; exit 1; fi +grep -q 'mcpp.rules.b' b2.log || { echo "FAIL: the refusal does not name the module"; cat b2.log; exit 1; } +echo "PASS: a unit whose feature is not activated is not compiled and cannot be imported" +cd .. + +# ── 3. two features, two modules ───────────────────────────────────────────── +write_app mcpp '"rules-a", "rules-b"' 'mcpp.rules.a mcpp.rules.b' +cd app +"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: two feature-selected rules did not build"; exit 1; } +out="$("$MCPP" run 2>&1 | grep '^A=' | tail -1)" +[[ "$out" == 'A=1 B=1 V=0.1.0' ]] || { echo "FAIL: expected A=1 B=1 V=0.1.0, got: $out"; exit 1; } +echo "PASS: two activated features give two modules" +cd .. + +# ── 4. the control: another namespace is warned about, once per unit ───────── +write_collection acme +write_app acme '"rules-a", "rules-b"' 'mcpp.rules.a mcpp.rules.b' +cd app +"$MCPP" build > b4.log 2>&1 || { cat b4.log; echo "FAIL: the acme collection did not build"; exit 1; } +n=$(grep -c 'prefix is reserved for rules maintained by the mcpp project' b4.log || true) +# three units -- the lib root `mcpp.plugins` and the two rules -- each claims the prefix +[[ "$n" -eq 3 ]] || { echo "FAIL: expected 3 reserved-prefix warnings (one per unit), got $n"; cat b4.log; exit 1; } +echo "PASS: the same collection under another namespace draws one warning per unit" +cd .. + +# ── 5. neither a lib root nor a listed unit: the diagnostic is unchanged ───── +rm -rf plugins app +mkdir -p plugins/rules app/src +cat > plugins/mcpp.toml <<'EOT' +[package] +name = "plugins" +version = "0.1.0" + +[build] +sources = ["rules/*.cppm"] + +[targets.plugins] +kind = "lib" +EOT +cat > plugins/rules/impl.cppm <<'EOT' +module nothing.exported; // an implementation unit is not an interface +EOT +cat > app/mcpp.toml <<'EOT' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +plugins = { path = "../plugins", host-module = true } +EOT +echo 'import mcpp; int main() {}' > app/build.mcpp +echo 'int main() {}' > app/src/main.cpp +cd app +if "$MCPP" build > b5.log 2>&1; then + echo "FAIL: a host-module package with no interface unit built"; cat b5.log; exit 1; fi +grep -q 'no interface unit at' b5.log || { echo "FAIL: the missing-lib-root diagnostic changed"; cat b5.log; exit 1; } +echo "PASS: a package with neither a lib root nor a listed interface unit keeps its diagnostic" + +echo "PASS: feature-controlled rule collection" diff --git a/tests/e2e/611_variant_switch_is_not_replayed.sh b/tests/e2e/611_variant_switch_is_not_replayed.sh new file mode 100755 index 00000000..4494109a --- /dev/null +++ b/tests/e2e/611_variant_switch_is_not_replayed.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# requires: gcc +# 611_variant_switch_is_not_replayed.sh — a plain `mcpp build` after +# `mcpp build --no-accel` builds the device variant again (mcpp 2026.9.5.3+). +# +# The device variant is part of the fingerprint, so the two builds land in +# different directories. The fast path, which runs before any plan exists, +# replayed whichever directory was built LAST: after `--no-accel`, a plain +# build reported "Finished in 0.00s" and `mcpp run` executed the CPU variant, +# while the manifest said the project is a device build. The graph now records +# whether an override chose it, and the fast paths decline such a graph. +# +# The fixture is the one e2e 605 uses: a cfg section keyed on `accelerator` +# supplies a define, so the running program reports which variant it is. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +"$MCPP" new probe > /dev/null; cd probe +rm -f src/*.cppm +cat > mcpp.toml <<'EOT' +[package] +name = "probe" +version = "0.1.0" +[language] +standard = "c++23" + +[build] +accel = "widget9+{w1}" + +[target.'cfg(accelerator = "widget")'.build] +defines = ["WIDGET_ON=1"] + +[targets.probe] +kind = "bin" +main = "src/main.cpp" +EOT +cat > src/main.cpp <<'EOT' +#include +#ifndef WIDGET_ON +#define WIDGET_ON 0 +#endif +int main() { std::printf("WIDGET=%d\n", WIDGET_ON); } +EOT + +variant() { "$MCPP" run "$@" 2>&1 | grep '^WIDGET=' | tail -1; } + +"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: the device build failed"; exit 1; } +[[ "$(variant)" == "WIDGET=1" ]] || { echo "FAIL: the first build is not the device variant"; exit 1; } + +"$MCPP" build --no-accel > b2.log 2>&1 || { cat b2.log; echo "FAIL: --no-accel failed"; exit 1; } +[[ "$(variant --no-accel)" == "WIDGET=0" ]] || { echo "FAIL: --no-accel did not produce the CPU variant"; exit 1; } + +# The measurement. Before the fix this printed "Finished dev in 0.00s" and the +# run below answered WIDGET=0. +"$MCPP" build > b3.log 2>&1 || { cat b3.log; echo "FAIL: the plain build after --no-accel failed"; exit 1; } +out="$(variant)" +[[ "$out" == "WIDGET=1" ]] || { + echo "FAIL: a plain build after --no-accel handed back the CPU variant: $out" + cat b3.log; exit 1; } +echo "PASS: a plain build after --no-accel builds and runs the device variant" + +# And the other direction still holds (it did before: the flag bypasses the +# fast path), so the two are symmetric. +"$MCPP" build --no-accel > b4.log 2>&1 +[[ "$(variant --no-accel)" == "WIDGET=0" ]] || { echo "FAIL: --no-accel after a plain build is not the CPU variant"; exit 1; } +echo "PASS: --no-accel after a plain build still selects the CPU variant" + +# The graph says which selection wrote it, in the file itself. +n_default=$(grep -l '^# mcpp:graph=normal;schedule=[a-z-]*;accel=default' target/*/*/build.ninja | wc -l) +n_override=$(grep -l '^# mcpp:graph=normal;schedule=[a-z-]*;accel=override' target/*/*/build.ninja | wc -l) +[[ "$n_default" -eq 1 && "$n_override" -eq 1 ]] || { + echo "FAIL: expected one default and one override graph, got default=$n_default override=$n_override" + head -3 target/*/*/build.ninja; exit 1; } +echo "PASS: each graph records the selection that wrote it" + +echo "PASS: a variant switch is not replayed" diff --git a/tests/e2e/88_xlings_environment.sh b/tests/e2e/88_xlings_environment.sh index 05e3f26d..53a4e7a2 100755 --- a/tests/e2e/88_xlings_environment.sh +++ b/tests/e2e/88_xlings_environment.sh @@ -21,7 +21,7 @@ version = "0.1.0" [xlings] # Host build-tools the project wants available (xlings "deps"). # -# ⚠️ THE VERSION HAS TO EXIST, AND UNTIL 2026.9.1.1 IT DID NOT HAVE TO. +# THE VERSION HAS TO EXIST, AND UNTIL 2026.9.1.1 IT DID NOT HAVE TO. # This read `make@4.4, cmake@3.28`; the index carries make 4.3 and cmake # 4.4.2/4.0.2, so neither has ever resolved. That was invisible because #531's # provisioning did not read its own result — every failure xlings reported was diff --git a/tests/e2e/_elf_tag.sh b/tests/e2e/_elf_tag.sh index 325cd9f9..509e62f8 100644 --- a/tests/e2e/_elf_tag.sh +++ b/tests/e2e/_elf_tag.sh @@ -9,7 +9,7 @@ # tag RPATH | RUNPATH | BOTH | NONE # paths ':'-joined contents of whichever tag is present # -# ⚠️ WHY A PARSER AND NOT `strings` +# WHY A PARSER AND NOT `strings` # # Removing DT_RPATH/DT_RUNPATH removes the ENTRY, not the string it pointed at: # `.dynstr` is tail-merged by the linker, so a shorter live string can begin diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index 9a6eb8f0..45bb22cc 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -20,7 +20,7 @@ fi echo "Using mcpp: $MCPP" $MCPP --version -# ⚠️ The suite takes ~20 minutes, which is long enough to be tempting to work +# The suite takes ~20 minutes, which is long enough to be tempting to work # through -- and rebuilding mcpp during it swaps the binary UNDER the run. # Later tests then measure a different engine from earlier ones, and the report # mixes the two without saying so. Measured twice in one session: three tests @@ -162,7 +162,7 @@ command -v python3 &>/dev/null && CAPS+=(python3) # jq: the JSON reader for the tests that consume mcpp's MACHINE interface # (`--format json`) rather than the tables it prints for people. # -# ⚠️ DECLARING IT IS PART OF ADDING IT. A `# requires:` token that no branch +# DECLARING IT IS PART OF ADDING IT. A `# requires:` token that no branch # here ever adds makes every test naming it skip for ever, silently — the # `65_*` block spent months in that state. Every GitHub-hosted runner ships jq, # so in CI this is always true and a skip there is a red (the matrix workflow @@ -247,7 +247,7 @@ done # Returns 0 (true) if the test should be skipped, prints reason. # Returns 1 (false) if all requirements are met. -# ⚠️ THERE IS DELIBERATELY NO "requires-hard" FORM. +# THERE IS DELIBERATELY NO "requires-hard" FORM. # # The obvious answer to "a test silently skipped on the runner that was # supposed to run it" is a token whose absence FAILS. It was implemented here, @@ -422,7 +422,7 @@ fi echo "E2E Summary: $PASS passed, $FAIL failed, $SKIP skipped" if [[ "$(_binary_stamp)" != "$MCPP_STAMP_AT_START" ]]; then echo - echo "⚠️ THE BINARY UNDER TEST CHANGED DURING THIS RUN." + echo " THE BINARY UNDER TEST CHANGED DURING THIS RUN." echo " $MCPP" echo " Earlier tests ran a different engine from later ones, so this" echo " summary describes no single build. Re-run without rebuilding." diff --git a/tests/matrix/compare.sh b/tests/matrix/compare.sh index 9595c928..1d7cadde 100755 --- a/tests/matrix/compare.sh +++ b/tests/matrix/compare.sh @@ -1,27 +1,27 @@ #!/usr/bin/env bash # tests/matrix/compare.sh # -# ⭐⭐ 期望表是仓库里的文件,改了行为就必须同时改它。支持矩阵因此不是一份会过期的 +# 期望表是仓库里的文件,改了行为就必须同时改它。支持矩阵因此不是一份会过期的 # 文档,而是一次测量与一份声明的比对。 # -# ⚠️⚠️ 「跳过」必须是期望表说的,不是运行时发现的。一格因为「今天这台机器没装某个 +# 「跳过」必须是期望表说的,不是运行时发现的。一格因为「今天这台机器没装某个 # 载荷」而跳过,与「这个组合本就不支持」是两回事 —— 前者会让矩阵在缺件的机器上悄悄 # 变绿,而那正是本仓库反复付出代价的那种假绿。 set -u measured="${1:?用法: compare.sh }" expected="${2:?}" host="${3:?}" -# ⚠️ MODE 也是参数,不只是键的一列。一次扫描只产出一种体系,而拿它去比整张表, +# MODE 也是参数,不只是键的一列。一次扫描只产出一种体系,而拿它去比整张表, # 另一种体系的每一行都会被报成「期望表说有而扫描没跑到」—— 一条真判据用错了论域, # 得到的是一整屏假红。 mode="${4:?}" -# ⚠️ 键含 `mode`。两种体系(payload / graph)跑的是同一组 (host, target, compiler), +# 键含 `mode`。两种体系(payload / graph)跑的是同一组 (host, target, compiler), # 少了它,后跑的一遍会把先跑的一遍在期望表里的那一行「解释掉」。 key() { awk -F'\t' -v h="$host" -v m="$mode" '$1==m && $2==h {print $1"\t"$2"\t"$3"\t"$4}' "$1" | sort; } val() { awk -F'\t' -v h="$host" -v m="$mode" '$1==m && $2==h {print $1"\t"$2"\t"$3"\t"$4"\t"$10"\t"$11}' "$1" | sort; } -# ⚠️ 先断言扫描真的跑了。一格没跑与全部通过,在退出码上没有区别。 +# 先断言扫描真的跑了。一格没跑与全部通过,在退出码上没有区别。 n_m=$(key "$measured" | wc -l) n_e=$(key "$expected" | wc -l) echo "measured $n_m cells, expected table has $n_e for host '$host' mode '$mode'" @@ -46,13 +46,13 @@ while IFS=$'\t' read -r md h t c st rs; do want=$(awk -F'\t' -v m="$md" -v h="$h" -v t="$t" -v c="$c" \ '$1==m && $2==h && $3==t && $4==c {print $10"\t"$11}' "$expected") if [ -z "$want" ]; then - # ⭐ 实测有而期望表没有 —— 支持面扩大了,这也要有人确认。 + # 实测有而期望表没有 —— 支持面扩大了,这也要有人确认。 echo "::error::[$md] $h $t $c → $st, and the expected table does not mention this cell" echo " 新增一行到 tests/matrix/expected.tsv,或说明为何不该出现" fail=1 continue fi - # ⭐ 理由也在判据里。一格从「因为能力 pin 而拒绝」变成「因为约定没被替换而 + # 理由也在判据里。一格从「因为能力 pin 而拒绝」变成「因为约定没被替换而 # 拒绝」,status 仍是 unsupported —— 而那是两条不同的规则,换了一条却不红, # 这张表就只在说「它没建出来」,不在说「为什么」。 wantSt="${want%%$'\t'*}"; wantRs="${want##*$'\t'}" diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index a6a62d72..36e0d96e 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -1,6 +1,6 @@ # tests/matrix/expected.tsv — 这个仓库声明的支持矩阵。 # -# ⭐⭐ 这是一份声明,不是一份记录。改了行为就必须同时改它,而 CI 比对的是 +# 这是一份声明,不是一份记录。改了行为就必须同时改它,而 CI 比对的是 # `tests/matrix/scan.sh` 的一次真实测量与这里的每一行。 # # 列:mode host target compiler compiler-triple c-lib c-abi c++-abi openkal status reason @@ -13,29 +13,29 @@ # unsupported / host-cannot-serve 本机没有载荷,图也没有供给这个系统 # unsupported / layer-requirement 某个包要求的层,解析没有给出 # -# ⚠️⚠️ 这里**没有** `mismatch`,而这是刻意的。写下 `mismatch` 就是把一个缺陷 +# 这里**没有** `mismatch`,而这是刻意的。写下 `mismatch` 就是把一个缺陷 # 声明成期望,矩阵会在它上面变绿。一格测出 `mismatch`,要么修 mcpp,要么 # 让 mcpp 在决定处给出一句带 reason 的拒绝 —— 没有第三条路。 # -# ⚠️ 只有 `mode host target compiler status reason` 六列参与比对。中间五列是 +# 只有 `mode host target compiler status reason` 六列参与比对。中间五列是 # 测量的记录,留在这里是为了让一行的变化可读:`c-abi` 从 payload 变成 graph # 会在 diff 里自己说话,而只有 status 的表说不出来。 # -# ⭐ reason 也是判据。一格从 `capability-pin` 变成 `convention-unreplaced`, +# reason 也是判据。一格从 `capability-pin` 变成 `convention-unreplaced`, # status 仍是 unsupported —— 那是两条不同的规则,换了一条却不红,这张表就 # 只在说「它没建出来」,不在说「为什么」。 # -# ⚠️⚠️ host 是 (os, arch),不是 os。mcpp 自己发布四份宿主二进制: +# host 是 (os, arch),不是 os。mcpp 自己发布四份宿主二进制: # linux-x86_64 / linux-aarch64 / macos-arm64 / windows-x86_64 # 两台 Linux 服务的行不一样 —— `x86_64-linux-gnu` 需要本机架构的 glibc 载荷, # 所以在 x86_64 上够得着、在 aarch64 上够不着。只写 `linux` 会让后跑的一台把先跑 # 的那台在这张表里的行「解释掉」,而覆盖的方向取决于谁后跑,不取决于谁对。 # # 全部四台:2026-08-26 由 `ci-target-matrix.yml` 实测(mcpp 2026.8.26.1)。 -# ⚠️ 每一行都来自它自己那台机器 —— 从别的宿主推断出来的一行,断言的是推断而不是 +# 每一行都来自它自己那台机器 —— 从别的宿主推断出来的一行,断言的是推断而不是 # 那台机器。 # -# ⚠️ 各台格数不同,而这是事实不是遗漏: +# 各台格数不同,而这是事实不是遗漏: # linux-x86_64 40 gcc + llvm × 12 目标(payload 24 / graph 16) # linux-aarch64 16 只有 musl-gcc —— llvm 在非 x86_64 Linux 上被显式延缓 # macos-arm64 20 只有 llvm × 12 目标(payload 12 / graph 8) diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index 771d9592..0e58aad4 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # tests/matrix/scan.sh — 把「这台机器支持哪些目标」变成一次测量的输出。 # -# ⭐⭐ 两个入口,各回答自己那半: +# 两个入口,各回答自己那半: # # mcpp why toolchain --target T --toolchain C --format json # 这一格**会解析成什么**,以及如果不解析,是因为哪一条规则。结构化, @@ -10,21 +10,21 @@ # mcpp build --target T # 解析通过之后**它到底建不建得出来**。 # -# ⚠️⚠️ 两个都要,而这是被实测逼出来的。只查不建会把 `llvm × x86_64-windows-gnu` +# 两个都要,而这是被实测逼出来的。只查不建会把 `llvm × x86_64-windows-gnu` # 报成绿:它解析得完全正常,失败发生在链接期的封闭性检查上。只建不查则回到老路 # —— 只能靠匹配句子来分辨「拒绝」与「炸了」,而 2026-08-26 同一次会话里,我把 # `cannot emit it` 改成 `cannot be emitted by`,一条断言当场变成空转。 # -# ⭐ 于是这里**没有一处字符串匹配**。分类全部来自 `data.status` / `data.reason` +# 于是这里**没有一处字符串匹配**。分类全部来自 `data.status` / `data.reason` # 与构建的退出码。 # -# ⚠️ 不直接问编译器。绕开被测对象去问它的组件,得到的是组件的默认行为而不是 mcpp +# 不直接问编译器。绕开被测对象去问它的组件,得到的是组件的默认行为而不是 mcpp # 的行为 —— 分析文档里有一次就是这么错的。 # # 输出 TSV 到 stdout,一行一格: # mode host target compiler compiler-triple c-lib c-abi c++-abi openkal status reason # -# ⚠️ `mode` 是键的一部分,不是注释。两种体系跑的是同一组 (host, target, compiler), +# `mode` 是键的一部分,不是注释。两种体系跑的是同一组 (host, target, compiler), # 少了它两张表会互相覆盖 —— 而覆盖的方向取决于谁后跑,不取决于谁对。 # # status 只有三种,而三者的区别是整套验收的核心: @@ -35,21 +35,21 @@ set -u MCPP="${MCPP:-mcpp}" MODE="${1:-payload}" # payload | graph -# ⚠️ jq 缺席必须是硬错误。它的失败方式本来是「每一格都空着」,而一张全空的表和 +# jq 缺席必须是硬错误。它的失败方式本来是「每一格都空着」,而一张全空的表和 # 一张全绿的表在退出码上没有区别 —— 这正是这套矩阵存在的理由。 command -v jq >/dev/null 2>&1 || { echo "scan: jq is required (every GitHub-hosted runner ships it)" >&2 exit 2 } -# ⚠️⚠️ `timeout` IS GNU coreutils AND macOS HAS NEITHER IT NOR `gtimeout`. +# `timeout` IS GNU coreutils AND macOS HAS NEITHER IT NOR `gtimeout`. # # Measured on macos-14, the first run that reached this script: every cell came # back `mismatch / query-failed` — all 20 of them — because the wrapper was not # a command. `mcpp toolchain list`, which this script does NOT wrap, worked # fine, which is how the two were told apart. # -# ⭐ `tests/e2e/run_all.sh` has had this exact detection since it shipped. The +# `tests/e2e/run_all.sh` has had this exact detection since it shipped. The # defect was not that the problem is hard; it is that a second copy of a # decision was written without looking at the first. # @@ -68,7 +68,7 @@ run_limited() { # seconds cmd… → run with a limit if one is available work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT mkdir -p "$work/src"; cd "$work" -# ⚠️⚠️ 构建机是 (os, arch),不是 os。 +# 构建机是 (os, arch),不是 os。 # # mcpp 自己发布四份宿主二进制:linux-x86_64 / linux-aarch64 / macosx-arm64 / # windows-x86_64。两台 Linux 服务的行**不一样** —— `x86_64-linux-gnu` 需要本机 @@ -91,14 +91,14 @@ HOST="$HOST_OS-$HOST_ARCH" # 就会漂移,而按列宽解析一张给人看的表,会让列宽变成测试套件的一部分。实测过 # 的代价:两版测试对「版本在第几列」的看法不同,读 `$NF` 的那版取到了 # `(default)` —— 一个恰好只出现在最可能被选中的那一行上的值。 -# ⚠️⚠️ CR 必须剥掉,而不是指望它不出现。 +# CR 必须剥掉,而不是指望它不出现。 # # git-bash 里 jq 以文本模式写 stdout,`\n` 变成 `\r\n`,而 `\r` 不在 IFS 里 —— # 于是每个词尾都挂着一个 CR。实测 windows-2022:48 格里 38 格是 # `unsupported / other`,因为 `--target "aarch64-linux-gnu\r"` 解析不了。 # 整台机器的扫描测的是一张被污染的目标表。 # -# ⭐ 剥在**读进来的那一处**,不在每个使用点 —— 后者是同一个决定写 N 遍。 +# 剥在**读进来的那一处**,不在每个使用点 —— 后者是同一个决定写 N 遍。 jq_r() { jq -r "$@" | tr -d '\r'; } LIST="$("$MCPP" toolchain list --format json 2>/dev/null | tr -d '\r')" @@ -106,13 +106,13 @@ LIST="$("$MCPP" toolchain list --format json 2>/dev/null | tr -d '\r')" targets() { printf '%s' "$LIST" | jq_r '.data.targets[].target' | sort -u; } -# ⭐ 每族只取最新的一个。矩阵回答的是「这个目标支不支持」,同一族的三个版本对 +# 每族只取最新的一个。矩阵回答的是「这个目标支不支持」,同一族的三个版本对 # 这个问题给同一个答案,而 5×12 与 2×12 在 CI 上是小时级的差别。 # -# ⚠️ 但收窄必须说出来。被丢掉的版本写到 stderr —— 一次没跑的测量和一次通过的 +# 但收窄必须说出来。被丢掉的版本写到 stderr —— 一次没跑的测量和一次通过的 # 测量,在退出码上没有区别。 # -# ⚠️⚠️ 而「装了哪些」不是「这台宿主声明了哪些」,把前者当成后者会让缓存决定判据。 +# 而「装了哪些」不是「这台宿主声明了哪些」,把前者当成后者会让缓存决定判据。 # # 实测 2026-08-26,同一个提交:PR 的 `scan (windows-x86_64)` 绿,合入 main 后同一 # 个 job 红 —— 因为那次 runner 恢复出来的缓存里多了一个 `gcc@16.1.0`,扫描于是产出 @@ -122,10 +122,10 @@ targets() { printf '%s' "$LIST" | jq_r '.data.targets[].target' | sort -u; } # 份声明**。让编译器轴跟着声明走,这一整类假红就消失了:`MATRIX_COMPILERS` 由 # workflow 从 expected.tsv 自己那一列算出来传进来。 # -# ⭐ 不传时退回「装了什么扫什么」,因为本机跑没有期望表可依。 +# 不传时退回「装了什么扫什么」,因为本机跑没有期望表可依。 compilers() { if [ -n "${MATRIX_COMPILERS:-}" ]; then - # ⚠️ 收窄仍然要说出来。装着却不在声明里的那些,写到 stderr —— 一次没跑的 + # 收窄仍然要说出来。装着却不在声明里的那些,写到 stderr —— 一次没跑的 # 测量和一次通过的测量,在退出码上没有区别。 printf '%s' "$LIST" | jq_r '.data.toolchains[] | .family + "@" + .version' \ | while IFS= read -r have; do @@ -146,7 +146,7 @@ emit() { printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$@"; } for tc in $(compilers); do for t in $(targets); do - # ⚠️⚠️ graph 体系不覆盖裸机行,而这是一句声明,不是一次省略。 + # graph 体系不覆盖裸机行,而这是一句声明,不是一次省略。 # # openkal 的这套依赖(`openkal-musl` + `openkal-llvm-runtime`)描述的是一个 # **有宿主的**系统;把它加到零 libc 的目标上问的不是「图能不能供给这个目标」, @@ -155,7 +155,7 @@ for tc in $(compilers); do # # 裸机 × 图由 135/136/292 用正确的包覆盖。这里跳过并说出来。 # - # ⚠️ THE SAME PREDICATE AS THE PROBE BELOW, AND IT HAD THE SAME DEFECT. + # THE SAME PREDICATE AS THE PROBE BELOW, AND IT HAD THE SAME DEFECT. # Written as `-none-elf$`, this skipped only the four targets that spell # their environment `elf`; the Cortex-M rows spell it `eabi`/`eabihf`, so # they were NOT skipped and produced fourteen graph cells describing a @@ -170,15 +170,15 @@ for tc in $(compilers); do [ "$MODE" = graph ] && printf '\n[dependencies]\nopenkal-musl = "0.3.5"\nopenkal-llvm-runtime = "0.1.3"\n' } > mcpp.toml - # ⚠️⚠️ 探针必须按目标的层级选,一份源码服务不了整张表。 + # 探针必须按目标的层级选,一份源码服务不了整张表。 # # 第一版对每一格都写 `#include `,于是四个裸机目标全报 # `fatal error: 'cstdio' file not found` 并被记成 `mismatch`。那不是 mcpp # 的失败 —— 零 libc 的目标本来就没有 ``,是探针问错了问题。 # - # ⭐ 一个自己就编不过的探针,产出的整列都是关于探针的。 + # 一个自己就编不过的探针,产出的整列都是关于探针的。 # - # ⚠️ AND THE PREDICATE IS THE OS FIELD, NOT A FILENAME SUFFIX. This read + # AND THE PREDICATE IS THE OS FIELD, NOT A FILENAME SUFFIX. This read # `-none-elf$` until 2026-09-04, which is a spelling rather than a property: # the Cortex-M rows are `…-none-eabi` and `…-none-eabihf`, so they fell into # the hosted branch, got `#include ` — and produced a whole column of @@ -197,14 +197,14 @@ for tc in $(compilers); do fi # ── 第一问:这一格会解析成什么 ──────────────────────────────────── - # ⚠️ stderr 留到一个文件里,不丢。前一版写的是 `2>/dev/null`,于是 + # stderr 留到一个文件里,不丢。前一版写的是 `2>/dev/null`,于是 # `query-failed` 是对的分类而**没有任何证据**说明为什么 —— macOS 上 20 格 # 全红,原因(`timeout` 不存在)被这个重定向吞掉了。 q="$(run_limited "${MATRIX_QUERY_TIMEOUT:-300}" \ "$MCPP" why toolchain --target "$t" --toolchain "$tc" --format json \ 2>"$work/q.err" | tr -d '\r')" if [ -z "$q" ]; then - # ⚠️ 查询本身没跑起来。这不是「这一格不支持」,而是「不知道」—— 两者必须 + # 查询本身没跑起来。这不是「这一格不支持」,而是「不知道」—— 两者必须 # 分开,否则一次环境故障会被整片读成「不支持」。 echo "scan: $t × $tc 查询无输出: $(head -2 "$work/q.err" | tr '\n' ' ')" >&2 emit "$MODE" "$HOST" "$t" "$tc" - - - - - mismatch query-failed @@ -215,7 +215,7 @@ for tc in $(compilers); do rs="$(jq_get '.data.reason // "-"')" tri="$(jq_get '(.data.triple.llvm // "") | if . == "" then "-" else . end')" clib="$(jq_get '.data.cLibrary.origin // "-"')" - # ⚠️ 缺席的层是 `-`,不是 `(none)`。一个空的接口名加一对括号,读起来像 + # 缺席的层是 `-`,不是 `(none)`。一个空的接口名加一对括号,读起来像 # 「有这一层而它没名字」,而实际是「这一层不存在」—— 裸机目标的 c-abi # 正是后者,那是一句陈述,不是一个空格。 lay() { jq_get "[.data.layers[] | select(.layer==\"$1\") @@ -226,7 +226,7 @@ for tc in $(compilers); do : "${tri:=-}" "${clib:=-}" "${cabi:=-}" "${cxxabi:=-}" "${okpkg:=-}" if [ "$st" = refused ]; then - # ⚠️ `other` 是「拒绝了而这一处分支还没有名字」。它是一句可见的承认, + # `other` 是「拒绝了而这一处分支还没有名字」。它是一句可见的承认, # 而承认之后要能查 —— 否则下一个人看到的仍是一个没有原因的 unsupported。 # 拒绝的**消息**在信封的 diagnostics 里,这里把它打出来。 if [ "$rs" = other ]; then @@ -240,7 +240,7 @@ for tc in $(compilers); do # ── 第二问:解析说可以,那它建得出来吗 ──────────────────────────── # - # ⭐ 退出码就是判据。`why` 已经回答了「为什么不」那一半,所以这里不需要再去 + # 退出码就是判据。`why` 已经回答了「为什么不」那一半,所以这里不需要再去 # 读任何一行输出。 printf '\n[toolchain]\ndefault = "%s"\n' "$tc" >> mcpp.toml rm -rf target @@ -248,10 +248,10 @@ for tc in $(compilers); do >"$work/b.out" 2>&1; then emit "$MODE" "$HOST" "$t" "$tc" "$tri" "$clib" "$cabi" "$cxxabi" "$okpkg" ok none else - # ⚠️ 与查询失败同型:`build-failed` 是对的分类,而没有证据说明为什么。 + # 与查询失败同型:`build-failed` 是对的分类,而没有证据说明为什么。 # macOS 上 20 格一模一样的红、日志里找不到原因,就是把输出丢掉的代价。 # 这里只留错误行,不倒整份构建日志 —— 40 格 × 一份完整日志读不动。 - # ⚠️ 三行,**整行**。前一版 `cut -c1-160` 把真正的错误砍在半路 —— + # 三行,**整行**。前一版 `cut -c1-160` 把真正的错误砍在半路 —— # macOS 那格只留下 `precompiled file '/private/var/…/target/` 就没了, # 而要看的正是后半截。判据的单位是一整行输出。 echo "scan: $t × $tc 构建失败:" >&2 diff --git a/tests/unit/test_cache_key.cpp b/tests/unit/test_cache_key.cpp index 4f23c863..d0b9c273 100644 --- a/tests/unit/test_cache_key.cpp +++ b/tests/unit/test_cache_key.cpp @@ -95,7 +95,7 @@ TEST(CacheKey, ToolchainIdentityChangesTheKey) { EXPECT_NE(ck::key_hex(b, pkg()), base); } } -// ⚠️ The axis that exists because an UPGRADE broke a build. +// The axis that exists because an UPGRADE broke a build. // // A freestanding triple silently implies -march/-mabi/-ffreestanding/ // -nostdinc++/-fno-exceptions/-fno-rtti, and WHICH ones is mcpp's decision — @@ -294,7 +294,7 @@ TEST(CacheKey, GeneratedFilesAreOrderIndependent) { // ── The key must describe the compilation that will happen ────────────────── // -// ⚠️⚠️ THIS KEY ONCE DERIVED ITS OWN INPUTS INSTEAD OF READING THE BUILD'S. +// THIS KEY ONCE DERIVED ITS OWN INPUTS INSTEAD OF READING THE BUILD'S. // // `freestanding::compile_flags` takes `targetCxxRuntime` because the answer // changes with it: a freestanding target whose graph supplies a C++ runtime is @@ -302,7 +302,7 @@ TEST(CacheKey, GeneratedFilesAreOrderIndependent) { // with it. `flags.cppm` passes it; `build_axes` did not, and so hashed the // flags of the other configuration. // -// ⭐ THE FAILURE IS A HIT ACROSS AN INCOMPATIBILITY, NOT A MISS. Two +// THE FAILURE IS A HIT ACROSS AN INCOMPATIBILITY, NOT A MISS. Two // configurations that must not share a slot produced the same key, so the // second build loaded the first's BMIs: // @@ -313,7 +313,7 @@ TEST(CacheKey, GeneratedFilesAreOrderIndependent) { // differently-sized copies of that one BMI: slotting per configuration was // working, choosing the slot was not. // -// ⚠️ IT WAS DORMANT UNTIL THE PARAMETER EXISTED. Before the flag became +// IT WAS DORMANT UNTIL THE PARAMETER EXISTED. Before the flag became // conditional the two computations agreed for every input, so ignoring one of // them was still correct. That is what makes the test worth writing against // `build_axes` rather than against a hand-built `BuildAxes`: the fixture above @@ -354,7 +354,7 @@ TEST(CacheKey, TheTwoFreestandingConfigurationsDoNotShareASlot) { ck::key_hex(ck::build_axes(freestanding_tc(true), m, "-std=c++23", {}, ""), pkg())); } -// ⭐ AND A HOSTED TARGET IS UNAFFECTED, so the fix cannot be read as "the key +// AND A HOSTED TARGET IS UNAFFECTED, so the fix cannot be read as "the key // now changes with something it should not". `freestanding::resolve` returns // nothing for a hosted triple, and the flags stay empty either way. TEST(CacheKey, AHostedTargetHasNoTargetImpliedFlagsEitherWay) { @@ -368,7 +368,7 @@ TEST(CacheKey, AHostedTargetHasNoTargetImpliedFlagsEitherWay) { EXPECT_EQ(a.targetImpliedFlags, b.targetImpliedFlags); } -// ⭐⭐ THE HEADER SET THE DRIVER IS POINTED AT IS PART OF THE IDENTITY. +// THE HEADER SET THE DRIVER IS POINTED AT IS PART OF THE IDENTITY. // // Everything else on axis A describes the COMPILER. Nothing described the // LIBRARY it compiles against — and the two are separately installed: one clang @@ -410,9 +410,9 @@ TEST(CacheKey, TheHeaderSetIsRecordedInTheEntry) { "-isystem/xim-x-glibc/2.44/include"); } -// ⭐⭐ AND THE PATHS IN IT ARE RELATIVE — INCLUDING THE ONES OUTSIDE ``. +// AND THE PATHS IN IT ARE RELATIVE — INCLUDING THE ONES OUTSIDE ``. // -// ⚠️ THE TWO TESTS ABOVE SET THE AXIS DIRECTLY, so neither of them runs the +// THE TWO TESTS ABOVE SET THE AXIS DIRECTLY, so neither of them runs the // relativization, and a store-only rule passed both while leaving this // developer's own home in every key. The toolchain that showed it is the // ordinary one: `CLibMode::Sysroot`, whose single compile token is @@ -424,7 +424,7 @@ TEST(CacheKey, TheHeaderSetIsRecordedInTheEntry) { // each other's entries, which is the property `normalize_driver_output` gives // up path information to preserve. // -// ⭐ THE CRITERION IS THE ABSENCE OF THE HOME, not the presence of a tag. +// THE CRITERION IS THE ABSENCE OF THE HOME, not the presence of a tag. // Asserting `starts_with("")` would still pass if the rest of the string // carried the absolute path behind it. TEST(CacheKey, TheHeaderSetCarriesNoAbsoluteHome) { @@ -457,7 +457,7 @@ TEST(CacheKey, TheHeaderSetCarriesNoAbsoluteHome) { EXPECT_NE(ck::key_hex(a, pkg()), ck::key_hex(b, pkg())); } -// ⚠️ Position-independent code was ABSENT from this key (issue #519). +// Position-independent code was ABSENT from this key (issue #519). // // `-fPIC` is whole-build — one shared link unit anywhere and every object in // the graph carries it — but the key hashes a package's DECLARED flags, not diff --git a/tests/unit/test_distribution.cpp b/tests/unit/test_distribution.cpp index bae075a9..12d4a5c2 100644 --- a/tests/unit/test_distribution.cpp +++ b/tests/unit/test_distribution.cpp @@ -583,7 +583,7 @@ TEST(Distribution, FreestandingCarriesNoRuntimeToDistribute) { EXPECT_FALSE(m.degraded); } -// ⚠️ The hosted form of the same fact. A package in the graph has compiled a +// The hosted form of the same fact. A package in the graph has compiled a // C++ runtime FOR THIS TARGET and its objects are already on the link line, so // there is no library to name and nothing to look for. Measured before this // existed: `ld64.lld: error: library not found for -lc++`. @@ -615,7 +615,7 @@ TEST(Distribution, GraphSuppliedRuntimeIsFormatIndependent) { } } -// ⚠️ And it does not depend on the contract the project asked for: a +// And it does not depend on the contract the project asked for: a // host-coupled request cannot be honoured by naming the system's runtime when // the graph's is already inside the artefact. TEST(Distribution, GraphSuppliedRuntimeIgnoresTheRequestedContract) { @@ -633,7 +633,7 @@ TEST(Distribution, GraphSuppliedRuntimeIgnoresTheRequestedContract) { // ── format_for: which format a target produces ────────────────────────────── // -// ⚠️ This was a lambda inside a fifteen-hundred-line function and therefore had +// This was a lambda inside a fifteen-hundred-line function and therefore had // no test, and what it got wrong was found by running three hosts against three // targets. The assertions below are the ones that would have found it in a // second: the canonical spellings mcpp itself uses contain neither `apple` nor @@ -663,7 +663,7 @@ TEST(Distribution, FormatFallsBackToSpellingForAnUnparseableTriple) { dist::Format::MachO); } -// ⚠️ And only then the host. A triple that says nothing at all is the one case +// And only then the host. A triple that says nothing at all is the one case // where the machine doing the building is the best available answer. TEST(Distribution, FormatUsesTheFallbackOnlyWhenTheTripleSaysNothing) { EXPECT_EQ(dist::format_for("", dist::Format::MachO), dist::Format::MachO); diff --git a/tests/unit/test_elf_runtime.cpp b/tests/unit/test_elf_runtime.cpp index 9021879d..c05db8dd 100644 --- a/tests/unit/test_elf_runtime.cpp +++ b/tests/unit/test_elf_runtime.cpp @@ -582,7 +582,7 @@ TEST(ElfRuntime, ThisBinaryExportsNothingOfItsOwn) { auto facts = elf::inspect_elf_runtime(self); ASSERT_TRUE(facts.has_value()) << facts.error(); - // ⚠️ PT_INTERP, not the ELF type: a PIE executable is ET_DYN exactly like + // PT_INTERP, not the ELF type: a PIE executable is ET_DYN exactly like // a shared library, and whether this binary is PIE is the payload // compiler's default rather than mcpp's decision. if (facts->interp.empty()) diff --git a/tests/unit/test_freestanding.cpp b/tests/unit/test_freestanding.cpp index 79e6a844..c73f7e2f 100644 --- a/tests/unit/test_freestanding.cpp +++ b/tests/unit/test_freestanding.cpp @@ -190,7 +190,7 @@ TEST(XpkgPayload, APinnedRefResolvesToThatVersionOrToNothing) { std::filesystem::create_directories(base / "xim-x-demo" / "1.0.0"); std::filesystem::create_directories(base / "xim-x-demo" / "2.0.0"); - // ⚠️ The whole point: asking for 1.0.0 and silently getting 2.0.0 is an + // The whole point: asking for 1.0.0 and silently getting 2.0.0 is an // answer only discovered later, in the artifact. auto pinned = xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@1.0.0")); ASSERT_TRUE(pinned.has_value()); @@ -231,7 +231,7 @@ TEST(XpkgEnvVar, BothSpellingsAreDerivedFromOneSanitizer) { // ── the target owns its C library ─────────────────────────────────────────── TEST(FreestandingTarget, BareMetalRowsNameTheirSysroot) { - // ⚠️ The axis that stops every bare-metal PACKAGE from naming a libc. + // The axis that stops every bare-metal PACKAGE from naming a libc. // Before it, a board-support package and a standard-library subset each // had to carry `[xlings] deps = ["xim:picolibc-riscv@..."]`, which bound // both to one libc, one ISA and one version of each — none of which is a @@ -264,7 +264,7 @@ TEST(FreestandingFlags, ExceptionsAndRttiAreOffForTheWholeGraph) { auto spec = resolve("riscv64-none-elf"); ASSERT_TRUE(spec.has_value()); auto f = compile_flags(*spec); - // ⚠️ These live with the TARGET, not in a project's cxxflags, because a + // These live with the TARGET, not in a project's cxxflags, because a // BMI records them: a dependency compiled with exceptions cannot be // imported by a TU without them, and clang reports that as a .pcm // "configuration mismatch" rather than as a flag disagreement. @@ -286,7 +286,7 @@ TEST(FreestandingFlags, HostedTargetsGetNoneOfThis) { TEST(BuildProgramCompatHint, RecognisesAllThreeFrontendSpellings) { using mcpp::build::mentions_missing_mcpp_api; - // ⚠️ Measured, not assumed: `if constexpr (requires { mcpp::runner("x"); })` + // Measured, not assumed: `if constexpr (requires { mcpp::runner("x"); })` // is a HARD ERROR when the name is absent, so a package cannot probe for a // newer API in-language. The compiler's error IS the compat channel, and // these are the three ways it arrives. @@ -351,7 +351,7 @@ TEST(FreestandingArtifacts, ObjcopyOnlyResolvesForABareMetalTarget) { // ── aarch64-none-elf: the second bare-metal architecture ──────────────────── // -// ⚠️ THESE ASSERT THE TWO VALUES THAT WOULD HAVE BEEN WRONG IF THE ROW HAD BEEN +// THESE ASSERT THE TWO VALUES THAT WOULD HAVE BEEN WRONG IF THE ROW HAD BEEN // FILLED IN BY ANALOGY WITH THE RISC-V ROWS ABOVE IT. // // `-mabi` is the first. RISC-V spells its ABI as a data model (`lp64d`, @@ -384,14 +384,14 @@ TEST(FreestandingTarget, Aarch64ResolvesNoCLibrary) { ASSERT_TRUE(t.has_value()); EXPECT_TRUE(mcpp::toolchain::triple::effective_sysroot(*t, nullptr).empty()); - // ⚠️ The other side: a project may still ASK for one, and the override is + // The other side: a project may still ASK for one, and the override is // what carries the request. An empty column means "nothing by default", not // "nothing is possible". const std::string want = "xim:some-aarch64-libc@1.0"; EXPECT_EQ(mcpp::toolchain::triple::effective_sysroot(*t, &want), want); } -// ⚠️ THIS TEST USED TO ASSERT THE OPPOSITE, AND IT WAS RIGHT UNTIL 2026-08-21. +// THIS TEST USED TO ASSERT THE OPPOSITE, AND IT WAS RIGHT UNTIL 2026-08-21. // // The libdir column names the sub-directory of a MULTILIB C library, and the // aarch64 and x86_64 rows had none in the index to point into — so an empty @@ -404,7 +404,7 @@ TEST(FreestandingTarget, Aarch64ResolvesNoCLibrary) { // `[target.aarch64-none-elf] sysroot = "xim:picolibc-aarch64@1.8.12"` got // `'stdio.h' file not found` while the package sat installed and correct. // -// ⭐ The values below are a CROSS-REPOSITORY fact: they must match the +// The values below are a CROSS-REPOSITORY fact: they must match the // directory layout those two packages ship. `xim-pkgindex`'s // `.agents/tools/build-baremetal-sysroot.sh` produces them from the same // march/mabi pair this table carries, so the two cannot drift silently — but @@ -422,7 +422,7 @@ TEST(FreestandingTarget, MultilibDirectoriesMatchTheIndexPayloads) { EXPECT_EQ(x->libdir, "x86-64/sysv"); EXPECT_EQ(x->libdir, std::string(x->march) + "/" + std::string(x->mabi)); - // ⚠️ Filling this column does NOT give those targets a C library by + // Filling this column does NOT give those targets a C library by // default. It is consulted only once a sysroot has been RESOLVED, and the // target table still binds none for these two rows — the zero-libc tier // stays the default and the package stays opt-in. @@ -433,7 +433,7 @@ TEST(FreestandingTarget, MultilibDirectoriesMatchTheIndexPayloads) { // ── The two tables keyed on the same triple must agree ─────────────────────── // -// ⚠️ A BARE-METAL TARGET IS DESCRIBED TWICE, IN TWO FILES, AND NOTHING WAS +// A BARE-METAL TARGET IS DESCRIBED TWICE, IN TWO FILES, AND NOTHING WAS // CHECKING THAT THE TWO DESCRIPTIONS COVER THE SAME ROWS. // // `kKnownTargets` (toolchain/triple.cppm) says a row exists, which tier it is @@ -444,7 +444,7 @@ TEST(FreestandingTarget, MultilibDirectoriesMatchTheIndexPayloads) { // about a missing row. The reverse — present in the second, absent from the // first — is dead data that reads as support. // -// ⭐ The check is possible only because both are compile-time tables with a +// The check is possible only because both are compile-time tables with a // single read point each. It costs fifteen lines and removes an entire class // of "added a target, forgot half of it". TEST(FreestandingTarget, EveryBareRowInTheTargetTableHasAnIsaProfile) { @@ -472,7 +472,7 @@ TEST(FreestandingTarget, EveryIsaProfileHasABareRowInTheTargetTable) { // ── Cortex-M ──────────────────────────────────────────────────────────────── // -// ⭐ THESE ARE RULES OVER THE TABLE, WHICH IS THE ONLY PLACE THEY CAN BE +// THESE ARE RULES OVER THE TABLE, WHICH IS THE ONLY PLACE THEY CAN BE // STATED. The e2e boots four M-profile images and measures instruction counts, // but it can only speak about the rows it happens to exercise. A rule quantified // over every row is what stops the eighth one from being added wrong, and the @@ -480,7 +480,7 @@ TEST(FreestandingTarget, EveryIsaProfileHasABareRowInTheTargetTable) { // the FPU compiles, links, and faults on real silicon. namespace { -// ⚠️ EVERY 32-BIT ARM ROW, NOT ONLY THE M-PROFILE ONES. The first version of +// EVERY 32-BIT ARM ROW, NOT ONLY THE M-PROFILE ONES. The first version of // this predicate was `starts_with("thumb")` — a SPELLING rather than the // property the rule is about. When `armv7a-none-eabi` was added, the rule // applied to it and this test did not, silently: the loop simply skipped the @@ -516,7 +516,7 @@ TEST(FreestandingTarget, SoftFloatArm32RowsDisableTheFpu) { " instructions that fault on a part with no FPU"; } } - // ⚠️ A denominator, because a loop that never ran satisfies every EXPECT + // A denominator, because a loop that never ran satisfies every EXPECT // above. Both halves must be non-empty for the contrast to mean anything. EXPECT_GT(soft, 0); EXPECT_GT(hard, 0); @@ -560,7 +560,7 @@ TEST(FreestandingTarget, EveryRowCompilesWithPerFunctionSections) { EXPECT_GT(rows, 0); } -// ⚠️⚠️ THE MULTILIB KEY IS NOT `/` ON ARM, AND ASSUMING IT WAS +// THE MULTILIB KEY IS NOT `/` ON ARM, AND ASSUMING IT WAS // PRODUCES AN ABI SUBSTITUTION THAT NOTHING REPORTS. // // The column names the sub-directory a multilib C library uses. On riscv, @@ -599,7 +599,7 @@ TEST(FreestandingTarget, Arm32SoftAndHardVariantsNameDifferentLibdirs) { << distinct.size() << " directories: a soft-float program would " "link a hard-float library, silently"; } - // ⚠️ A denominator. If no architecture had two rows the loop above would + // A denominator. If no architecture had two rows the loop above would // assert nothing, and the property would be untested rather than held. EXPECT_GT(pairs, 0) << "no architecture carries both float ABIs; the check " "above quantified over nothing"; diff --git a/tests/unit/test_graph_shape.cpp b/tests/unit/test_graph_shape.cpp new file mode 100644 index 00000000..40a75ae9 --- /dev/null +++ b/tests/unit/test_graph_shape.cpp @@ -0,0 +1,54 @@ +#include +import std; +import mcpp.build.graph_shape; + +// The header line build.ninja carries is what the fast paths read before any +// plan exists. Three facts ride it -- the graph's shape, the module-edge +// schedule, and (2026.9.5.3+) whether a `--accel` / `--no-accel` override +// chose the device variant -- and the fast paths decline a graph that says +// anything other than "plain build, manifest's own variant". + +namespace { + +std::filesystem::path write_graph(const std::string& first) { + static const auto run = std::random_device{}(); + auto dir = std::filesystem::temp_directory_path() + / ("mcpp-graph-shape-" + std::to_string(run) + "-" + + std::to_string(std::hash{}(first))); + std::filesystem::create_directories(dir); + auto p = dir / "build.ninja"; + std::ofstream os(p, std::ios::trunc); + os << "# Auto-generated by mcpp v0.0.1. Do not edit by hand.\n" + << first << "\n" + << "ninja_required_version = 1.11\n"; + return p; +} + +} // namespace + +TEST(GraphShape, TheHeaderNamesShapeScheduleAndSelection) { + EXPECT_EQ(mcpp::build::header_line(mcpp::build::GraphShape::Normal, "none", false), + "# mcpp:graph=normal;schedule=none;accel=default"); + EXPECT_EQ(mcpp::build::header_line(mcpp::build::GraphShape::WithTests, "two-phase", true), + "# mcpp:graph=test;schedule=two-phase;accel=override"); +} + +TEST(GraphShape, OnlyAPlainGraphWithTheManifestsVariantIsReplayed) { + using namespace mcpp::build; + EXPECT_TRUE(is_plain_build_graph(write_graph(header_line(GraphShape::Normal, "none", false)))); + // An override chose this variant: a plain build must not replay it. + EXPECT_FALSE(is_plain_build_graph(write_graph(header_line(GraphShape::Normal, "none", true)))); + // The test-shaped graph was already refused. + EXPECT_FALSE(is_plain_build_graph(write_graph(header_line(GraphShape::WithTests, "none", false)))); +} + +TEST(GraphShape, AGraphThatPredatesTheFieldIsAMiss) { + using namespace mcpp::build; + // Written by a 2026.9.5.2 mcpp: shape and schedule, no selection. It is + // not known to be the manifest's variant, so it is prepared once more and + // rewritten with the field -- a miss, never a guess. + auto p = write_graph("# mcpp:graph=normal;schedule=none"); + EXPECT_EQ(read_shape(p), GraphShape::Normal); + EXPECT_EQ(read_accel_selection(p), ""); + EXPECT_FALSE(is_plain_build_graph(p)); +} diff --git a/tests/unit/test_hostflags.cpp b/tests/unit/test_hostflags.cpp index 36e49fdf..c135aa0b 100644 --- a/tests/unit/test_hostflags.cpp +++ b/tests/unit/test_hostflags.cpp @@ -268,7 +268,7 @@ TEST(GraphRuntimeFlags, MachOTakesEmulatedTlsAndHiddenVisibilityButNotDwarf) { EXPECT_TRUE(has(f, "-fvisibility-inlines-hidden")); } -// ⚠️ ELF takes NONE of them, and that is a decision rather than an omission. +// ELF takes NONE of them, and that is a decision rather than an omission. // There a `thread_local` is a fixed offset from the thread pointer, which the // C library establishes itself; adding the flag would work, cost an // indirection on every access, and make ELF the only target whose thread @@ -278,7 +278,7 @@ TEST(GraphRuntimeFlags, ElfTakesNone) { EXPECT_TRUE(f.empty()); } -// ⚠️ And nothing at all when the runtime is NOT the graph's, whatever the +// And nothing at all when the runtime is NOT the graph's, whatever the // target. The predicate is `targetCxxRuntime`; a native or payload-served build // of the same triple must be untouched. TEST(GraphRuntimeFlags, PayloadServedTargetTakesNoneEvenOnPe) { @@ -297,7 +297,7 @@ TEST(GraphRuntimeFlags, UnparseableTripleTakesNone) { graph_tc("not-a-triple-at-all")).empty()); } -// ⭐⭐ `--no-default-config` IS NOT PART OF THE PAYLOAD'S HEADER SET, AND WAS +// `--no-default-config` IS NOT PART OF THE PAYLOAD'S HEADER SET, AND WAS // BEING SUPPRESSED WITH IT. // // The payload's `-isystem` rows describe a C library a graph-supplied target @@ -311,14 +311,14 @@ TEST(GraphRuntimeFlags, UnparseableTripleTakesNone) { // `-clang++.cfg` a working workaround for mcpp#514 — a workaround that // only existed because this token went missing. TEST(HostFlags, TheCfgBypassSurvivesAGraphSuppliedTargetSide) { - // ⚠️ A FIXTURE, NOT THE MACHINE'S OWN TOOLCHAIN. The first draft used a + // A FIXTURE, NOT THE MACHINE'S OWN TOOLCHAIN. The first draft used a // synthetic `Toolchain` with no `binaryPath`, so `resolve_clang_driver` // reported no cfg and the whole test SKIPPED — a check that asserts // nothing while reporting success, which is the one failure mode a test // must not have. `resolve_clang_driver` only asks whether a sibling // `.cfg` EXISTS, so two empty files are a complete fixture. namespace fs = std::filesystem; - // ⚠️ A FIXED NAME AND `remove_all` FIRST, NOT A PROCESS ID. The first + // A FIXED NAME AND `remove_all` FIRST, NOT A PROCESS ID. The first // draft reached for `::getpid()` and ``, which do not exist under // MSVC — it built on the machine it was written on and failed on Windows // CI, which is the only place that half of this project is visible. diff --git a/tests/unit/test_install_integrity.cpp b/tests/unit/test_install_integrity.cpp index f9180680..0c99b21a 100644 --- a/tests/unit/test_install_integrity.cpp +++ b/tests/unit/test_install_integrity.cpp @@ -202,7 +202,7 @@ TEST(InstallEvidence, OneEntryTheInstallWroteIsEnough) { fs::remove_all(dir); } -// ⭐ THE HALF THAT MAKES THE UPGRADE SEAMLESS. `is_install_complete` is +// THE HALF THAT MAKES THE UPGRADE SEAMLESS. `is_install_complete` is // marker-only by design, so a store poisoned before this check existed carries // a `.mcpp_ok` that short-circuits forever. Guarding only the WRITE site would // help users who have not hit the bug and do nothing for the ones who filed diff --git a/tests/unit/test_link_intent_spelling.cpp b/tests/unit/test_link_intent_spelling.cpp index a0de0373..cbed0b61 100644 --- a/tests/unit/test_link_intent_spelling.cpp +++ b/tests/unit/test_link_intent_spelling.cpp @@ -13,7 +13,7 @@ using mcpp::build::render_link_intent_flags; // reader — a consumer driven by native `cl.exe`, which rejects `-L` — and this // is where "neutral" is turned back into a command line. // -// ⚠️ THE FLAVOUR IS A QUESTION ABOUT THE DRIVER, NOT ABOUT THE TARGET, and that +// THE FLAVOUR IS A QUESTION ABOUT THE DRIVER, NOT ABOUT THE TARGET, and that // is the opposite of the rule three other flags in this area follow. `-fPIC`, // `--out-implib` and `/DEF:` reach the LINKER (through `-Wl,`), so the target // ABI decides their spelling. These reach whatever mcpp INVOKES: with the MSVC diff --git a/tests/unit/test_linkage_form.cpp b/tests/unit/test_linkage_form.cpp index b7ebfa4d..018eca16 100644 --- a/tests/unit/test_linkage_form.cpp +++ b/tests/unit/test_linkage_form.cpp @@ -138,7 +138,7 @@ TEST(LinkageForm, AFreestandingTargetHasNothingToLoadASharedLibraryWith) { } TEST(LinkageForm, AFullyStaticLibcVetoesTheSharedForm) { - // ⚠️ The two axes named `linkage` are NOT independent. A `-static` image + // The two axes named `linkage` are NOT independent. A `-static` image // has no interpreter, so it cannot load a shared object — and musl // defaults to `linkage = "static"`, which makes this the COMMON path // there rather than a corner. diff --git a/tests/unit/test_loader_contract.cpp b/tests/unit/test_loader_contract.cpp index 9846630f..31a5196f 100644 --- a/tests/unit/test_loader_contract.cpp +++ b/tests/unit/test_loader_contract.cpp @@ -56,9 +56,19 @@ TEST(GraphShape, UnlabelledOrUnknownGraphIsNeverPlain) { }; EXPECT_TRUE(is_plain_build_graph( - write("normal.ninja", "# banner\n# mcpp:graph=normal\nrule x\n"))); + write("normal.ninja", "# banner\n# mcpp:graph=normal;schedule=none;accel=default\nrule x\n"))); EXPECT_FALSE(is_plain_build_graph( - write("test.ninja", "# banner\n# mcpp:graph=test\nrule x\n"))); + write("test.ninja", "# banner\n# mcpp:graph=test;schedule=none;accel=default\nrule x\n"))); + + // A plain-shaped graph an `--accel` / `--no-accel` build wrote (2026.9.5.3+): + // the variant a flag chose is not the variant a plain build produces. + EXPECT_FALSE(is_plain_build_graph( + write("override.ninja", "# mcpp:graph=normal;schedule=none;accel=override\n"))); + + // A graph from 2026.9.5.2 and earlier: shape and schedule, no selection + // field. Not known to be the manifest's variant, so a miss, not a guess. + EXPECT_FALSE(is_plain_build_graph( + write("no-selection.ninja", "# banner\n# mcpp:graph=normal\nrule x\n"))); // A build.ninja from before the marker existed. It MUST read as a miss: // treating it as plain is precisely the replay #407 is about. diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 6554670a..1abc74e1 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -135,7 +135,7 @@ soname = "libdep.so.1" } TEST(Manifest, RejectsSonameOnANonLibraryTarget) { - // ⚠️ NARROWED from "non-shared" to "non-library" (#519). A soname is the + // NARROWED from "non-shared" to "non-library" (#519). A soname is the // name a library is FOUND by, and a package needs to be able to state it // while still being consumed as a static library — otherwise two copies // of one library can never resolve to a single file. An EXECUTABLE still @@ -593,7 +593,7 @@ defines = ["TEST_USE_MODULES", "VALUE=42"] // silence, because the only sentence mcpp offers about the key says the // opposite of what happens. // -// ⚠️ THE LIST BELOW IS ANOTHER COPY, AND THAT IS THE POINT. A copy that fails +// THE LIST BELOW IS ANOTHER COPY, AND THAT IS THE POINT. A copy that fails // loudly when it disagrees is the whole difference from the arrangement that // let this happen twice. Add a key to the parser, add it here; if you forget, // the negative control at the bottom of each test still passes and this one @@ -1276,7 +1276,7 @@ package = { // Feature System v2 Stage 2a: optional deps activated by a feature. // TOML surface uses a dedicated [feature-deps.] section. -// ⚠️ The fixture used to say `zlib = "1.3.x"`, which cannot resolve: a trailing +// The fixture used to say `zlib = "1.3.x"`, which cannot resolve: a trailing // `.x` is not a selector this resolver has. It went unnoticed because the same // form was in the documented `[feature-deps]` example, and because a fixture // only has to PARSE — nothing here ever asked the index for that package. It @@ -3611,7 +3611,7 @@ dependency_linkage = "shared" ASSERT_TRUE(ok); EXPECT_EQ(ok->buildConfig.dependencyLinkage, "shared"); - // ⚠️ "dynamic" is the LIBC axis' word. Accepting it here would silently + // "dynamic" is the LIBC axis' word. Accepting it here would silently // mean "static", because that is what an unparsed value resolves to. auto bad = load_inline("deplinkage_bad", R"( [package] @@ -3657,7 +3657,7 @@ zlib = { version = "1.0.0", linkage = "dynamic" } } TEST(Manifest, ALibraryTargetMayDeclareASoname) { - // ⚠️ This used to make the WHOLE MANIFEST FAIL TO LOAD, in both parsers. + // This used to make the WHOLE MANIFEST FAIL TO LOAD, in both parsers. // A soname is the name a library is FOUND by, and it is the only way two // copies of one library can resolve to a single file — so a package has // to be able to state it while still being consumed as a static library, @@ -3861,7 +3861,7 @@ schedule = "on" EXPECT_TRUE(o->buildConfig.bmiSchedule.empty()) << "the pre-rename key `schedule` is still being read"; - // ⚠️ AND THE PARSER MUST NOT WARN ABOUT ITS OWN KEY. + // AND THE PARSER MUST NOT WARN ABOUT ITS OWN KEY. // // Checking only the VALUE is what let this ship broken. The rename reached // the read (`build.bmi_schedule`) but not the accepted-key list, which kept @@ -3893,7 +3893,7 @@ schedule = "on" // was accepted in silence. A configuration key that does nothing is worse than // one that does not exist. // -// ⚠️ AND THE CHECK MUST NOT SEE THE CONDITIONAL CHANNEL. TOML presents +// AND THE CHECK MUST NOT SEE THE CONDITIONAL CHANNEL. TOML presents // `[target..dependencies]` as a KEY of `[target.]`, so a // hand-written "known keys" list has to enumerate `build`, `dependencies`, // `dev-dependencies`, `build-dependencies`, `feature-deps` too — and that list @@ -3958,7 +3958,7 @@ runner = ["qemu-system-riscv64", "-machine", "virt", "-kernel"] ASSERT_EQ(it->second.runner.size(), 4u); EXPECT_EQ(it->second.runner.front(), "qemu-system-riscv64"); EXPECT_EQ(it->second.runner.back(), "-kernel"); - // ⚠️ The unknown-key sweep is about SCALARS ("a scalar that does + // The unknown-key sweep is about SCALARS ("a scalar that does // nothing"). It skipped tables but not arrays, so this key was reported as // "unsupported key 'runner' (ignored)" while in fact being honoured — // worse than either statement being true. Seen in CI. @@ -4014,7 +4014,7 @@ sysroot = "xim:newlib-riscv@4.4" << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); } -// ⚠️ The distinction this test pins is the reason the field is an optional. +// The distinction this test pins is the reason the field is an optional. // An empty string is a REQUEST (no C library), not an absence. TEST(Manifest, TargetSysrootEmptyStringIsRecordedAsPresent) { constexpr auto src = R"( @@ -4080,20 +4080,20 @@ sysroot = "" // ── Dependency version requirements are checked where they are written ─────── // -// ⚠️ The parser that decides which published version satisfies a requirement +// The parser that decides which published version satisfies a requirement // existed all along; the dependency reader did not use it, and handed its // string to the installer instead. A requirement the matcher could never // satisfy therefore reached the network and came back as // `E_NOT_FOUND: package '…@0.1.x' not found` — naming the PACKAGE, which // exists. This repository's own documentation recommended that form. -// ⚠️ PARSE-acceptance, which is a weaker property than installability and was +// PARSE-acceptance, which is a weaker property than installability and was // measured to be weaker: `"0.0"` parses here and then fails at fetch with // `install path missing`. The check added below is a manifest check and has no // business ruling on the installer's path derivation, so this test pins what // the PARSER must keep accepting and says nothing about what fetches. TEST(Manifest, DependencyVersionParserAcceptsEveryEstablishedForm) { - // ⭐ This test exists to prevent the check from NARROWING anything. + // This test exists to prevent the check from NARROWING anything. for (const char* v : { "0.0.1", "0.0", "^0.0.1", ">=0.0.1, <0.1.0", "*" }) { auto src = std::format(R"( [package] @@ -4107,7 +4107,7 @@ cmdline = "{}" } } -// ⚠️ REPORTED, NOT REJECTED, and the distinction is load-bearing. The first +// REPORTED, NOT REJECTED, and the distinction is load-bearing. The first // version of this check returned an error, and a project pinned to a PUBLISHED // package carrying such a string stopped loading entirely — including when the // offending entry belonged to a feature nobody activates. Published data must @@ -4126,7 +4126,7 @@ cmdline = "0.0.x" for (auto const& w : m->schemaWarnings) if (w.find("not a requirement") != std::string::npos) found = true; EXPECT_TRUE(found) << "no warning naming the requirement"; - // ⭐ And it must say that the PACKAGE is not the problem, because the + // And it must say that the PACKAGE is not the problem, because the // failure the reader will otherwise see names exactly that. for (auto const& w : m->schemaWarnings) if (w.find("not a requirement") != std::string::npos) @@ -4211,7 +4211,7 @@ TEST(ManifestHooks, AbsentSectionIsInertButNotDisabled) { EXPECT_FALSE(m->hooks.sideEffect); } -// ⚠️ The experimental gate. Refused rather than downgraded: honouring it would +// The experimental gate. Refused rather than downgraded: honouring it would // give an experimental feature a veto over every build, and ignoring it would // leave the project believing its build is gated on a notifier when nothing // is. Both silent options are worse than the error. @@ -4395,7 +4395,7 @@ during_build = { cmd = "play bgm.mp3", loop = true } EXPECT_TRUE(m->hooks.active()); } -// ⭐ The two keys that only exist for one interval. Accepted-and-ignored is +// The two keys that only exist for one interval. Accepted-and-ignored is // the failure mode this rejects: the user writes `loop` on `build_start`, // nothing repeats, and the feature looks broken rather than misused. Each // message names the event that does support the key. @@ -4746,7 +4746,7 @@ llvm = { macosx = "20", default = "22" } // ─── Tool tiers: `when` on an entry, and `[feature-xlings.]` ──────────── // -// ⭐ THE TWO SPELLINGS OF A TABLE MUST NOT BE CONFUSABLE. `[xlings.workspace]` +// THE TWO SPELLINGS OF A TABLE MUST NOT BE CONFUSABLE. `[xlings.workspace]` // already accepted an object keyed by platform; the tier form is an object // keyed by `version` and `when`. Neither key set contains a member of the // other, so the presence of `version`/`when` decides — and this test is what @@ -4845,7 +4845,7 @@ hardware = {} // never activates the feature never asks for it. EXPECT_EQ(std::ranges::find(m->xlings.deps, "xim:probe-rs@0.24.0"), m->xlings.deps.end()); - // ⚠️ A misspelt feature installs nothing and says nothing, which is the + // A misspelt feature installs nothing and says nothing, which is the // shape #531 was filed for. Reported as a schema warning rather than an // error, so a package may adopt a feature before its consumers upgrade. bool sawTypo = false; diff --git a/tests/unit/test_ninja_backend.cpp b/tests/unit/test_ninja_backend.cpp index 53db9bb6..d86e2944 100644 --- a/tests/unit/test_ninja_backend.cpp +++ b/tests/unit/test_ninja_backend.cpp @@ -51,7 +51,7 @@ BuildPlan minimal_plan() { plan.toolchain.binaryPath = "/usr/bin/g++"; plan.toolchain.targetTriple = "x86_64-linux-gnu"; - // ⚠️ AND THE TARGET SIDE, WHICH THIS FIXTURE USED TO LEAVE DEFAULT. + // AND THE TARGET SIDE, WHICH THIS FIXTURE USED TO LEAVE DEFAULT. // // A default-constructed `TargetSide` has every layer at `Origin::None`, // which is not a native build — it is "nothing has been resolved". No @@ -1623,7 +1623,7 @@ TEST(LinkFailureAdvice, StaysSilentOnUnrelatedFailures) { } TEST(LinkFailureAdvice, CarriesNoVersionLiteral) { - // ⚠️ The `import std` advice needed tests/e2e/135 to keep its version + // The `import std` advice needed tests/e2e/135 to keep its version // honest. This one is built so it cannot go stale the same way: it names a // package and a FEATURE, and the feature pulls the implementation. A digit // sequence that looks like a version here would be a regression in kind. @@ -1644,7 +1644,7 @@ TEST(LinkFailureAdvice, CarriesNoVersionLiteral) { // name. Twenty-six lines below `cc`, this file already carried the rule that // prevents it, written for `c_ldflags` and never applied to its sibling. // -// ⭐ The checker is exported so it can be asserted against manifests +// The checker is exported so it can be asserted against manifests // `emit_ninja_string` is supposed never to produce. A test that could only // reach it through a BuildPlan could not show the checker works at all. @@ -1746,7 +1746,7 @@ TEST(ActionOrdering, AGeneratedHeaderGetsAPhonyAndTheCompileEdgeWaitsForIt) { << "the compile edge does not wait for the generator\n" << ninja; } -// ⚠️ THE DENOMINATOR. "every edge that should carry the ordering does" is +// THE DENOMINATOR. "every edge that should carry the ordering does" is // vacuously true when no edge does — and that vacuum is exactly the state this // change fixes, so the count of edges is asserted separately from the count // that carries it. A bare equality would have passed against the defect. diff --git a/tests/unit/test_pack_archive_remove.cpp b/tests/unit/test_pack_archive_remove.cpp index ae1e40b9..464276c7 100644 --- a/tests/unit/test_pack_archive_remove.cpp +++ b/tests/unit/test_pack_archive_remove.cpp @@ -19,7 +19,7 @@ using mcpp::toolchain::msvc_dialect; // ar one verb, then the archive, then every member // lib.exe one flag PER member, and the archive comes LAST // -// ⚠️ WHY THIS IS A UNIT TEST. mcpp's Windows CI archives with clang's `llvm-ar`, +// WHY THIS IS A UNIT TEST. mcpp's Windows CI archives with clang's `llvm-ar`, // which takes the GNU spelling, so no job anywhere executes the MSVC branch. The // packer originally assumed `ar` syntax on every platform and nothing could have // caught it: the two constants live in dialect.cppm, the order they are assembled diff --git a/tests/unit/test_pack_relocate.cpp b/tests/unit/test_pack_relocate.cpp index ddf76bc0..51881928 100644 --- a/tests/unit/test_pack_relocate.cpp +++ b/tests/unit/test_pack_relocate.cpp @@ -264,7 +264,7 @@ TEST(PackRelocate, AMissingFileIsAnError) { // ── the strip table ───────────────────────────────────────────────────── TEST(PackStrip, StaticArchiveKeepsItsSymbolIndex) { - // ⚠️ THE MEASUREMENT BEHIND THIS TEST. `strip --strip-all` on a `.a` + // THE MEASUREMENT BEHIND THIS TEST. `strip --strip-all` on a `.a` // removes the archive symbol index, and the consumer's link then fails // with `archive has no index; run ranlib to add one` — a message that // names neither strip nor the publisher. `--strip-debug` keeps it diff --git a/tests/unit/test_post_install.cpp b/tests/unit/test_post_install.cpp index 85f44fb2..3dfa0f1f 100644 --- a/tests/unit/test_post_install.cpp +++ b/tests/unit/test_post_install.cpp @@ -114,7 +114,7 @@ mcpp::toolchain::XimToolchainPackage gcc_pkg() { } // namespace fixup_gate -// ⚠️ THE REGRESSION. An empty runtime identity used to be +// THE REGRESSION. An empty runtime identity used to be // std::unexpected("… default SubOS has no RuntimeBinding identity …") // and `prepare.cppm` turned that into `error: toolchain post-install fixup: …`, // so `mcpp build` AND `mcpp toolchain install` both died on any Linux machine @@ -193,7 +193,7 @@ TEST(PostInstallFixup, APackageWithNoFixupReportsNothingToReport) { << result->skippedReason; } -// ⭐⭐ THE VERSION THAT WAS ASKED FOR AND THE VERSION THAT WAS INSTALLED ARE +// THE VERSION THAT WAS ASKED FOR AND THE VERSION THAT WAS INSTALLED ARE // TWO VOCABULARIES FOR ONE FACT. // // A RuntimeBinding carries the DECLARED identity; xlings names the payload @@ -252,7 +252,7 @@ TEST(GlibcPayload, ARequestResolvesToItsOneRefinement) { EXPECT_EQ(*got, only); } -// ⚠️ PER COMPONENT, NOT PER CHARACTER. `2.4` is not a request that `2.44` +// PER COMPONENT, NOT PER CHARACTER. `2.4` is not a request that `2.44` // answers — a prefix match on the string would say it is, and would then hand // a build the wrong C library without saying anything. TEST(GlibcPayload, AStringPrefixIsNotARefinement) { @@ -262,7 +262,7 @@ TEST(GlibcPayload, AStringPrefixIsNotARefinement) { EXPECT_FALSE(got.has_value()); } -// ⭐ AND THE REFUSAL STILL STANDS WHEN THERE IS NO ONE ANSWER. "The resolution +// AND THE REFUSAL STILL STANDS WHEN THERE IS NO ONE ANSWER. "The resolution // of this request" has to be a single payload to be an answer at all; two // refinements are not a menu to pick from. TEST(GlibcPayload, TwoRefinementsAreRefusedRatherThanChosenBetween) { @@ -273,7 +273,7 @@ TEST(GlibcPayload, TwoRefinementsAreRefusedRatherThanChosenBetween) { EXPECT_FALSE(got.has_value()); } -// ⭐⭐ THE RESOLVER ITSELF, because it has TWO callers and they failed +// THE RESOLVER ITSELF, because it has TWO callers and they failed // separately. The first version of this fix lived inside the toolchain fixup; // `probe`'s compile-side payload discovery spelled the same lookup its own way // and kept missing — and ITS failure names no version at all: @@ -306,7 +306,7 @@ TEST(PayloadDirForVersion, TwoRefinementsAreNotAnAnswer) { EXPECT_FALSE(mcpp::xlings::paths::payload_dir_for_version(fx.root, "2.44")); } -// ⚠️ PER COMPONENT, NOT PER CHARACTER — a string prefix would hand a build the +// PER COMPONENT, NOT PER CHARACTER — a string prefix would hand a build the // wrong C library and say nothing. TEST(PayloadDirForVersion, AStringPrefixIsNotARefinement) { GlibcRootFixture fx{"mcpp_pdfv_strprefix"}; @@ -320,7 +320,7 @@ TEST(GlibcPayload, NothingInstalledIsStillRefused) { EXPECT_FALSE(got.has_value()); } -// ⚠️ AN EMPTY REQUEST IS NOT A REQUEST FOR EVERYTHING. +// AN EMPTY REQUEST IS NOT A REQUEST FOR EVERYTHING. // // `packageRoot / ""` is `packageRoot`, and that IS a directory — so the // exact-match branch would hand the CONTAINER back and every caller would diff --git a/tests/unit/test_provisions.cpp b/tests/unit/test_provisions.cpp index 41d822a1..bf534be8 100644 --- a/tests/unit/test_provisions.cpp +++ b/tests/unit/test_provisions.cpp @@ -230,8 +230,8 @@ std::filesystem::path write_iface(std::string_view stem, std::string_view body) TEST(HostModuleIdentity, TheDeclaredNameWinsOverThePackageName) { auto p = write_iface("declared-wins", - "export module mcpp.build.protobuf;\nimport std;\n"); - EXPECT_EQ(prov::host_module_name(p, "protobufgen"), "mcpp.build.protobuf"); + "export module mcpp.rules.protobuf;\nimport std;\n"); + EXPECT_EQ(prov::host_module_name(p, "protobufgen"), "mcpp.rules.protobuf"); } TEST(HostModuleIdentity, ADottedNameSurvivesWhole) { @@ -297,15 +297,15 @@ TEST(HostModuleIdentity, DistinctModuleNamesFromOnePackageNameAreFine) { } TEST(ReservedPrefix, AnOutsidePackageClaimingMcppIsWarnedAbout) { - auto w = prov::reserved_prefix_warning("mcpp.build.protobuf", "acme", + auto w = prov::reserved_prefix_warning("mcpp.rules.protobuf", "acme", "acme.protobufgen"); ASSERT_TRUE(w.has_value()); - EXPECT_NE(w->find("mcpp.build.protobuf"), std::string::npos); + EXPECT_NE(w->find("mcpp.rules.protobuf"), std::string::npos); EXPECT_NE(w->find("acme.protobufgen"), std::string::npos); } TEST(ReservedPrefix, TheOfficialNamespaceIsSilent) { - EXPECT_FALSE(prov::reserved_prefix_warning("mcpp.build.protobuf", "mcpp", + EXPECT_FALSE(prov::reserved_prefix_warning("mcpp.rules.protobuf", "mcpp", "mcpp.protobuf").has_value()); } @@ -317,3 +317,37 @@ TEST(ReservedPrefix, AnOrdinaryNameIsSilent) { EXPECT_FALSE(prov::reserved_prefix_warning("mcppish", "acme", "acme.mcppish").has_value()); } + +// A package that offers several rules through features (mcpp 2026.9.5.3+) +// contributes every module INTERFACE unit among its resolved sources. The +// detector decides what counts as one, and the cases below are the ones a +// wrong answer would turn into a compile of something that cannot be compiled +// alone -- an implementation unit, a partition -- or into a phantom module +// named by a comment. +TEST(InterfaceUnit, ThePrimaryInterfaceDeclarationIsRecognised) { + EXPECT_EQ(prov::declared_interface_name("export module mcpp.rules.cuda;\n"), + "mcpp.rules.cuda"); + EXPECT_EQ(prov::declared_interface_name( + "module;\n#include \nexport module a.b;\nimport std;\n"), + "a.b"); + EXPECT_EQ(prov::declared_interface_name("\texport module\tx ; // trailing\n"), + "x"); +} + +TEST(InterfaceUnit, WhatIsNotAPrimaryInterfaceIsNotNamed) { + EXPECT_EQ(prov::declared_interface_name("module a.b;\n"), ""); // implementation + EXPECT_EQ(prov::declared_interface_name("export module a.b:part;\n"), ""); // partition + EXPECT_EQ(prov::declared_interface_name("module;\n"), ""); // global fragment + EXPECT_EQ(prov::declared_interface_name("export module;\n"), ""); + EXPECT_EQ(prov::declared_interface_name("// export module a.b;\n"), ""); // documentation + EXPECT_EQ(prov::declared_interface_name("exportmodule a.b;\n"), ""); + EXPECT_EQ(prov::declared_interface_name("int x; // not a module at all\n"), ""); +} + +TEST(InterfaceUnit, TheFirstDeclarationWins) { + // A file names one module; anything after the first declaration is that + // module's body, and a quoted declaration inside it is text. + EXPECT_EQ(prov::declared_interface_name( + "export module first;\n/* export module second; */\n"), + "first"); +} diff --git a/tests/unit/test_runtime_search.cpp b/tests/unit/test_runtime_search.cpp index bd43b8fe..0aeac2b2 100644 --- a/tests/unit/test_runtime_search.cpp +++ b/tests/unit/test_runtime_search.cpp @@ -44,7 +44,7 @@ TEST(RuntimeSearch, MachineLocalIsEverythingButTheHostDefaults) { EXPECT_TRUE(search::is_machine_local(Origin::SubosFarm)); EXPECT_FALSE(search::is_machine_local(Origin::HostDefault)); - // ⚠️ `$ORIGIN` is NOT machine-local, and getting this backwards would be + // `$ORIGIN` is NOT machine-local, and getting this backwards would be // worse than the gap it closes: it is resolved by the loader relative to // the artifact, so it means the same thing wherever that artifact is // copied — `pack` REWRITES everything else into this form. Marking it diff --git a/tests/unit/test_scaffold.cpp b/tests/unit/test_scaffold.cpp index 0299c23a..222ce45a 100644 --- a/tests/unit/test_scaffold.cpp +++ b/tests/unit/test_scaffold.cpp @@ -391,7 +391,7 @@ widget = "1.0.0" // ── [template.inject] self = false ─────────────────────────────────────────── // -// ⚠️ A TEMPLATE THAT MUST NOT DEPEND ON ITS OWN PACKAGE IS NOT HYPOTHETICAL. +// A TEMPLATE THAT MUST NOT DEPEND ON ITS OWN PACKAGE IS NOT HYPOTHETICAL. // // The self-dependency keeps a template from drifting from the library that // ships it, and for almost every template that is right. It is wrong for one diff --git a/tests/unit/test_schedule_policy.cpp b/tests/unit/test_schedule_policy.cpp index cdc25b2e..496e3cdf 100644 --- a/tests/unit/test_schedule_policy.cpp +++ b/tests/unit/test_schedule_policy.cpp @@ -129,7 +129,7 @@ TEST(SchedulePolicy, ZeroJobsStaysZeroRatherThanBecomingNonsense) { << "a zero cap silently means no bound at all: " << d.reason; } -// ⚠️ THE DEFAULT CONFIGURATION MUST STILL BE BOUNDED. +// THE DEFAULT CONFIGURATION MUST STILL BE BOUNDED. // // `resolve_jobs` returns 0 when the user passed neither `--jobs` nor // `[build] jobs` — "say nothing, leave the backend's default". For every other diff --git a/tests/unit/test_source_kind.cpp b/tests/unit/test_source_kind.cpp index b49c2b6d..30ef3f89 100644 --- a/tests/unit/test_source_kind.cpp +++ b/tests/unit/test_source_kind.cpp @@ -188,7 +188,7 @@ TEST(SourceKind, SameStemAcrossModuleExtensionsNeverCollides) { EXPECT_TRUE(seen.insert(n).second) << "collision: " << f << " -> " << n; } - // ⚠️ KNOWN GAP, deliberately pinned rather than asserted away: `foo.c` and + // KNOWN GAP, deliberately pinned rather than asserted away: `foo.c` and // `foo.cpp` in one directory have always shared `foo.o`. Fixing it renames // every C object, which is exactly the cache-layout change described // above and needs a cache-key revision to be safe. Tracked separately — @@ -240,6 +240,53 @@ TEST(SourceKind, ClassifiesDeviceTranslationUnits) { EXPECT_EQ(mcpp::classify("src/k.hip", t), SourceKind::Device); } +TEST(SourceKind, ClassifiesShaderAndKernelLanguages) { + // The criterion is "a separate compiler consumes it", not "NVIDIA ships + // it". Before this, the list was `.cu` and `.hip` alone, and a shader in a + // constrained glob was refused with "no role for the extension '.comp'" — + // so the file never reached MCPP_DEVICE_SOURCES and the rule package that + // exists to compile it was told there was nothing to compile. + auto t = mcpp::builtin_extension_table(); + for (const char* p : {"shaders/s.comp", "shaders/s.vert", "shaders/s.frag", + "shaders/s.geom", "shaders/s.tesc", "shaders/s.tese", + "shaders/s.mesh", "shaders/s.task", "shaders/s.rgen", + "shaders/s.rint", "shaders/s.rahit", "shaders/s.rchit", + "shaders/s.rmiss", "shaders/s.rcall", "shaders/s.glsl", + "shaders/s.hlsl", "kernels/k.cl", "kernels/k.metal"}) + EXPECT_EQ(mcpp::classify(p, t), SourceKind::Device) << p; +} + +TEST(SourceKind, ShaderExtensionsDoNotWidenTheDefaultGlob) { + // The whole safety argument for widening the table rests on this: the + // default globs are unchanged, so no published package starts compiling a + // vendored shader on the next upgrade. Asserted over the WHOLE list rather + // than the two names that were there before, so a future addition cannot + // pass this file while changing what a default build compiles. + const auto globs = mcpp::default_source_globs(mcpp::builtin_extension_table()); + const std::vector expected{ + "src/**/*.cppm", "src/**/*.cpp", "src/**/*.cc", "src/**/*.c", + "src/**/*.S", "src/**/*.s", "src/**/*.asm", + }; + EXPECT_EQ(globs, expected); +} + +TEST(SourceKind, ShaderExtensionsCannotBeClaimedAsModuleInterfaces) { + // Reserved for the same reason `.cu` is: routing a shader to the C++ + // module rule fails somewhere that names neither the file nor the key. + for (const char* ext : {".comp", ".hlsl", ".cl", ".metal"}) { + auto err = mcpp::validate_module_extensions(std::vector{ext}); + ASSERT_TRUE(err.has_value()) << ext; + EXPECT_NE(err->find(ext), std::string::npos) << ext; + } +} + +TEST(SourceKind, ShaderObjectsUseTheCollisionProofName) { + // `scale.comp` and `scale.vert` are one rename apart in a shader + // directory, and both would be `scale.o` under the stem-named form. + EXPECT_EQ(mcpp::object_filename_for("shaders/scale.comp", ".o"), "scale.comp.o"); + EXPECT_EQ(mcpp::object_filename_for("shaders/scale.vert", ".o"), "scale.vert.o"); +} + TEST(SourceKind, DeviceHeadersAffectGraphShape) { // `.cuh` reaches a device TU through the preprocessor, so editing one can // change what the graph should be. Leaving it in `Other` is why a project diff --git a/tests/unit/test_symbol_provision.cpp b/tests/unit/test_symbol_provision.cpp index ba168955..b65028f4 100644 --- a/tests/unit/test_symbol_provision.cpp +++ b/tests/unit/test_symbol_provision.cpp @@ -68,7 +68,7 @@ TEST(SymbolProvision, CopyRelocatedDataIsNotAnExport) { } TEST(SymbolProvision, CopyRelocationsMatchByAddressNotByName) { - // ⚠️ MEASURED, on every mcpp binary: glibc's `environ` is a WEAK alias of + // MEASURED, on every mcpp binary: glibc's `environ` is a WEAK alias of // `__environ` at one address, and only `__environ` appears in `.rela.dyn`. // A name-keyed filter reports `environ` as a hijacked symbol in every // dynamically linked executable ever built. @@ -106,7 +106,7 @@ TEST(SymbolProvision, AnUnknownMachineDeclinesRatherThanGuessing) { // ── stage two ────────────────────────────────────────────────────────────── TEST(SymbolProvision, AnExportWithNoSecondProviderIsNotAConflict) { - // ⚠️ mcpp's OWN arrangement. A `kind = "shared"` dependency's link unit + // mcpp's OWN arrangement. A `kind = "shared"` dependency's link unit // takes only its own objects, so its static dependency lands in the // consumer's executable and the shared library binds back to it. One copy // in the process, entirely benign — and stage one alone would warn about diff --git a/tests/unit/test_targetside.cpp b/tests/unit/test_targetside.cpp index 466962c6..79ec84cc 100644 --- a/tests/unit/test_targetside.cpp +++ b/tests/unit/test_targetside.cpp @@ -440,7 +440,7 @@ TEST(TargetSideRequest, AFilledEnvSegmentStatesNothing) { EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); } -// ⚠️ REPORTED, NOT REFUSED. The graph supplies the C library either way, so the +// REPORTED, NOT REFUSED. The graph supplies the C library either way, so the // segment is ignored rather than violated and the artifact is identical with or // without it. Refusing was tried and broke every project spelling the host // target `x86_64-linux-gnu` — which is what `mcpp toolchain list` prints. @@ -470,12 +470,12 @@ TEST(TargetSideRequest, APrebuiltCLibraryIsWhatTheRequestSelected) { EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); } -// ⚠️ On Windows the same segment names the OBJECT ABI — `gnu` is PE with the +// On Windows the same segment names the OBJECT ABI — `gnu` is PE with the // GNU ABI, `msvc` is PE with Microsoft's — and both are compatible with more // than one C library. Reporting such a build as "asking for the `gnu` C ABI" // describes an axis the name never addressed, and the correction it suggested // named a target that does not exist. -// ⚠️ AND THE OBJECT-ABI AXIS REPORTS TOO, WHICH IS THE REVERSAL. +// AND THE OBJECT-ABI AXIS REPORTS TOO, WHICH IS THE REVERSAL. // // It was exempted on the grounds that `gnu` on Windows names the Itanium C++ // ABI rather than a C library. True, and incomplete: the segment bundles the @@ -495,7 +495,7 @@ TEST(TargetSideRequest, TheObjectAbiAxisReportsTheCLibraryHalfToo) { auto why = ts::check_request(ts::resolve(in)); ASSERT_NE(why, std::nullopt); EXPECT_NE(why->find("musl"), std::string::npos) << *why; - // ⭐ And it says the ABI half was honoured, so the reader does not conclude + // And it says the ABI half was honoured, so the reader does not conclude // the object ABI changed as well. EXPECT_NE(why->find("object ABI"), std::string::npos) << *why; EXPECT_NE(why->find("--target x86_64-windows"), std::string::npos) << *why; @@ -515,13 +515,13 @@ TEST(TargetSideRequest, TheObjectFormatAxisStaysExempt) { EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); } -// ⭐ AND WHEN IT DOES NOT NAME ONE, THE REPORT SAYS WHAT IT DOES NAME. +// AND WHEN IT DOES NOT NAME ONE, THE REPORT SAYS WHAT IT DOES NAME. // // Staying silent is correct as a DIAGNOSTIC and insufficient as a REPORT. The // reader sees `x86_64-windows-gnu` above a line reading `c-abi musl`, finds no // row called `gnu`, and maps it to the nearest thing that looks like a C // library name. Measured twice, by the same reader, on two different days. -// ⚠️ THE GLOSS IS NOW THE OBJECT-FORMAT AXIS ALONE. The object-ABI axis warns +// THE GLOSS IS NOW THE OBJECT-FORMAT AXIS ALONE. The object-ABI axis warns // instead (see `TheObjectAbiAxisReportsTheCLibraryHalfToo`), and leaving both // in place would state one finding twice — once as an aside, once as a warning. TEST(TargetSideReport, TheObjectAbiAxisIsNoLongerGlossed) { @@ -553,7 +553,7 @@ TEST(TargetSideReport, OnBareMetalTheSegmentNamesTheObjectFormat) { std::string::npos) << r; } -// ⚠️ AND IT DOES NOT FIRE WHEN THE C LIBRARY CAME FROM A PAYLOAD. +// AND IT DOES NOT FIRE WHEN THE C LIBRARY CAME FROM A PAYLOAD. // // A payload C library IS what the triple selected — the triple is how it was // selected — so `gnu → ucrt` follows visibly and a gloss would be noise on @@ -603,7 +603,7 @@ TEST(TargetSideRequirements, ARequirementIsCheckedAgainstWhatResolved) { EXPECT_NE(why->find("default = \"llvm\""), std::string::npos) << "a diagnostic that names no next step is a diagnostic the reader " "must still go and research"; - // ⚠️ AND THE STEP IT NAMES MUST NOT BE A GLOBAL ONE. Until 2026.8.26.2 the + // AND THE STEP IT NAMES MUST NOT BE A GLOBAL ONE. Until 2026.8.26.2 the // first remedy offered was `mcpp toolchain default llvm` — the default for // every project on the machine, changed because ONE project's dependency // asked. mcpp now applies the graph's requirement itself wherever its own @@ -657,7 +657,7 @@ TEST(TargetSideConflict, TwoSuppliersAreNamedTogetherWithHowEachArrived) { // ── Which layer decides the payload's C-library flags ─────────────────────── // -// ⚠️ THE PREDICATE THIS REPLACES WAS AN `OR` OVER TWO LAYERS, AND SHIPPED. +// THE PREDICATE THIS REPLACES WAS AN `OR` OVER TWO LAYERS, AND SHIPPED. // // bool system_from_graph() const { // return kernelAbi.fromGraph() || cAbi.fromGraph(); @@ -672,7 +672,7 @@ TEST(TargetSideConflict, TwoSuppliersAreNamedTogetherWithHowEachArrived) { // error: hermetic link check failed // crt1.o (bare name — the linker cannot resolve it) // -// ⭐ ONE TEST PER `Origin`, BECAUSE THE QUESTION HAS ONE ANSWER PER VALUE. +// ONE TEST PER `Origin`, BECAUSE THE QUESTION HAS ONE ANSWER PER VALUE. // A predicate written as a list of cases answers the ones its author thought // of; four tests against a four-valued enum make the fifth value's absence // visible when someone adds it. diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index ec6af7b9..3e609f61 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -92,7 +92,7 @@ TEST(TripleRequest, TheOnlySupportedSiblingIsTaken) { auto r = triple::resolve_request(*parse("aarch64-linux")); EXPECT_EQ(r.triple.str(), "aarch64-linux-musl"); EXPECT_TRUE(r.completedFromVocabulary); - // ⚠️ mcpp CHOOSING A ROW IS NOT THE PROJECT NAMING A C LIBRARY. `envExplicit` + // mcpp CHOOSING A ROW IS NOT THE PROJECT NAMING A C LIBRARY. `envExplicit` // feeds the request/fact comparison and the report's display name; setting // it here would make mcpp compare its own answer against itself. EXPECT_FALSE(r.triple.envExplicit); @@ -307,7 +307,7 @@ const std::string kNewlib = "xim:newlib-riscv@4.4"; const std::string kEmpty = ""; // -// ⚠️ The absent/empty distinction is the whole point of these three tests. A +// The absent/empty distinction is the whole point of these three tests. A // plain `std::string` would make "the project said nothing" and "the project // asked for no C library" the same value, and a kernel project would silently // get picolibc back. @@ -365,7 +365,7 @@ TEST(Triple, LlvmTripleWindowsGnu) { EXPECT_EQ(t->llvm_triple(""), "x86_64-w64-windows-gnu"); } -// ⚠️ `aarch64` becomes `arm64` and the version is appended. A build that +// `aarch64` becomes `arm64` and the version is appended. A build that // emitted mcpp's own spelling produced `--target=aarch64-macos`, which clang // accepts as a triple it has never heard of and then treats as bare-metal // aarch64 — the module and its importers then agree with each other and with @@ -394,7 +394,7 @@ TEST(Triple, LlvmTripleFreestandingIsUnchanged) { // ── The object format each target has, asked of the triple rather than of the // machine running the build. -// ⚠️ The artefact-format decision used to test the triple for the substrings +// The artefact-format decision used to test the triple for the substrings // `apple` and `darwin`. Those are LLVM's words; mcpp's canonical form is // `aarch64-macos`, which contains neither — so the test fell through to a // question about the HOST, and produced opposite errors on opposite hosts: an @@ -427,7 +427,7 @@ TEST(Triple, OsFieldIdentifiesFreestanding) { // ── The env segment may be declined on every platform ──────────────────────── // -// ⚠️ `x86_64-linux` parsed and `x86_64-windows` did not. The rule "a target +// `x86_64-linux` parsed and `x86_64-windows` did not. The rule "a target // triple states a REQUEST, and a request must be able to say nothing" therefore // held on two platforms out of four, and the two where it did not were exactly // the ones whose segment names something other than a C library — so a user was @@ -464,7 +464,7 @@ TEST(Triple, WritingTheSegmentOutIsStillARequest) { EXPECT_TRUE(m->envExplicit); } -// ⚠️ THE FILL IS `gnu` AND NOT THE HOST'S OWN ENV. +// THE FILL IS `gnu` AND NOT THE HOST'S OWN ENV. // // `host_triple()` answers `msvc` on a Windows machine. Filling from it would // give one command a different identity — a different output directory and @@ -484,7 +484,7 @@ TEST(Triple, MacosCarriesNoSegmentToDecline) { EXPECT_FALSE(t->envExplicit); } -// ⚠️ NO TWO ROWS MAY SHARE A CANONICAL NAME, AND THIS WAS NOT A HYPOTHETICAL. +// NO TWO ROWS MAY SHARE A CANONICAL NAME, AND THIS WAS NOT A HYPOTHETICAL. // // Adding `x86_64-windows-musl` and later correcting its `pin` column produced // TWO rows with that name — the edit inserted a corrected row without removing @@ -492,7 +492,7 @@ TEST(Triple, MacosCarriesNoSegmentToDecline) { // was correct and nothing failed; what the table carried was a second row of // dead data whose columns disagreed with the live one. // -// ⭐ Caught by reading the diff, which is the wrong mechanism: a duplicate is a +// Caught by reading the diff, which is the wrong mechanism: a duplicate is a // property of the table and a machine can see it. The cost of the check is four // lines. TEST(Triple, TheTargetTableHasNoDuplicateNames) { diff --git a/tests/unit/test_windows_defaults.cpp b/tests/unit/test_windows_defaults.cpp index e37e354f..e7025330 100644 --- a/tests/unit/test_windows_defaults.cpp +++ b/tests/unit/test_windows_defaults.cpp @@ -81,7 +81,7 @@ TEST(WindowsDefaults, OriginClassification) { EXPECT_FALSE(tc_origin_is_user_explicit(TcOrigin::GraphRequirement)); } -// ⚠️⚠️ AND IT IS THE ONE ORIGIN THAT MAY NEVER BECOME THE MACHINE'S DEFAULT. +// AND IT IS THE ONE ORIGIN THAT MAY NEVER BECOME THE MACHINE'S DEFAULT. // // Two branches persist one: the Windows first-run diversion, whose condition is // `tcSpec.has_value()`, and the MSVC repair, whose gate is "mcpp chose this @@ -89,7 +89,7 @@ TEST(WindowsDefaults, OriginClassification) { // — so a bare Windows box building ONE llvm-requiring project would have handed // llvm to every later project that asked for nothing. // -// ⭐ THIS IS THE ONLY PLACE THE INVARIANT CAN BE MEASURED WITHOUT A BARE +// THIS IS THE ONLY PLACE THE INVARIANT CAN BE MEASURED WITHOUT A BARE // WINDOWS MACHINE. The e2e that checks `config.toml`'s sha256 runs where a // toolchain is already configured, so it never reaches either branch; here the // rule itself is the subject.