Skip to content

Commit 08341b3

Browse files
fix(build): the generation carries the compiler cap, not build.ninja
A capped graph named its cap in every compile command, and the cap follows the job count, so build.ninja differed between -j 1 and -j 4 (e2e 861). The generation's owner record now carries the cap and every executor of the generation reads it there; the fast path opens its generation with the cap its first drive left beside build.ninja, and replans without one. Outside a generation an executor takes --cap or MCPP_COMPILER_CAP. The note for clang without coroutines on the 32-bit MSVC ABI looks for the compiler after the executor's `--` (e2e 892). Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent dd53530 commit 08341b3

8 files changed

Lines changed: 84 additions & 13 deletions

File tree

‎src/build/execute.cppm‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2185,7 +2185,11 @@ std::optional<int> run_ninja_fast(const std::string& ninjaProgram,
21852185
return text.find(" __compiler-cap ") != std::string::npos;
21862186
}();
21872187
if (capped) {
2188-
auto opened = mcpp::build::schedule::detach::RunScope::begin(outputDir);
2188+
// The graph names no cap; its first drive left it beside build.ninja.
2189+
// Without it the graph is replanned rather than run unbounded.
2190+
const int cap = mcpp::build::read_compiler_cap_sidecar(outputDir);
2191+
if (cap <= 0) return std::nullopt;
2192+
auto opened = mcpp::build::schedule::detach::RunScope::begin(outputDir, cap);
21892193
if (!opened) { mcpp::ui::error(opened.error()); return 1; }
21902194
compilerRun.emplace(std::move(*opened));
21912195
compilerRun->environment(childEnv);

‎src/build/ninja_backend.cppm‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@ void write_consumer_include_sidecar(const std::filesystem::path& outputDir,
318318
std::vector<std::filesystem::path>
319319
read_consumer_include_sidecar(const std::filesystem::path& outputDir);
320320

321+
// The compiler cap of the generation a capped graph runs in, beside
322+
// build.ninja and not in it (e2e 861): the fast path replays the graph
323+
// without a plan and opens its generation with this cap. Removed when the
324+
// graph runs no compiler through the capped executor; 0 when absent.
325+
void write_compiler_cap_sidecar(const std::filesystem::path& outputDir, int cap);
326+
int read_compiler_cap_sidecar(const std::filesystem::path& outputDir);
327+
321328
// #693 (M4): whether a Ninja reads build.ninja in the encoding mcpp writes it.
322329
//
323330
// mcpp writes the file in UTF-8: every path in it has a UTF-8 spelling
@@ -1180,6 +1187,21 @@ void write_consumer_include_sidecar(const std::filesystem::path& outputDir,
11801187
for (auto const& d : dirs) f << d.string() << '\n';
11811188
}
11821189

1190+
constexpr std::string_view kCompilerCapSidecar = ".mcpp-compiler-cap";
1191+
1192+
void write_compiler_cap_sidecar(const std::filesystem::path& outputDir, int cap) {
1193+
const auto path = outputDir / kCompilerCapSidecar;
1194+
std::error_code ec;
1195+
if (cap <= 0) { std::filesystem::remove(path, ec); return; }
1196+
std::ofstream(path, std::ios::binary | std::ios::trunc) << cap << '\n';
1197+
}
1198+
1199+
int read_compiler_cap_sidecar(const std::filesystem::path& outputDir) {
1200+
std::ifstream f(outputDir / kCompilerCapSidecar, std::ios::binary);
1201+
int cap = 0;
1202+
return f >> cap && cap > 0 ? cap : 0;
1203+
}
1204+
11831205
std::vector<std::filesystem::path>
11841206
read_consumer_include_sidecar(const std::filesystem::path& outputDir) {
11851207
std::vector<std::filesystem::path> dirs;
@@ -1474,8 +1496,10 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements,
14741496
// larger slot count as their own concurrency bound.
14751497
// Every capped graph (2026.11.7.1): the executor also registers each
14761498
// compiler as a participant of the build's generation.
1499+
// No number of compilers here: the generation carries the cap, so the
1500+
// graph is the same whatever the job count (e2e 861).
14771501
const std::string compilerPrefix = plan.scheduleCompilerCap > 0
1478-
? std::format("$mcpp __compiler-cap --sem .mcpp-sched --cap {} --output $out --depfile $out.d -- ", plan.scheduleCompilerCap)
1502+
? std::string("$mcpp __compiler-cap --sem .mcpp-sched --output $out --depfile $out.d -- ")
14791503
: std::string{};
14801504
// The other split shape. Clang publishes no BMI early — it writes the BMI
14811505
// at the end of the compile — so the GCC trick of releasing the file
@@ -2113,7 +2137,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements,
21132137
// wrong in this file before.
21142138
append("rule cxx_module_bmi\n");
21152139
append(" command = $mcpp bmi-compile --bmi $bmi_out --slot $slot"
2116-
" --self $mcpp --sem $sched_sem --cap $sched_cap --object $obj_out"
2140+
" --self $mcpp --sem $sched_sem --object $obj_out"
21172141
" --command-file $out.cmd --dep-from $scan_dep --dep-to $out.d\n");
21182142
append(std::format(
21192143
" rspfile = $out.cmd\n"
@@ -3204,7 +3228,6 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements,
32043228
e += " slot = " + slot + "\n";
32053229
e += " scan_dep = " + escape_ninja_path(ddi) + ".dep\n";
32063230
e += " sched_sem = .mcpp-sched\n";
3207-
e += std::format(" sched_cap = {}\n", plan.scheduleCompilerCap);
32083231
if (auto inc = local_include_flags(cu, dial); !inc.empty())
32093232
e += " local_includes =" + inc + "\n";
32103233
if (auto fl = unit_cxx_flags(cu); !fl.empty())
@@ -4545,7 +4568,7 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
45454568
plan.outputDir});
45464569
std::optional<mcpp::build::schedule::detach::RunScope> compilerRun;
45474570
if (!opts.dryRun && plan.scheduleCompilerCap > 0) {
4548-
auto opened = mcpp::build::schedule::detach::RunScope::begin(plan.outputDir);
4571+
auto opened = mcpp::build::schedule::detach::RunScope::begin(plan.outputDir, plan.scheduleCompilerCap);
45494572
if (!opened) return std::unexpected(BuildError{opened.error(), plan.outputDir});
45504573
compilerRun.emplace(std::move(*opened));
45514574
reclaim_stale_schedule_tokens(plan.outputDir);
@@ -4561,6 +4584,7 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
45614584
plan.targetSide.cAbiDecl ? plan.targetSide.cAbiDecl->absent
45624585
: std::vector<mcpp::targetside::CAbiAbsentEntry>{});
45634586
write_consumer_include_sidecar(plan.outputDir, root_include_dirs_of(plan));
4587+
write_compiler_cap_sidecar(plan.outputDir, plan.scheduleCompilerCap);
45644588
write_graph_c_library_sidecar(plan.outputDir, plan.targetSide.cAbi);
45654589
std::string placements;
45664590
mcpp::build::progress::Attribution attribution;

‎src/build/schedule/detach_codegen.cppm‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,18 @@ import mcpp.toolchain.fingerprint;
8383
export namespace mcpp::build::schedule::detach {
8484

8585
inline constexpr std::string_view kRunEnv = "MCPP_COMPILER_RUN";
86+
// The cap of an executor that runs outside a generation (Ninja started by
87+
// hand), when its command line names none.
88+
inline constexpr std::string_view kCapEnv = "MCPP_COMPILER_CAP";
8689

8790
// One runtime generation, outside the build identity. Its actor barrier covers
8891
// every helper that may write state, not only helpers holding compiler slots.
92+
// The generation carries the compiler cap: build.ninja names no number of
93+
// compilers, so the graph is the same whatever the job count (e2e 861), and
94+
// every executor of the generation reads the cap from its owner record.
8995
class RunScope {
9096
public:
91-
static std::expected<RunScope, std::string> begin(const std::filesystem::path& outputDir);
97+
static std::expected<RunScope, std::string> begin(const std::filesystem::path& outputDir, int compilerCap);
9298
RunScope(RunScope&&) noexcept;
9399
RunScope& operator=(RunScope&&) noexcept;
94100
~RunScope();
@@ -194,6 +200,7 @@ struct Owner {
194200
bool managed = false;
195201
std::filesystem::path generation;
196202
mcpplibs::os::process::program program;
203+
int cap = 0; // the generation's compiler cap
197204

198205
static Owner inherited() {
199206
Owner owner;
@@ -205,6 +212,7 @@ struct Owner {
205212
std::string version;
206213
if (!(record >> version >> owner.program.id >> owner.program.started_ns)
207214
|| version != "mcpp-compiler-run-v1") owner.program = {};
215+
else if (!(record >> owner.cap)) owner.cap = 0;
208216
return owner;
209217
}
210218
bool active() const {
@@ -332,8 +340,16 @@ struct Permit {
332340
std::filesystem::path reservation;
333341
};
334342
std::expected<Permit, int>
335-
acquire_permit(const Actor& actor, const std::filesystem::path& requested, int cap) {
343+
acquire_permit(const Actor& actor, const std::filesystem::path& requested, int requestedCap) {
336344
const auto dir = actor.owner.permits(requested);
345+
const int cap = [&] {
346+
if (actor.owner.managed) return actor.owner.cap;
347+
if (requestedCap > 0) return requestedCap;
348+
int fromEnv = 0;
349+
if (const auto value = mcpp::invocation::get(kCapEnv))
350+
std::from_chars(value->data(), value->data() + value->size(), fromEnv);
351+
return fromEnv;
352+
}();
337353
if (!actor.ready) return std::unexpected(130);
338354
if (dir.empty() || cap <= 0) {
339355
if (actor.owner.managed) return std::unexpected(2);
@@ -619,7 +635,8 @@ RunScope::RunScope(RunScope&&) noexcept = default;
619635
RunScope& RunScope::operator=(RunScope&&) noexcept = default;
620636
RunScope::~RunScope() = default;
621637

622-
std::expected<RunScope, std::string> RunScope::begin(const std::filesystem::path& outputDir) {
638+
std::expected<RunScope, std::string> RunScope::begin(const std::filesystem::path& outputDir, int compilerCap) {
639+
if (compilerCap <= 0) return std::unexpected("a compiler-run generation needs a compiler cap");
623640
const auto self = mcpplibs::os::process::self();
624641
if (!self || self->id == 0 || self->started_ns == 0)
625642
return std::unexpected("cannot identify the compiler-run owner and its birth time");
@@ -675,7 +692,7 @@ std::expected<RunScope, std::string> RunScope::begin(const std::filesystem::path
675692
auto witnesses = transactions::Witnesses::start(state->generation);
676693
if (!witnesses) return std::unexpected(witnesses.error());
677694
state->witnesses = std::move(*witnesses);
678-
const auto record = std::format("mcpp-compiler-run-v1\n{} {}\n", self->id, self->started_ns);
695+
const auto record = std::format("mcpp-compiler-run-v1\n{} {} {}\n", self->id, self->started_ns, compilerCap);
679696
if (!transactions::replace_record(state->generation / "owner", record))
680697
return std::unexpected("cannot publish compiler-run ownership");
681698
return RunScope(std::move(state));

‎src/cli.cppm‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ int run(int argc, char** argv) {
286286
for (; i < argc && std::string_view(argv[i]) != "--"; ++i) {
287287
const std::string_view option = argv[i];
288288
if ((option != "--sem" && option != "--cap" && option != "--output" && option != "--depfile") || i + 1 >= argc) {
289-
std::println(stderr, "error: __compiler-cap: expected --sem <dir> --cap <N> -- <compiler>...");
289+
std::println(stderr, "error: __compiler-cap: expected --sem <dir> [--cap <N>] -- <compiler>...");
290290
return 2;
291291
}
292292
const std::string_view value = argv[++i];
@@ -298,7 +298,8 @@ int run(int argc, char** argv) {
298298
if (ec != std::errc{} || end != value.data() + value.size()) return 2;
299299
}
300300
}
301-
if (semaphore.empty() || cap <= 0 || i + 1 >= argc) return 2;
301+
// No --cap: the generation's, or MCPP_COMPILER_CAP outside one.
302+
if (semaphore.empty() || cap < 0 || i + 1 >= argc) return 2;
302303
std::vector<std::string> command;
303304
for (++i; i < argc; ++i) command.emplace_back(argv[i]);
304305
return mcpp::build::schedule::detach::run_capped(command, semaphore, cap, output, depfile);

‎src/toolchain/msvc_coroutines.cppm‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ std::optional<FailedCommand> failed_command_in(std::string_view output) {
173173
for_each_line(output, [&](std::string_view line) {
174174
if (auto at = line.find("command: "); at == 0) line.remove_prefix(9);
175175
auto words = split_words(line);
176+
// A compile run through the capped executor (`<mcpp> __compiler-cap
177+
// ... -- <compiler> ...`) names its compiler after the `--`.
178+
if (words.size() > 1 && words[1] == "__compiler-cap") {
179+
const auto end = std::ranges::find(words, std::string("--"));
180+
words.erase(words.begin(), end == words.end() ? end : end + 1);
181+
}
176182
if (words.empty() || !names_clang_driver(words.front())) return;
177183
FailedCommand cmd;
178184
cmd.compiler = words.front();

‎tests/e2e/helpers/check_shared_compiler_cap.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,12 @@ def update(start):
132132
graph.write_text(text)
133133
env = os.environ.copy()
134134
env["MCPP_CAP_COUNTER"] = str(counter)
135+
env["MCPP_COMPILER_CAP"] = str(cap) # build.ninja names no cap; outside a generation it is this
135136
command = [ninja, "-j", str(cap * 6)]
136137
if owner_driver:
137138
env.update(GTEST_FILTER="DetachLifetime.SubprocessDriver",
138139
MCPP_TEST_LIFETIME_ROLE="root", MCPP_TEST_LIFETIME_OUTPUT=str(build),
139-
MCPP_TEST_LIFETIME_NINJA_JOBS=str(cap*6))
140+
MCPP_TEST_LIFETIME_NINJA_JOBS=str(cap*6), MCPP_TEST_LIFETIME_CAP=str(cap))
140141
command=[str(owner_driver)]
141142
checked(command, build, log, env)
142143
semaphore = next((project/".mcpp/compiler-runs").glob("*/slots")) if owner_driver else build/".mcpp-sched"

‎tests/unit/test_detach_status.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ int lifetime_driver() {
102102
std::stoi(value("--cap")), value("--object"), command);
103103
}
104104
if (role == "root") {
105-
auto run = detach::RunScope::begin(lifetime_env("MCPP_TEST_LIFETIME_OUTPUT", fs::current_path().string()));
105+
// The generation's cap; build.ninja names none (e2e 861).
106+
auto run = detach::RunScope::begin(lifetime_env("MCPP_TEST_LIFETIME_OUTPUT", fs::current_path().string()),
107+
std::stoi(lifetime_env("MCPP_TEST_LIFETIME_CAP", "1")));
106108
if (!run) { std::println(std::cerr, "{}", run.error()); return 2; }
107109
std::ofstream("root.ready") << mcpplibs::os::process::self()->id;
108110
mcpp::run::Env env;

‎tests/unit/test_msvc_coroutines.cpp‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ TEST(MsvcCoroutines, CoroutineUseNoteDoesNotOfferCxx20) {
127127
EXPECT_NE(note.find("llvm@22.1.8"), std::string::npos);
128128
}
129129

130+
// A compile run through the capped executor names its compiler after `--`.
131+
TEST(MsvcCoroutines, CoroutineUseThroughTheCappedExecutorGetsTheNote) {
132+
constexpr std::string_view failure =
133+
"FAILED: obj/main.o \n"
134+
"C:/mcpp/bin/mcpp.exe __compiler-cap --sem .mcpp-sched --output obj/main.o --depfile obj/main.o.d -- "
135+
"C:/Users/runneradmin/.mcpp/registry/data/xpkgs/xim-x-llvm/23.1.3/bin/clang++.exe @obj/main.o.rsp "
136+
"-std=c++20 --target=i686-pc-windows-msvc -c D:/a/_temp/src/main.cpp -o obj/main.o\n"
137+
"D:/a/_temp/src/main.cpp:2:71: error: use of undeclared identifier 'std'\n"
138+
"D:/a/_temp/src/main.cpp:3:9: error: std::coroutine_traits type was not found; include <coroutine> before defining a coroutine\n";
139+
auto cmd = co::failed_command_in(failure);
140+
ASSERT_TRUE(cmd.has_value());
141+
EXPECT_TRUE(cmd->compiler.ends_with("clang++.exe"));
142+
EXPECT_EQ(cmd->target, "i686-pc-windows-msvc");
143+
EXPECT_FALSE(co::advice(failure, answering(false)).empty());
144+
}
145+
130146
TEST(MsvcCoroutines, CompilerThatPredefinesTheMacroGetsNoNote) {
131147
EXPECT_TRUE(co::advice(kStdModuleFailure, answering(true)).empty());
132148
}

0 commit comments

Comments
 (0)