Skip to content

Commit 71d09f0

Browse files
committed
fix: Finished once per command, the host tool's sub-build states its own output, module maps rewritten when they differ, and a workspace addresses a member's outside source by its place in the workspace
1 parent fb14218 commit 71d09f0

9 files changed

Lines changed: 138 additions & 17 deletions

‎.agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,26 @@ command lines that T7's criteria compare.
597597
command file: the second CI round of #754 failed e2e 258, 262 and 848 on
598598
the Windows legs with `D8004: '/reference' requires an argument`. A unit
599599
test now writes both compilers' files and reads them back.
600+
- **The review of the pull request** (section 15.3) found five more places,
601+
each fixed with a unit test:
602+
- `mcpp run --format <name>` wrote `Finished` twice, once for the pack and
603+
once for the build it drives again to resolve the runner. `Finished` is
604+
now written once per command, in the progress model.
605+
- An eleventh drive site, the sub-build of a dependency's host tool inside
606+
planning (`prepare/features.cpp`), took the new default and was reported
607+
as lines of the command, while its caller folds its output into an error
608+
message. It states `Caller`.
609+
- An argument file holds the build directory's absolute path while its name
610+
hashes relative paths, so a moved or restored build directory kept the
611+
old file. Both module map files are now written whenever their content
612+
differs.
613+
- The root was recognised by the name `workspace`; a member of that name
614+
would have kept its BMIs at their names. A virtual root is recognised by
615+
`virtualRoot`.
616+
- A member's file inside another member's directory was addressed under
617+
the containing package, which the virtual root replaced when the selection
618+
did not hold that member. A workspace plan addresses such a file by its
619+
place in the workspace (`obj/<member>/__ws/<path>`).
600620
- **B2's predicate** is "ELF and not freestanding", read from the target
601621
triple with the host triple when the target is empty. Mach-O is excluded
602622
because its compilers default to PIC; WebAssembly refuses shared objects.
@@ -631,3 +651,12 @@ command lines that T7's criteria compare.
631651
on the host against this branch: 9 of 9 sections pass, xlings built from its
632652
source among them; against 2026.10.1.1 every CHANGE section fails.
633653

654+
### 15.3 Review
655+
656+
A review of the full diff, independent of the author, read the plan, backend,
657+
pack, cache and progress code against this record. Its three findings above
658+
medium confidence and its two below were confirmed in the code and fixed
659+
(section 15.1). It found the per-unit maps, their keys, the dyndep and staging
660+
paths, the cache population, both argument-file writers, the job count and the
661+
token reclaim consistent with sections 3 and 4.
662+

‎docs/specs/build-database.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,11 @@ mcpp 输出的 S1 文档满足 S1 等级 2,不输出 `ide.options`。等级 3
128128
之间保持稳定。**已实现**
129129
- **R3.8a** 一个模块名在一个程序之内标识一个模块,而一个配置可以包含多个程序
130130
(#732)。除根包之外,每个包的 BMI 都位于其所属包的子目录下(2026.10.1.2+,#751)。
131-
导入其他包模块的单元,以及提供这样一个模块的单元,其 `arguments` 带有构建所用的
132-
一份模块映射:GCC 为 `-fmodule-mapper=<映射文件>`(相对 `work-directory`),Clang
133-
与 MSVC 为一个参数文件 `@<绝对路径>/modmap/<包>-<哈希>.modmap`,其中每行一个
134-
`-fmodule-file=<名字>=<路径>` 或 `/reference <名字>=<路径>`。映射只列出该单元经其
131+
导入其他包模块的单元,其 `arguments` 带有构建所用的一份模块映射:GCC 为
132+
`-fmodule-mapper=<映射文件>`(相对 `work-directory`;提供这样一个模块的单元也带,
133+
因为 GCC 经映射写出 BMI),Clang 与 MSVC 为一个参数文件
134+
`@<绝对路径>/modmap/<包>-<哈希>.modmap`,其中每行一个 `-fmodule-file=<名字>=<路径>`
135+
或 `/reference <名字>=<路径>`。映射只列出该单元经其
135136
导入所能到达的模块,因此同一单元在一个配置的每一种成员选择下,`arguments` 逐字相同。
136137
`.modmap` 后缀与 CMake 模块映射相同,读者可据此把未展开的参数文件识别为模块机制。
137138
同一个名字由两个包提供时,两个单元的 `provides` 都列出它;只按名字在文档中查找提供方

‎src/build/ninja_backend.cppm‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4733,22 +4733,27 @@ std::unique_ptr<Backend> make_ninja_backend() {
47334733
}
47344734

47354735
// The module maps of the units that reach a BMI below its provider's directory
4736-
// (B1). The key in a file's name is a hash of what the file holds, so a file
4737-
// that exists is already right, and a changed resolution names a new file.
4736+
// (B1). The key in a file's name is a hash of the relative paths the file
4737+
// holds, so it is the same in every build directory. The argument file also
4738+
// holds the build directory's absolute path, so a file is written whenever
4739+
// what it holds differs from what is there: a build directory that was moved
4740+
// or restored at another path would otherwise keep pointing at the old one.
47384741
void write_module_maps(const BuildPlan& plan) {
47394742
const bool msvc = msvc_module_spelling(plan);
4740-
for (auto const& [key, scope] : plan.moduleScopes) {
4743+
auto write_if_changed = [](const std::filesystem::path& path, const std::string& text) {
47414744
std::error_code ec;
4742-
const auto path = plan.outputDir / scope.mapFile;
4743-
if (!std::filesystem::exists(path, ec)) {
4744-
std::filesystem::create_directories(path.parent_path(), ec);
4745-
std::ofstream(path, std::ios::binary | std::ios::trunc) << scope.content;
4745+
if (std::filesystem::exists(path, ec)) {
4746+
std::ifstream in(path, std::ios::binary);
4747+
if (std::string(std::istreambuf_iterator<char>(in), {}) == text) return;
47464748
}
4747-
if (scope.argsFile.empty()) continue;
4748-
const auto args = plan.outputDir / scope.argsFile;
4749-
if (std::filesystem::exists(args, ec)) continue;
4750-
std::ofstream(args, std::ios::binary | std::ios::trunc)
4751-
<< module_map_arguments(scope.arguments, msvc);
4749+
std::filesystem::create_directories(path.parent_path(), ec);
4750+
std::ofstream(path, std::ios::binary | std::ios::trunc) << text;
4751+
};
4752+
for (auto const& [key, scope] : plan.moduleScopes) {
4753+
write_if_changed(plan.outputDir / scope.mapFile, scope.content);
4754+
if (!scope.argsFile.empty())
4755+
write_if_changed(plan.outputDir / scope.argsFile,
4756+
module_map_arguments(scope.arguments, msvc));
47524757
}
47534758
}
47544759

‎src/build/plan.cppm‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1954,6 +1954,19 @@ make_plan(const mcpp::manifest::Manifest& manifest,
19541954
};
19551955
auto outside_prefix = [&](const std::filesystem::path& src)
19561956
-> std::filesystem::path {
1957+
// IN A WORKSPACE PLAN, BY ITS PLACE IN THE WORKSPACE (pack drive and
1958+
// selection design 2026-10-01, B1). The package that contains the file
1959+
// may be a member the selection does not hold, and then the virtual
1960+
// root contains it instead: a member that lists a file of another
1961+
// member's directory had one address under `-p` and another under
1962+
// `--workspace`. The workspace's directory is the same in every
1963+
// selection.
1964+
if (manifest.package.virtualRoot) {
1965+
std::error_code ec;
1966+
auto rel = std::filesystem::relative(src, projectRoot, ec);
1967+
if (!ec && !rel.empty() && !rel.generic_string().starts_with(".."))
1968+
return std::filesystem::path("__ws") / safe_object_prefix({}, rel.parent_path());
1969+
}
19571970
if (auto c = container_of(src)) {
19581971
std::error_code ec;
19591972
auto rel = std::filesystem::relative(src, packages[*c].root, ec);
@@ -3275,7 +3288,10 @@ make_plan(const mcpp::manifest::Manifest& manifest,
32753288
// after every producer of a compile unit (a target's `main` included).
32763289
{
32773290
const auto traits = mcpp::toolchain::bmi_traits(tc);
3278-
const auto rootPackage = qualified_package_name(manifest);
3291+
// A workspace plan's root is virtual and provides nothing, so every
3292+
// member is placed below its own directory, whatever it is named.
3293+
const auto rootPackage = manifest.package.virtualRoot
3294+
? std::string{} : qualified_package_name(manifest);
32793295
auto basename = [&](std::string_view name) {
32803296
std::string out;
32813297
for (char c : name) out.push_back(c == ':' ? '-' : c);

‎src/build/prepare/features.cpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,6 +1727,10 @@ step6_build_tool(PrepareState& state, HostToolCtx& ctx) {
17271727
auto be = mcpp::build::make_ninja_backend();
17281728
mcpp::build::BuildOptions bopt;
17291729
bopt.ninjaTargets = { goal.generic_string() };
1730+
// A build inside planning: its output is this function's error message,
1731+
// not lines of the command's report (pack drive and selection design
1732+
// 2026-10-01, A2).
1733+
bopt.report = mcpp::build::BuildOptions::Report::Caller;
17301734
// Unfiltered inner output on demand: the filter drops
17311735
// ninja's own progress and command echoes, which is right
17321736
// for a normal build and wrong when the question is "what

‎src/build/progress.cppm‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ void programs_done();
220220
void checking();
221221

222222
// `Finished` (design §4.5): the whole command's time, and how it was spent.
223+
// Written once per command; a later call does nothing.
223224
void finished(std::string_view profile, std::string_view descriptor);
224225
// A command that builds several configurations writes one `Finished`, after
225226
// all of them: `finished` then only records what it was given, and
@@ -739,6 +740,8 @@ struct Report {
739740
bool failureReported = false;
740741
bool deferred = false;
741742
std::optional<std::pair<std::string, std::string>> deferredFinish;
743+
// `Finished` was written: a command states it once.
744+
bool finishedWritten = false;
742745
// The status row's screen (revision 3, §5.9 to §5.13): an animation fed
743746
// by the build, or none.
744747
std::unique_ptr<screen::Animation> animation;
@@ -1362,6 +1365,13 @@ void finished(std::string_view profile, std::string_view descriptor) {
13621365
r.deferredFinish.emplace(std::string(profile), std::string(descriptor));
13631366
return;
13641367
}
1368+
// ONCE PER COMMAND. `mcpp run --format <name>` packs, which states its
1369+
// build and writes `Finished`, and then drives the build again to
1370+
// resolve the runner, a scan that finds nothing to do and would write
1371+
// a second `Finished` counting the first's time again (pack drive and
1372+
// selection design 2026-10-01, A3).
1373+
if (r.finishedWritten) return;
1374+
r.finishedWritten = true;
13651375
// The breakdown and the longest step explain a wait; a command shorter
13661376
// than a minute has none worth a longer line (build output design
13671377
// revision 3, §7.3).

‎tests/unit/test_build_progress.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,3 +475,16 @@ TEST(ProgressModel, AStepWhoseEntriesAreReadInTwoPiecesCountsOnce) {
475475
auto out = testing::internal::GetCapturedStderr();
476476
EXPECT_EQ(count(out, "Compiling lib"), 1u) << out;
477477
}
478+
479+
// `Finished` is written once per command. `mcpp run --format <name>` packs, and
480+
// the pack states its build with `Finished`; the run then drives the same build
481+
// again, and its second `Finished` repeated the line with the first's time
482+
// counted twice (pack drive and selection design 2026-10-01, A3).
483+
TEST(ProgressModel, FinishedIsWrittenOncePerCommand) {
484+
mcpp::ui::disable_color();
485+
testing::internal::CaptureStderr();
486+
mcpp::build::progress::finished("release", "optimized");
487+
mcpp::build::progress::finished("release", "optimized");
488+
auto out = testing::internal::GetCapturedStderr();
489+
EXPECT_EQ(count(out, "Finished"), 1u) << out;
490+
}

‎tests/unit/test_module_address.cpp‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ TEST(ModuleAddress, ArgumentFilesAreWrittenAsEachCompilerReadsThem) {
334334
ms.arguments = {"/reference", "m=C:/build/ifc.cache/core/m.ifc",
335335
"/reference", "n=C:/build/ifc.cache/core/n.ifc"};
336336
msvc.moduleScopes.emplace("core-1", ms);
337+
// A file a moved or restored build directory left behind is rewritten.
338+
std::filesystem::create_directories(msvc.outputDir / "modmap");
339+
std::ofstream(msvc.outputDir / "modmap/core-1.modmap") << "/reference m=D:/old/m.ifc\n";
337340
write_module_maps(msvc);
338341
EXPECT_EQ(read(msvc.outputDir / "modmap/core-1.modmap"),
339342
"\xEF\xBB\xBF/reference m=C:/build/ifc.cache/core/m.ifc\n"

‎tests/unit/test_object_address.cpp‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,3 +415,43 @@ TEST(ObjectAddress, AMembersOutsideSourceIsImmuneToAnotherMemberListingIt) {
415415
// Filed under its declaring member, not at the root's flat address.
416416
EXPECT_NE(alone.generic_string().find("obj/core/"), std::string::npos) << alone;
417417
}
418+
419+
// The same for a file inside ANOTHER member's directory: the member that
420+
// contains it may be outside the selection, and then the virtual root contains
421+
// the file instead. The address is the file's place in the workspace.
422+
TEST(ObjectAddress, AMembersSourceInAnotherMembersDirectoryIsImmuneToTheSelection) {
423+
Tmp t;
424+
const auto ws = t.path / "ws";
425+
auto plan_with = [&](bool withLib) -> std::filesystem::path {
426+
mcpp::manifest::Manifest root;
427+
root.package.name = "workspace";
428+
root.package.version = "0.0.0";
429+
root.package.standard = "c++23";
430+
root.package.virtualRoot = true;
431+
std::vector<mcpp::modgraph::PackageRoot> packages;
432+
auto rootPkg = makePackage(ws, "workspace");
433+
rootPkg.manifest = root;
434+
packages.push_back(rootPkg);
435+
packages.push_back(makePackage(ws / "app", "app"));
436+
if (withLib) packages.push_back(makePackage(ws / "lib", "lib"));
437+
438+
mcpp::modgraph::Graph graph;
439+
graph.units.push_back(unitFor(ws / "app", "../lib/src/shared.c", "app"));
440+
if (withLib) graph.units.push_back(unitFor(ws / "lib", "src/lib.c", "lib"));
441+
std::vector<std::size_t> topo;
442+
for (std::size_t i = 0; i < graph.units.size(); ++i) topo.push_back(i);
443+
auto plan = make_plan(root, gccLike(), {}, graph, topo, packages, ws,
444+
ws / "target" / "t", {}, {}, {});
445+
EXPECT_TRUE(plan) << (plan ? std::string{} : plan.error());
446+
if (!plan) return {};
447+
for (auto const& cu : plan->compileUnits)
448+
if (cu.packageName == "app") return cu.object;
449+
ADD_FAILURE() << "app's unit is not in the plan";
450+
return {};
451+
};
452+
const auto alone = plan_with(false);
453+
const auto beside = plan_with(true);
454+
EXPECT_FALSE(alone.empty());
455+
EXPECT_EQ(alone, beside);
456+
EXPECT_NE(alone.generic_string().find("obj/app/__ws/lib/src/"), std::string::npos) << alone;
457+
}

0 commit comments

Comments
 (0)