Skip to content

Commit 6e2cefe

Browse files
committed
Merge branch 'feat/eight-t3' into feat/eight-reports
2 parents 35dcd1e + c28b8d1 commit 6e2cefe

12 files changed

Lines changed: 465 additions & 26 deletions

‎docs/specs/build-database.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ mcpp 输出的 S1 文档满足 S1 等级 2,不输出 `ide.options`。等级 3
100100

101101
### 3.3 翻译单元
102102

103-
- **R3.7** 除 NASM 单元外,构建计划中的每个编译单元是一个翻译单元。`source`、
103+
- **R3.7** 除 NASM 单元与规则声明的设备源文件(`SourceKind::Device`)外,构建计划中
104+
的每个编译单元是一个翻译单元;两者都不在 S1 文档与 `compile_commands.json` 中
105+
出现,但原因不同——NASM 单元是构建计划的编译单元,只是被逐出翻译单元的集合;
106+
设备源文件从不是构建计划的编译单元(引擎对其扩展名没有编译规则,能编译它的只有
107+
包自己的构建程序,通过一个动作),因而也从不进入这一集合。`source`、
104108
`work-directory`、`arguments`、`object` 与 `compile_commands.json` 中对应条目的
105109
`file`、`directory`、`arguments`、`output` 取自同一条记录,因而逐字相同。
106110
`work-directory` 是编译器实际运行的目录——即输出目录
@@ -170,7 +174,10 @@ mcpp 输出的 S1 文档满足 S1 等级 2,不输出 `ide.options`。等级 3
170174
的成员中,构建程序失败的包被描述为不含该程序产生的指令(清单自身的配置、工具链、
171175
模块图与标准库单元仍照常描述),`diagnostics` 另有一条 `error`,
172176
`MCPP_BUILD_DATABASE_PROGRAM_FAILED`,`path` 为该包的 `build.mcpp`;后续失败若是
173-
由缺失的指令引起,则按前一条规则使整个成员失败。只要 `diagnostics` 中有一条
177+
由缺失的指令引起,则按前一条规则使整个成员失败。一项检查若以构建程序的指令为
178+
前提(例如"每个设备源文件都被某个动作消费"),对本轮构建程序失败的包不运行:
179+
该包已经带着这一条 `PROGRAM_FAILED` 诊断被描述,不应因指令缺失这一后果本身被
180+
判成第二个失败,把真正的诊断挤出信封。只要 `diagnostics` 中有一条
174181
`error`,退出码就是 1,无论 `data` 是否出现。**已实现**(离线诊断码:
175182
mcpp >= 2026.9.16.1;成员独立规划、`path` 与构建程序失败的描述:mcpp >= 2026.9.26.2)
176183
- **R5.3** 信封的 `effects` 为 `read-project` 与 `write-global-cache`,运行了构建程序时

‎src/build/plan.cppm‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1854,8 +1854,24 @@ make_plan(const mcpp::manifest::Manifest& manifest,
18541854
};
18551855

18561856
// 1. Compile units in topological order
1857+
//
1858+
// A device-kind source (`SourceKind::Device`) is not one of them. The
1859+
// engine has no compile rule for it — it is compiled, if at all, by the
1860+
// package's build program through an action (`mcpp::action`), never by a
1861+
// `cxx_object`/`c_object` edge — so turning it into a `CompileUnit` here
1862+
// gave every consumer of `plan.compileUnits` a unit nothing runs: a dead
1863+
// `cxx_object` edge in `build.ninja` with no consumer, and an entry in
1864+
// `compile_commands.json` and the S1 document naming a compiler that never
1865+
// ran on the file (design 2026-09-27 §4.1, mcpp#724). Fixed at the source,
1866+
// once, so ninja, the compile database and S1 agree without each needing
1867+
// its own filter for this kind. The file itself still reaches `watch`
1868+
// (build_database.cppm expands the sources glob directly, not through
1869+
// `plan.compileUnits`), and it still reaches the package's build program
1870+
// through `MCPP_DEVICE_SOURCES` (features.cpp), which is the one thing
1871+
// that does compile it.
18571872
for (auto idx : topoOrder) {
18581873
auto& u = graph.units[idx];
1874+
if (u.kind == mcpp::SourceKind::Device) continue;
18591875
CompileUnit cu;
18601876
cu.source = u.path;
18611877
cu.packageName = u.packageName;

‎src/build/prepare.cppm‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,4 +725,27 @@ prepare_build(bool print_fingerprint, bool includeDevDeps = false,
725725
std::vector<mcpp::manifest::Target> extraTargets = {},
726726
BuildOverrides overrides = {});
727727

728+
// The PlanNotes a failed call recorded before the phase that failed it.
729+
//
730+
// On success, `prepare_build` copies `PrepareState::planNotes` into
731+
// `BuildContext::planNotes` (phase13_finish) — but on failure it returns only
732+
// `r.error()`, a plain string, and the `PrepareState` that held the notes is a
733+
// local of `prepare_build` and is gone the moment it returns. A note recorded
734+
// by an earlier phase (`MCPP_BUILD_DATABASE_PROGRAM_FAILED`, say) was
735+
// therefore lost on every failure of a later phase, not only the one that
736+
// motivated this (design 2026-09-27 §4.2, mcpp#724 side finding A, fix item
737+
// 2): under `emit build-database`'s `plan_only`, a member the caller could
738+
// otherwise describe the ordinary way (R5.2) instead reported nothing but the
739+
// later phase's own message.
740+
//
741+
// Same per-run-sink discipline as `mcpp::build::refusal` (refusal.cppm), and
742+
// for the same reason: widening `prepare_build`'s return type would touch
743+
// every caller of `.error()` to carry something only the failure path of one
744+
// caller (`emit`) reads. Written immediately before prepare_build's own
745+
// failing return, from the state that failure saw; read by the caller that
746+
// turns that failure into diagnostics. `take` reads and clears, so neither a
747+
// later failure of the SAME call nor a later, unrelated call inherits a stale
748+
// set of notes.
749+
export std::vector<PlanNote> take_notes_on_failure();
750+
728751
} // namespace mcpp::build

‎src/build/prepare/driver.cpp‎

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ import mcpp.platform;
2424

2525
namespace mcpp::build {
2626

27+
namespace {
28+
// `thread_local` for the same reason `mcpp::build::refusal`'s sink is
29+
// (refusal.cppm): `prepare_build` recurses for nested host sub-builds on the
30+
// calling thread, and a failure of the INNER call must not leave notes behind
31+
// for an outer call that goes on to succeed. Cleared at the top of every
32+
// `prepare_build` call and on its success path, so only a call that is
33+
// itself failing can leave something here for its caller to take.
34+
thread_local std::vector<PlanNote> g_notesOnFailure;
35+
} // namespace
36+
37+
std::vector<PlanNote> take_notes_on_failure() {
38+
auto notes = std::move(g_notesOnFailure);
39+
g_notesOnFailure.clear();
40+
return notes;
41+
}
42+
2743
std::expected<BuildContext, std::string>
2844
prepare_build(bool print_fingerprint,
2945
bool includeDevDeps,
@@ -32,18 +48,29 @@ prepare_build(bool print_fingerprint,
3248
PrepareState state(print_fingerprint, includeDevDeps,
3349
std::move(extraTargets), std::move(overrides));
3450
pending_flag_words_notes().clear();
51+
g_notesOnFailure.clear();
52+
53+
// Every early return below carries `state.planNotes` as they stood at the
54+
// failing phase, so a caller whose only handle on the failure is
55+
// `.error()` (a plain string) can still read what an earlier phase
56+
// recorded — see `take_notes_on_failure`'s declaration in prepare.cppm.
57+
auto fail = [&](std::string message) -> std::unexpected<std::string> {
58+
g_notesOnFailure = state.planNotes;
59+
return std::unexpected(std::move(message));
60+
};
3561

36-
if (auto r = phase0_manifest_and_workspace(state); !r) return std::unexpected(r.error());
37-
if (auto r = phase1_toolchain_spec_and_axes(state); !r) return std::unexpected(r.error());
38-
if (auto r = phase2_define_toolchain_resolver(state); !r) return std::unexpected(r.error());
39-
if (auto r = phase3_xlings_before_graph(state); !r) return std::unexpected(r.error());
40-
if (auto r = phase4a_graph_load(state); !r) return std::unexpected(r.error());
41-
if (auto r = phase4b_graph_worklist(state); !r) return std::unexpected(r.error());
42-
if (auto r = phase5_toolchain_after_graph(state); !r) return std::unexpected(r.error());
43-
if (auto r = phase6_features_and_host_tools(state); !r) return std::unexpected(r.error());
44-
if (auto r = phase9_target_side(state); !r) return std::unexpected(r.error());
45-
if (auto r = phase11_scan(state); !r) return std::unexpected(r.error());
62+
if (auto r = phase0_manifest_and_workspace(state); !r) return fail(r.error());
63+
if (auto r = phase1_toolchain_spec_and_axes(state); !r) return fail(r.error());
64+
if (auto r = phase2_define_toolchain_resolver(state); !r) return fail(r.error());
65+
if (auto r = phase3_xlings_before_graph(state); !r) return fail(r.error());
66+
if (auto r = phase4a_graph_load(state); !r) return fail(r.error());
67+
if (auto r = phase4b_graph_worklist(state); !r) return fail(r.error());
68+
if (auto r = phase5_toolchain_after_graph(state); !r) return fail(r.error());
69+
if (auto r = phase6_features_and_host_tools(state); !r) return fail(r.error());
70+
if (auto r = phase9_target_side(state); !r) return fail(r.error());
71+
if (auto r = phase11_scan(state); !r) return fail(r.error());
4672

73+
g_notesOnFailure.clear();
4774
return phase13_finish(state);
4875
}
4976

‎src/build/prepare/features.cpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1785,6 +1785,12 @@ std::expected<void, std::string> phase6_features_and_host_tools(PrepareState& st
17851785
pkg.manifest.package.name, r.error()),
17861786
mcpp::wire::Severity::Error,
17871787
(pkg.root / "build.mcpp").string()});
1788+
// Same reason as the root's mirror of this in
1789+
// target_side.cpp: a later check whose premise is this
1790+
// program's directives (the device-source check) must be
1791+
// able to tell this package apart from one with no program
1792+
// at all.
1793+
state.programFailedPackages.insert(pkg.root.string());
17881794
continue;
17891795
}
17901796
return std::unexpected(std::format(

‎src/build/prepare/state.cppm‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,16 @@ struct PrepareState {
462462
std::vector<std::pair<std::string, std::string>> abiRequiresExceptions;
463463
std::map<std::string, std::vector<std::string>> capExclusive;
464464
std::map<std::string, std::vector<std::string>> deviceSourcesByPackage;
465+
// Keyed like `deviceSourcesByPackage`, by `pkg.root.string()` (root
466+
// package included: `packages[0].root == *root`). Holds a package whose
467+
// build program failed IN THIS PASS, under `plan_only` (`emit
468+
// build-database`) — the one case a failed program does not already end
469+
// the whole call (SPEC-005 R5.2, #699 item 2, E3). A check whose premise
470+
// is that program's directives must not run for such a package: with no
471+
// directives applied, every premise reads as unmet, which is a symptom of
472+
// the recorded `MCPP_BUILD_DATABASE_PROGRAM_FAILED`, not a second defect
473+
// (design 2026-09-27 §4.2, mcpp#724 side finding A).
474+
std::set<std::string> programFailedPackages;
465475
std::function<std::optional<std::string>()> checkVersionFloors;
466476
mcpp::targetside::TargetSide resolvedTargetSide;
467477
std::optional<std::size_t> cxxLayerProviderIndex;

‎src/build/prepare/target_side.cpp‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,10 @@ std::expected<void, std::string> phase9_target_side(PrepareState& state) {
16221622
state.planNotes.push_back({"MCPP_BUILD_DATABASE_PROGRAM_FAILED",
16231623
bp.error(), mcpp::wire::Severity::Error,
16241624
(*state.root / "build.mcpp").string()});
1625+
// Named so the device-source check below (and anything else whose
1626+
// premise is this program's directives) can tell a package whose
1627+
// program failed apart from one that simply has no program.
1628+
state.programFailedPackages.insert(state.root->string());
16251629
}
16261630
if (bp) {
16271631
// THE SAME RULE THE DEPENDENCIES ARE HELD TO, WITH THE ROOT AS A PARTY.
@@ -1764,8 +1768,18 @@ std::expected<void, std::string> phase9_target_side(PrepareState& state) {
17641768
// condition an action needs anyway -- one that compiles a file it does not
17651769
// declare as an input does not rerun when that file changes -- so a rule
17661770
// that satisfies it is a rule that rebuilds correctly.
1771+
//
1772+
// THE PREMISE OF THIS CHECK IS THE BUILD PROGRAM'S DIRECTIVES: an action
1773+
// consuming a device source is one such directive. A package whose program
1774+
// failed in this pass (`plan_only`, above) applied none of them, so every
1775+
// device source would read as an orphan -- not a second defect, only the
1776+
// shape the first one takes here. Such a package already carries its one
1777+
// diagnostic, `MCPP_BUILD_DATABASE_PROGRAM_FAILED`; this check does not run
1778+
// for it, exactly as SPEC-005 R5.2 now states (design 2026-09-27 §4.2,
1779+
// mcpp#724 side finding A).
17671780
for (std::size_t i = 0; i < state.packages.size(); ++i) {
17681781
auto const& pkg = state.packages[i];
1782+
if (state.programFailedPackages.contains(pkg.root.string())) continue;
17691783
auto dit = state.deviceSourcesByPackage.find(pkg.root.string());
17701784
if (dit == state.deviceSourcesByPackage.end() || dit->second.empty()) continue;
17711785
auto const& mm = (i == 0) ? *state.m : pkg.manifest;
@@ -1781,6 +1795,9 @@ std::expected<void, std::string> phase9_target_side(PrepareState& state) {
17811795
orphans += " " + rel + "\n";
17821796
if (orphans.empty()) continue;
17831797
std::error_code hasEc;
1798+
// The `programFailedPackages` skip above means this package's program,
1799+
// if it has one, ran and succeeded — `exists(build.mcpp)` here can no
1800+
// longer be true of a program that merely started and failed.
17841801
const bool hasProgram = std::filesystem::exists(pkg.root / "build.mcpp", hasEc)
17851802
|| !pkg.manifest.buildConfig.ruleModules.empty();
17861803
refusal::record(refusal::Code::DeviceSourceUnconsumed);

‎src/build/prepare/xlings.cpp‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -429,17 +429,24 @@ std::expected<void, std::string> phase3_xlings_before_graph(PrepareState& state)
429429
penv.workspace.emplace_back(entry.target, entry.pin());
430430
}
431431
}
432-
if (state.runtimeSelection.ownerRoot == state.workRoot) {
433-
mcpp::config::ensure_project_index_dir(
434-
**cfg2, state.workRoot, state.m->indices, penv);
435-
} else {
436-
if (!state.m->indices.empty())
437-
mcpp::config::ensure_project_index_dir(
438-
**cfg2, state.workRoot, state.m->indices, {});
439-
if (materializeRootRuntime)
440-
mcpp::config::ensure_project_index_dir(
441-
**cfg2, state.runtimeSelection.ownerRoot, {}, penv);
442-
}
432+
// `state.workRoot` is where THIS invocation writes (SPEC-005
433+
// R2.1's cache directory under `emit build-database`'s
434+
// `plan_only`, or a host tool's private store under tool
435+
// provisioning); `runtimeSelection.ownerRoot` is always the real
436+
// project root (`select_runtime`, `runtime_selection.cppm`),
437+
// regardless of either. The two used to disagree on where the
438+
// runtime-environment half (`penv`: deps/subos/workspace) of
439+
// `.mcpp/.xlings.json` belongs whenever they differ: the custom-
440+
// indices half already went to `workRoot`, but `penv` went to
441+
// `ownerRoot` — the actual project tree — which is exactly what
442+
// `emit` must never write into (design 2026-09-27 §4.3, mcpp#724
443+
// side finding B). Both halves now go to the one root this
444+
// invocation writes everything else to, `workRoot`; when it
445+
// equals `ownerRoot` (an ordinary rooted build) that is the same
446+
// write the `if` branch always made, so this is one call in
447+
// every case, not two.
448+
mcpp::config::ensure_project_index_dir(
449+
**cfg2, state.workRoot, state.m->indices, penv);
443450

444451
// `[xlings] deps` are DECLARED above and, until now, nothing
445452
// installed them (mcpp-index #281 §9).

‎src/cli/cmd_build.cppm‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,10 +407,28 @@ export int cmd_emit_build_database(const mcpplibs::cmdline::ParsedArgs& parsed)
407407
includeDevDeps,
408408
std::move(discovered->targets), mo);
409409
if (!ctx) {
410+
// A wholly-failed member contributes exactly one `error`
411+
// diagnostic, `path` its `mcpp.toml` (SPEC-005 R5.2) — that
412+
// invariant is kept exactly, so a note an earlier phase
413+
// recorded (most importantly
414+
// `MCPP_BUILD_DATABASE_PROGRAM_FAILED`) is folded into THIS
415+
// diagnostic's own message instead of becoming a diagnostic of
416+
// its own. Without it, a later phase's failure that follows
417+
// from the missing directives (SPEC-005 R5.2's own words) read
418+
// as a single, unexplained symptom, and the actual cause —
419+
// recorded, then discarded the moment `prepare_build` returned
420+
// — never reached the reader (design 2026-09-27 §4.2, mcpp#724
421+
// side finding A, fix item 2).
422+
std::string message = member.empty() ? ctx.error()
423+
: std::format("{}: {}", member, ctx.error());
424+
for (auto const& note : mcpp::build::take_notes_on_failure())
425+
message += note.path.empty()
426+
? std::format("\n earlier in this pass, {}: {}",
427+
note.code, note.message)
428+
: std::format("\n earlier in this pass, {} ({}): {}",
429+
note.code, note.path, note.message);
410430
diagnostics.push_back({plan_failure_code(), Severity::Error,
411-
member.empty() ? ctx.error()
412-
: std::format("{}: {}", member, ctx.error()),
413-
memberPath});
431+
std::move(message), memberPath});
414432
failedMemberRoots.push_back(memberRoot);
415433
continue;
416434
}

0 commit comments

Comments
 (0)