Skip to content

Commit 101f6b8

Browse files
committed
review fixes: the tail prefilter, the scan pass's options, the xlings upgrade, the GCC map
- A `**/` also matches no directory, so the literal tail of `**/name` does not start with its slash; a root-level match of `**/main.cpp` was dropped. - Under -k the scans run in the main pass; the scan pass takes MCPP_NINJA_DEBUG, and -j precedes its goal. - The versions of the vendored xlings's candidate sources are read through the memo, and an upgrade copies beside the binary and renames over it, so a failed copy keeps the old binary. - The GCC module map also lists a name a unit imports that no unit provides, at GCC's own path; the clang and MSVC paths use native separators.
1 parent d0c1b6e commit 101f6b8

6 files changed

Lines changed: 96 additions & 23 deletions

File tree

‎.agents/docs/2026-09-30-build-wall-time-progress-count-and-hang-plan.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,23 @@ without a new staleness class.
972972
- Revert probes: the W1 property test, e2e 846 and e2e 847 each fail on
973973
2026.9.30.1.
974974

975+
### 10.5 Review of the implementation
976+
977+
An independent review of the pull request's diff found the following, each
978+
resolved as stated.
979+
980+
| Finding | Resolution |
981+
|---|---|
982+
| W8: the literal-tail prefilter dropped a root-level match of `**/name`, since `**/` also matches no directory | The `/` after a `**` is not part of the tail; `Scanner.ADoubleStarSlashTailMatchesAtTheRoot` |
983+
| W2: the scan pass took neither `-k` nor `MCPP_NINJA_DEBUG`, and put `-j` after its goal | Under `-k` the scans run in the main pass; `-d` is passed; `-j` precedes the goal |
984+
| W3: the candidates' versions were asked without the memo on the path where the vendored binary is behind the pin | They are read through the memo |
985+
| W3: a failed copy during an upgrade removed the working binary | The copy is written beside the binary and renamed over it; a failure keeps the old one |
986+
| W10: the GCC map listed only names a unit provides | A name a unit of the package imports that no unit provides is listed at GCC's own path |
987+
| W10: the clang and MSVC paths mixed separators | Native separators |
988+
| W10: the GCC map's relative path in the compile database | Not a defect: the database's directory is the build directory (`compile_commands.cppm:484`) |
989+
| W2: the phase is one value for the whole command, so two configurations built at once show the phase of the pass that began last | Recorded; cosmetic |
990+
| W8: a file symlink whose target is deleted during planning stays listed | Recorded; the per-pattern walk had the same window between its walk and its use |
991+
975992
## 8. Appendix: readings
976993

977994
- **Clean builds, wall time (s).**

‎src/build/ninja_backend.cppm‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4338,16 +4338,25 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
43384338
std::string out;
43394339
int ninjaExit = 0;
43404340
bool scanFailed = false;
4341-
if (goalArg.empty()
4341+
// Under `-k` the build goes on past a failure and states every one, so
4342+
// the scans run in the main pass there: a failed scan pass would end the
4343+
// build at its first failure, and running both would state a failed scan
4344+
// twice.
4345+
if (goalArg.empty() && !opts.keepGoing
43424346
&& manifest.find("\nbuild " + std::string(kScannedGoal) + " : phony") != std::string::npos) {
43434347
const auto scanDeadline =
43444348
std::chrono::milliseconds(static_cast<long long>(opts.buildTimeoutSecs) * 1000);
4349+
// The main pass's options, and its goal last.
43454350
std::vector<std::string> scan{ninjaProgram};
43464351
if (!opts.verbose && !opts.progress) scan.push_back("--quiet");
43474352
scan.insert(scan.end(), {std::string("-C"), plan.outputDir.string()});
43484353
if (opts.verbose) scan.push_back("-v");
4349-
scan.push_back(std::string(kScannedGoal));
4354+
if (const char* topics = std::getenv("MCPP_NINJA_DEBUG"); topics && *topics) {
4355+
scan.push_back("-d");
4356+
scan.push_back(topics);
4357+
}
43504358
if (opts.parallelJobs) scan.push_back(std::format("-j{}", opts.parallelJobs));
4359+
scan.push_back(std::string(kScannedGoal));
43514360
if (opts.progress) {
43524361
auto run = run_ninja_reporting(scan, nenv, scanDeadline, *opts.progress, opts.verbose,
43534362
command_prefixes(flags, plan),

‎src/build/plan.cppm‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3224,12 +3224,23 @@ make_plan(const mcpp::manifest::Manifest& manifest,
32243224
if (collided.contains(name)) bound.emplace_back(name, path);
32253225
}
32263226
if (bound.empty()) continue; // sees no collided name
3227-
// GCC's mapper answers only what it lists: the standard
3228-
// library modules are listed where GCC's own mapper puts them.
3227+
// GCC's mapper answers only what it lists. The standard
3228+
// library modules, and any name a unit of the package imports
3229+
// that no unit of the graph provides (a BMI placed by other
3230+
// means), are listed where GCC's own mapper puts them.
32293231
if (gcc) {
3230-
entries.emplace_back("std", std::string(traits.bmiDir) + "/" + basename("std"));
3231-
entries.emplace_back("std.compat",
3232-
std::string(traits.bmiDir) + "/" + basename("std.compat"));
3232+
std::set<std::string, std::less<>> listed;
3233+
for (auto const& [name, path] : entries) listed.insert(name);
3234+
auto flat = [&](const std::string& name) {
3235+
if (listed.insert(name).second)
3236+
entries.emplace_back(name, std::string(traits.bmiDir) + "/" + basename(name));
3237+
};
3238+
flat("std");
3239+
flat("std.compat");
3240+
for (auto const& cu : plan.compileUnits)
3241+
if (cu.packageName == pkg)
3242+
for (auto const& imp : cu.imports)
3243+
if (!graph.providersOf.contains(imp)) flat(imp);
32333244
}
32343245
std::sort(entries.begin(), entries.end());
32353246
ModuleScope scope;
@@ -3251,7 +3262,9 @@ make_plan(const mcpp::manifest::Manifest& manifest,
32513262
const bool separate = !prefix.empty() && prefix.back() == ' ';
32523263
if (separate) prefix.remove_suffix(1);
32533264
for (auto const& [name, path] : bound) {
3254-
const auto value = name + "=" + (outputDir / path).string();
3265+
auto bmi = outputDir / std::filesystem::path(path);
3266+
bmi.make_preferred();
3267+
const auto value = name + "=" + bmi.string();
32553268
if (separate) {
32563269
flags.push_back(std::string(prefix));
32573270
flags.push_back(mcpp::manifest::flag_element(value));

‎src/fallback/xlings_binary.cppm‎

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ struct XlingsSource {
5656
std::optional<XlingsSource> choose_xlings_source(std::optional<XlingsSource> override_,
5757
std::optional<XlingsSource> released,
5858
std::optional<XlingsSource> onPath);
59-
// The same choice over this process's sources.
60-
std::optional<XlingsSource> select_xlings_source(const std::filesystem::path& destBin = {});
59+
// The same choice over this process's sources. Their versions are read
60+
// through `versionMemo` (known_xlings_version).
61+
std::optional<XlingsSource> select_xlings_source(const std::filesystem::path& destBin = {},
62+
const std::filesystem::path& versionMemo = {});
6163

6264
// The version `bin` answers, asked at most once per process. With `memoFile`,
6365
// the answer is also kept across processes, keyed by the binary's path, size
@@ -120,7 +122,7 @@ acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet = false,
120122
// re-acquired, which replaced 2026.8.2.1 with the system's 0.4.51 --
121123
// older still, and equally missing the feature the check exists to
122124
// restore. Look before leaping.
123-
auto candidate = select_xlings_source(destBin);
125+
auto candidate = select_xlings_source(destBin, versionMemo);
124126
if (!candidate || candidate->version.empty()
125127
|| !version_is_older(have, candidate->version)) {
126128
// stderr, not stdout. This is a remark about the environment,
@@ -138,15 +140,21 @@ acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet = false,
138140
settled.insert(destBin);
139141
return destBin;
140142
}
143+
// Beside the binary first and renamed over it, so a copy that fails
144+
// (a full disk, a running binary) leaves the old one in place.
141145
std::error_code rec;
142-
std::filesystem::copy_file(candidate->path, destBin,
146+
auto staged = destBin;
147+
staged += ".new";
148+
std::filesystem::copy_file(candidate->path, staged,
143149
std::filesystem::copy_options::overwrite_existing, rec);
144-
if (!rec) {
145-
std::filesystem::permissions(destBin,
150+
if (!rec)
151+
std::filesystem::permissions(staged,
146152
std::filesystem::perms::owner_exec
147153
| std::filesystem::perms::group_exec
148154
| std::filesystem::perms::others_exec,
149155
std::filesystem::perm_options::add, rec);
156+
if (!rec) std::filesystem::rename(staged, destBin, rec);
157+
if (!rec) {
150158
if (!quiet && !updated)
151159
std::println(stderr,
152160
"{:>12} vendored xlings {} -> {} from {} (pinned {})",
@@ -158,8 +166,11 @@ acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet = false,
158166
settled.insert(destBin);
159167
return destBin;
160168
}
161-
// The copy failed; the chain below tries again from the start.
162-
std::filesystem::remove(destBin, rec);
169+
// The copy failed: the old binary stays, as it would with no source.
170+
std::error_code sec;
171+
std::filesystem::remove(staged, sec);
172+
settled.insert(destBin);
173+
return destBin;
163174
}
164175

165176
std::error_code ec;
@@ -177,7 +188,7 @@ acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet = false,
177188
// The first acquisition takes the source a replacement would take
178189
// (select_xlings_source): the override, otherwise the newer of the
179190
// released copy and the PATH copy.
180-
if (auto src = select_xlings_source(destBin)) {
191+
if (auto src = select_xlings_source(destBin, versionMemo)) {
181192
std::filesystem::copy_file(src->path, destBin,
182193
std::filesystem::copy_options::overwrite_existing, ec);
183194
if (!ec) {
@@ -290,22 +301,24 @@ std::optional<XlingsSource> choose_xlings_source(std::optional<XlingsSource> ove
290301
return released;
291302
}
292303

293-
std::optional<XlingsSource> select_xlings_source(const std::filesystem::path& destBin) {
304+
std::optional<XlingsSource> select_xlings_source(const std::filesystem::path& destBin,
305+
const std::filesystem::path& versionMemo) {
294306
std::optional<XlingsSource> override_, released, onPath;
295307
std::error_code ec;
296308
if (const char* e = std::getenv("MCPP_VENDORED_XLINGS"); e && *e) {
297309
std::filesystem::path p{e};
298310
if (std::filesystem::exists(p, ec))
299-
override_ = XlingsSource{p, vendored_xlings_version(p), "MCPP_VENDORED_XLINGS"};
311+
override_ = XlingsSource{p, known_xlings_version(p, versionMemo), "MCPP_VENDORED_XLINGS"};
300312
}
301313
if (!override_) {
302314
if (auto r = released_xlings_source(destBin); !r.empty())
303-
released = XlingsSource{r, vendored_xlings_version(r), "the release of this mcpp"};
315+
released = XlingsSource{r, known_xlings_version(r, versionMemo),
316+
"the release of this mcpp"};
304317
if (auto sys = mcpp::platform::fs::which(
305318
std::string("xlings") + std::string(mcpp::platform::exe_suffix))) {
306319
const bool isDest = !destBin.empty() && std::filesystem::equivalent(*sys, destBin, ec);
307320
if (!isDest && std::filesystem::exists(*sys, ec))
308-
onPath = XlingsSource{*sys, vendored_xlings_version(*sys), "PATH"};
321+
onPath = XlingsSource{*sys, known_xlings_version(*sys, versionMemo), "PATH"};
309322
}
310323
}
311324
return choose_xlings_source(std::move(override_), std::move(released), std::move(onPath));

‎src/modgraph/scanner.cppm‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,9 +692,14 @@ std::vector<std::filesystem::path> expand_glob_one(const std::filesystem::path&
692692
if (!fs::exists(start, startEc)) return out;
693693

694694
// Every match ends with the text after the glob's last `*`, which the
695-
// matcher reads literally: a cheap test that turns most entries away.
695+
// matcher reads literally: a cheap test that turns most entries away. A
696+
// `**/` also matches no directory at all (`**/main.cpp` matches a root
697+
// `main.cpp`), so the `/` after a `**` is not part of the tail.
696698
const auto star = glob.find_last_of('*');
697-
const std::string_view tail = star == std::string_view::npos ? glob : glob.substr(star + 1);
699+
std::string_view tail = star == std::string_view::npos ? glob : glob.substr(star + 1);
700+
if (star != std::string_view::npos && star > 0 && glob[star - 1] == '*'
701+
&& tail.starts_with('/'))
702+
tail.remove_prefix(1);
698703
auto listing = tree_listing(root, start);
699704
for (auto const& f : listing->files) {
700705
if (!f.relative) continue;

‎tests/unit/test_modgraph.cpp‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,3 +1433,19 @@ TEST(ModuleResolution, OneFileReachedTwiceIsNamedAsSuchWhateverItsSpelling) {
14331433
EXPECT_NE(errors.find("one file is reached as two packages"), std::string::npos) << errors;
14341434
std::filesystem::remove_all(dir);
14351435
}
1436+
1437+
// A kept walk matched by the literal tail of a glob: `**/` also matches no
1438+
// directory, so `**/main.cpp` matches a `main.cpp` at the root as well as one
1439+
// below it, as the per-pattern walk did.
1440+
TEST(Scanner, ADoubleStarSlashTailMatchesAtTheRoot) {
1441+
auto dir = make_tempdir("mcpp-glob-tail");
1442+
write(dir / "main.cpp", "int main() {}\n");
1443+
write(dir / "sub" / "main.cpp", "int main() {}\n");
1444+
write(dir / "sub" / "other.cpp", "int x;\n");
1445+
auto files = expand_glob(dir, "**/main.cpp");
1446+
ASSERT_EQ(files.size(), 2u);
1447+
EXPECT_EQ(files[0], dir / "main.cpp");
1448+
EXPECT_EQ(files[1], dir / "sub" / "main.cpp");
1449+
std::filesystem::remove_all(dir);
1450+
}
1451+

0 commit comments

Comments
 (0)