Skip to content

Commit fcbfc68

Browse files
committed
the query answers why, and three tests asserted things that were not the claim
`compiler.chosenBy` carries the selection origin into the machine interface: a consumer asking why a build resolved llvm had to parse the status line, which is the substring matching that document exists to remove. Three test defects found by reading, not by failing: - the capability-row refusal offered "depend on a package that supplies this target's system" — a remedy a capability pin ignores by construction, and the sentence directly above it already said so. Split by row kind. - 303 read `suppliesTarget` through jq's `//`, which returns its right side when the left is null OR FALSE. `false` is an answer; it read as absent, and windows-x86_64 reported the field missing when it was there. - 281 asserted the old global remedy, which the only refusal that still reaches it cannot be fixed by. 303's third half stacks a musl c-abi over the host's own target and not every host stacks that; granted by reason, with linux-x86_64 as the denominator.
1 parent 3d59901 commit fcbfc68

10 files changed

Lines changed: 249 additions & 35 deletions

.github/workflows/ci-target-matrix.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,19 @@ jobs:
188188
"OK: a request that named no C library resolves to a row that exists" || fail=1
189189
check 300_a_registered_family_is_not_reported_unknown.sh \
190190
"OK: a registered family is not reported unknown" || fail=1
191-
check 303_the_query_gives_one_answer_for_the_c_library.sh \
192-
"OK: the query gives one answer for the C library" || fail=1
191+
# ⚠️ 303's third half stacks a musl c-abi over this host's own target,
192+
# and not every host stacks that — an MSVC-ABI host answers the
193+
# layering question first, correctly, and there is then no
194+
# two-answer document to check. Granted by reason; linux-x86_64 below
195+
# is the denominator that must run the whole file.
196+
if [ "${{ matrix.host }}" = linux-x86_64 ]; then
197+
check 303_the_query_gives_one_answer_for_the_c_library.sh \
198+
"OK: the query gives one answer for the C library" || fail=1
199+
else
200+
check_or_declared_skip 303_the_query_gives_one_answer_for_the_c_library.sh \
201+
"OK: the query gives one answer for the C library" \
202+
"refuses a musl c-abi over its own target" || fail=1
203+
fi
193204
194205
# ⚠️ 301/302 NEED TWO COMPILER FAMILIES, AND THAT IS A PROPERTY OF THE
195206
# MACHINE RATHER THAN OF THE CLAIM. "A requirement that DIFFERS from

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@
4545
| 已有 gcc 默认的机器 | 拒绝,要求改全局默认 | 装/用 llvm,`config.toml` 不动 |
4646
| 什么都没装的机器 | 装 gcc → 持久化 gcc → 再拒绝 | 首次运行分支根本不进,直接装 llvm |
4747

48+
⭐ 状态行点名是哪个包要求的、顶掉了什么;`why toolchain --format json` 新增
49+
`compiler.chosenBy = {origin, requiredBy, replaced}`,让「为什么是 llvm」不必去
50+
解析那行提示 —— 那正是机器接口存在的理由所要消除的字符串匹配。
51+
4852
拒绝只剩一种局面:工程自己在 `[toolchain]``[target.X]` 写下了相反的编译器。
4953
⚠️ 那种局面里全局默认与本次构建无关,因此原来那条 `mcpp toolchain default llvm`
5054
的建议**连问题都解决不了**,已改为指向那条陈述本身。两个包要求不同的族则是错误

docs/11-machine-output.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,28 @@ It resolves and reports; it does not build. `data` is:
262262
| `requested` | `{target, toolchain}` — what was asked for |
263263
| `status` | `ok` or `refused` |
264264
| `reason` | a refusal token, or `none` |
265-
| `compiler` | `{family, version, driver}` — the driver that would run |
265+
| `compiler` | `{family, version, driver, chosenBy}` — the driver that would run, and why |
266266
| `triple` | `{requested, toolchain, llvm}` |
267267
| `cLibrary` | `{mode, path, origin, suppliesTarget}``mode` is `sysroot` / `payload-first` / `none`; `origin` is `payload` / `subos` / `host` / `none` |
268268
| `layers[]` | the five target-side layers: `{layer, interface, impl, origin, subset}` |
269269

270+
**`compiler.chosenBy` answers "why this one".** `{origin, requiredBy,
271+
replaced}``origin` is the same phrase the build's status line uses
272+
(`[toolchain] in mcpp.toml`, `your default`, `target default`,
273+
`required by the dependency graph`, `first-run default`). `requiredBy` names the
274+
package when a `requires = ["mcpp:compiler=…"]` decided it, and `replaced` names
275+
the spec that was displaced; both are empty when nothing was.
276+
277+
```jsonc
278+
"compiler": { "family": "clang", "version": "22.1.8", "driver": "…/clang++",
279+
"chosenBy": { "origin": "required by the dependency graph",
280+
"requiredBy": "openkal-llvm-runtime@0.1.3",
281+
"replaced": "gcc@16.1.0" } }
282+
```
283+
284+
Without it a consumer asking *why* would have to parse the status line — the
285+
substring matching this document exists to remove.
286+
270287
⚠️ **`cLibrary` and `layers[].c-abi` answer two questions, and `suppliesTarget`
271288
says which one governs.** `cLibrary` describes the *payload's* link model — the
272289
search paths a payload-supplied C library would use. `layers[].c-abi` describes

docs/zh/11-machine-output.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,27 @@ mcpp why toolchain [--target <triple>] [--toolchain <spec>] --format json
231231
| `requested` | `{target, toolchain}` —— 问的是什么 |
232232
| `status` | `ok``refused` |
233233
| `reason` | 拒绝的记号,或 `none` |
234-
| `compiler` | `{family, version, driver}` —— 真正会跑的驱动器 |
234+
| `compiler` | `{family, version, driver, chosenBy}` —— 真正会跑的驱动器,以及为什么是它 |
235235
| `triple` | `{requested, toolchain, llvm}` |
236236
| `cLibrary` | `{mode, path, origin, suppliesTarget}`;`mode``sysroot` / `payload-first` / `none`,`origin``payload` / `subos` / `host` / `none` |
237237
| `layers[]` | 目标侧五层:`{layer, interface, impl, origin, subset}` |
238238

239+
**`compiler.chosenBy` 回答「为什么是它」。** `{origin, requiredBy, replaced}`
240+
—— `origin` 与构建状态行用的是同一句话(`[toolchain] in mcpp.toml`
241+
`your default``target default``required by the dependency graph`
242+
`first-run default`)。当某条 `requires = ["mcpp:compiler=…"]` 做了决定时,
243+
`requiredBy` 点名那个包,`replaced` 点名被顶掉的那个 spec;没有发生时两者都为空。
244+
245+
```jsonc
246+
"compiler": { "family": "clang", "version": "22.1.8", "driver": "…/clang++",
247+
"chosenBy": { "origin": "required by the dependency graph",
248+
"requiredBy": "openkal-llvm-runtime@0.1.3",
249+
"replaced": "gcc@16.1.0" } }
250+
```
251+
252+
没有它,要问「为什么」的消费方只能去解析状态行 —— 而那正是这份文档存在的理由所要
253+
消除的字符串匹配。
254+
239255
⚠️ **`cLibrary``layers[].c-abi` 回答的是两个问题,`suppliesTarget` 说明哪一个
240256
管用。** `cLibrary` 描述的是**载荷**的链接模型 —— 一份由载荷供给的 C 库会用到的
241257
搜索路径。`layers[].c-abi` 描述的是**这次构建**。当依赖供给 C 库时两者分叉,而在

src/build/prepare.cppm

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,19 @@ export struct BuildContext {
581581
// whether a cached build.ninja was generated for the profile being asked
582582
// for — and so `Finished <profile>` stops being a hardcoded "release".
583583
std::string profile;
584+
// ⭐ WHY THIS COMPILER — carried so the QUERY can answer it too.
585+
//
586+
// A build says so on its status line. `mcpp why toolchain --format json`
587+
// exists precisely to answer "what would this resolve to, and why", and a
588+
// consumer that had to parse the prose to learn that a dependency chose the
589+
// compiler would be doing the substring matching the machine interface was
590+
// introduced to remove.
591+
struct CompilerChoice {
592+
std::string origin; // tc_origin_name(): who decided
593+
std::string requiredBy; // the package, when the graph decided
594+
std::string replaced; // the spec displaced, when one was
595+
};
596+
CompilerChoice compilerChoice;
584597
// Resolved global-cache mode. Read side is honored in prepare_build; write
585598
// side in run_build_plan.
586599
CacheMode cacheMode = CacheMode::Global;
@@ -5408,31 +5421,54 @@ prepare_build(bool print_fingerprint,
54085421
// here — where both halves are known — beats a compiler
54095422
// complaining about a file the reader never named.
54105423
else if (tcOrigin == TcOrigin::TargetPin) {
5424+
// ⚠️⚠️ THE TWO ROWS REFUSE UNDER ONE RULE AND FOR TWO
5425+
// REASONS, AND ONE REMEDY DOES NOT SERVE BOTH.
5426+
//
5427+
// A CONVENTION pin is cancelled by a graph that supplies the
5428+
// target's system — that is `graphSuppliesSystem`, one
5429+
// screen up — so "depend on a package that supplies it" is
5430+
// exactly the way out.
5431+
//
5432+
// A CAPABILITY pin is not: `targetPinIsCapability` keeps it
5433+
// applied no matter what the graph supplies, because no
5434+
// other family emits the target at all. Offering the same
5435+
// remedy there prints an instruction that the sentence
5436+
// directly above it has already ruled out — the failure
5437+
// this release removes from `check_requirements`, reproduced
5438+
// three screens away.
5439+
std::string_view why = targetPinIsCapability
5440+
? "The row names its compiler as a capability: no other "
5441+
"family emits this target."
5442+
: "The row's payload is what supplies this target's "
5443+
"headers and C library,\n and nothing in the "
5444+
"dependency graph supplies them instead.";
5445+
std::string remedy = targetPinIsCapability
5446+
? std::format(
5447+
" Drop the package that requires `{}`, or "
5448+
"take a version of it built\n"
5449+
" for `{}`.",
5450+
reqCompiler, targetPinCandidate)
5451+
: std::format(
5452+
" Depend on a package that supplies this "
5453+
"target's system (its kernel\n"
5454+
" interface and C library) so the row's "
5455+
"payload is not needed, or drop\n"
5456+
" the package that requires `{}`.",
5457+
reqCompiler);
54115458
refusal::record(refusal::Code::CompilerRequirementConflict);
54125459
return std::unexpected(std::format(
54135460
"`{}` requires the compiler to be `{}`, and target '{}' "
54145461
"cannot be built with it here.\n"
54155462
" target row {:<14} ({})\n"
54165463
" required {:<14} (required by {})\n"
5417-
" {}\n"
5418-
" Depend on a package that supplies this target's "
5419-
"system (its kernel\n"
5420-
" interface and C library) so the row's payload is "
5421-
"not needed, or drop\n"
5422-
" the package that requires `{}`.",
5464+
" {}\n{}",
54235465
reqCompilerBy, reqCompiler,
54245466
targetRowName.empty() ? overrides.target_triple
54255467
: targetRowName,
54265468
targetPinCandidate,
54275469
targetPinIsCapability ? "capability" : "convention",
54285470
reqCompiler, reqCompilerBy,
5429-
targetPinIsCapability
5430-
? "The row names its compiler as a capability: no "
5431-
"other family emits this target."
5432-
: "The row's payload is what supplies this target's "
5433-
"headers and C library,\n and nothing in the "
5434-
"dependency graph supplies them instead.",
5435-
reqCompiler));
5471+
why, remedy));
54365472
}
54375473
// Free to take it. `tcSpec` is either absent (nothing configured
54385474
// anywhere) or one of mcpp's own remembered answers.
@@ -7301,6 +7337,10 @@ prepare_build(bool print_fingerprint,
73017337
ctx.runtimeSelection = runtimeSelection;
73027338
ctx.runtimeBinding = runtimeBindingSnapshot;
73037339
ctx.profile = effectiveProfile;
7340+
ctx.compilerChoice = { std::string(tc_origin_name(tcOrigin)),
7341+
graphCompilerRequiredBy,
7342+
graphCompilerReplaced.empty() ? pinReplacedDefault
7343+
: graphCompilerReplaced };
73047344
ctx.cacheMode = cacheMode;
73057345
ctx.projectRoot= *root;
73067346
ctx.outputDir = target_dir(*tc, fp, workRoot);

src/doctor.cppm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,10 +886,20 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec)
886886

887887
data["status"] = "ok";
888888
data["reason"] = "none";
889+
// ⭐ AND WHY THIS ONE. The build's status line says it; a consumer of the
890+
// machine interface asking "what would this resolve to, and why" would
891+
// otherwise have to parse that prose — the substring matching this document
892+
// exists to remove. `requiredBy` and `replaced` are empty unless something
893+
// was required and something was displaced.
889894
data["compiler"] = {
890895
{"family", std::string(tc.compiler_name())},
891896
{"version", tc.version},
892897
{"driver", tc.binaryPath.string()},
898+
{"chosenBy", {
899+
{"origin", ctx->compilerChoice.origin},
900+
{"requiredBy", ctx->compilerChoice.requiredBy},
901+
{"replaced", ctx->compilerChoice.replaced},
902+
}},
893903
};
894904
data["triple"] = {
895905
{"requested", std::string(target)},

tests/e2e/281_target_side_rules.sh

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,25 @@ out="$(MCPP_TOOLCHAIN=gcc@16.1.0 "$MCPP" build 2>&1)" || rc=$?
5454
[[ "$rc" -ne 0 ]] || { echo "a requirement gcc does not meet was accepted:"; echo "$out"; exit 1; }
5555
grep -q "requires the compiler to be" <<< "$out" || {
5656
echo "the refusal does not name the requirement:"; echo "$out"; exit 1; }
57-
grep -q "mcpp toolchain default" <<< "$out" || {
57+
# ⚠️ THE NEXT STEP MUST BE ONE THAT WOULD ACTUALLY WORK, AND UNTIL 2026.8.26.2
58+
# THE FIRST ONE OFFERED WAS `mcpp toolchain default llvm`.
59+
#
60+
# That is a GLOBAL change — the default for every project on the machine —
61+
# printed because ONE project's dependency asked. And since the graph's
62+
# requirement is now applied wherever mcpp's own answer was revisable, the only
63+
# way to reach this refusal is a compiler the project STATED (here,
64+
# `MCPP_TOOLCHAIN`). In that situation the global default is not what is being
65+
# used, so changing it fixes nothing: the advice has to name the statement that
66+
# decided.
67+
grep -q "remove it" <<< "$out" || {
5868
echo "the refusal names no next step:"; echo "$out"; exit 1; }
69+
# ⚠️ `if`, NOT `grep … && { … }`. Under `set -e` a trailing `&&` list whose
70+
# left side fails takes the script down — and here grep FAILING is the passing
71+
# case. This repo has paid for that shape more than once.
72+
if grep -q "mcpp toolchain default" <<< "$out"; then
73+
echo "the refusal offers a global change that would not fix this failure:"
74+
echo "$out"; exit 1
75+
fi
5976
# The evidence the decision rests on must be printed even though the compiler
6077
# layer comes from the payload and is suppressed in an ordinary report.
6178
grep -qE "^\s+compiler\s+gcc" <<< "$out" || {

tests/e2e/301_the_graphs_compiler_is_taken_and_nothing_is_written.sh

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,41 @@ else
113113
exit 1
114114
fi
115115

116-
# ── And the status line says who asked ───────────────────────────────────
116+
# ── And the query says who asked ─────────────────────────────────────────
117117
#
118118
# ⚠️ A COMPILER THE USER DID NOT NAME, REPORTED WITHOUT ITS REASON, IS A RULE
119119
# THAT CAN ONLY BE LEARNED BY EXPERIMENT. The same argument that put a reason on
120120
# the target row's substitution applies here, and more so: this one overrides a
121121
# value the user set with `mcpp toolchain default`.
122+
#
123+
# ⭐ FROM `compiler.chosenBy`, NOT FROM THE PROSE. A consumer asking "why this
124+
# compiler" reading the status line would be doing the substring matching the
125+
# machine interface exists to remove — and so would this test.
126+
by="$(printf '%s' "$j" | jq -r '.data.compiler.chosenBy.requiredBy // ""' | tr -d '\r')"
127+
was="$(printf '%s' "$j" | jq -r '.data.compiler.chosenBy.replaced // ""' | tr -d '\r')"
128+
case "$by" in
129+
needs-"$want"@*) echo " ok and the query names the package that asked ($by)" ;;
130+
"") echo "FAIL: compiler.chosenBy.requiredBy is empty — the document does not"
131+
echo " say a dependency decided this"; exit 1 ;;
132+
*) echo "FAIL: compiler.chosenBy.requiredBy is '$by', expected needs-$want@…"
133+
exit 1 ;;
134+
esac
135+
if [ -n "$was" ]; then
136+
echo " ok and names what it displaced ($was)"
137+
else
138+
echo "FAIL: compiler.chosenBy.replaced is empty, yet '$current' was displaced"
139+
exit 1
140+
fi
141+
142+
# ⭐ AND THE HUMAN-FACING LINE STILL CARRIES IT. The token is for programs; the
143+
# person running the build reads the status line, and a decision reported there
144+
# without its reason is the rule learned by experiment.
122145
out="$("$MCPP" build 2>&1 || true)"
123146
ok=1
124147
printf '%s\n' "$out" | grep -q "needs-$want" || ok=0
125148
printf '%s\n' "$out" | grep -q "mcpp:compiler=$want" || ok=0
126149
if [ "$ok" = 1 ]; then
127-
echo " ok and the status line names the package that asked"
150+
echo " ok and the status line names it too"
128151
else
129152
echo "FAIL: the resolution does not say who required this compiler"
130153
printf '%s\n' "$out" | head -6 | sed 's/^/ /'

tests/e2e/302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,48 @@ else
109109
exit 1
110110
fi
111111

112+
# ── Half three: a capability row's remedy is not a convention row's ──────
113+
#
114+
# ⚠️⚠️ THE TWO ROWS REFUSE UNDER ONE RULE AND FOR TWO REASONS, AND ONE REMEDY
115+
# DOES NOT SERVE BOTH.
116+
#
117+
# A convention pin is cancelled by a graph that supplies the target's system, so
118+
# "depend on a package that supplies it" is the way out. A capability pin is
119+
# not — it stays applied whatever the graph supplies, because no other family
120+
# emits the target. Printed there, that remedy is an instruction the sentence
121+
# directly above it has already ruled out.
122+
#
123+
# ⭐ FOUND BY READING THE MESSAGE, NOT BY A FAILING BUILD. This half exists so
124+
# the next rewording cannot put it back.
125+
#
126+
# ⚠️ The refusal is decided from the VOCABULARY (the row's pin) before any
127+
# payload is resolved, so this half is host-independent and needs nothing
128+
# installed.
129+
printf '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nneeds-gcc = { path = "../needs-gcc" }\n' \
130+
> mcpp.toml
131+
j3="$("$MCPP" why toolchain --target riscv64-none-elf --format json 2>/dev/null)"
132+
reason3="$(printf '%s' "$j3" | jq -r '.data.reason // "-"' | tr -d '\r')"
133+
msg3="$(printf '%s' "$j3" | jq -r '.diagnostics[].message' | tr -d '\r')"
134+
135+
if [ "$reason3" != compiler-requirement-conflict ]; then
136+
echo "FAIL: a capability row against a gcc requirement gave reason '$reason3'"
137+
printf '%s\n' "$msg3" | sed 's/^/ /'
138+
exit 1
139+
fi
140+
echo " ok a capability row refuses a requirement it cannot satisfy"
141+
142+
if printf '%s\n' "$msg3" | grep -qi 'capability'; then
143+
echo " ok and it says the pin is a capability"
144+
else
145+
echo "FAIL: the refusal does not say the row's pin is a capability"
146+
printf '%s\n' "$msg3" | sed 's/^/ /'
147+
exit 1
148+
fi
149+
if printf '%s\n' "$msg3" | grep -q "supplies this target's system"; then
150+
echo "FAIL: the remedy offers to supply the system, which a capability pin ignores"
151+
printf '%s\n' "$msg3" | sed 's/^/ /'
152+
exit 1
153+
fi
154+
echo " ok and it does not offer a remedy a capability pin ignores"
155+
112156
echo "OK: a stated compiler outranks the graph and two requirements do not stack"

0 commit comments

Comments
 (0)