-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathhostprogram.cppm
More file actions
509 lines (483 loc) · 26.2 KB
/
Copy pathhostprogram.cppm
File metadata and controls
509 lines (483 loc) · 26.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
// mcpp.build.hostprogram — compiling the bundled `mcpp` module for build.mcpp.
//
// Split out of build_program.cppm for a blunt reason: that file's anonymous
// namespace miscompiles its own neighbours under clang 22.1.8 + C++20 modules
// + -O2. PR#332 established it (an UNUSED `split_ws` was enough to corrupt a
// local vector in `contract_env`), and growing `build_mcpp_module` in place
// reproduced it again — `Segmentation fault: 11` on every macOS build.mcpp
// e2e, in code this change never touched. Mechanism unknown, reproduction
// solid, and the cheap response is to stop growing that namespace.
//
// See .agents/docs/2026-08-02-host-compile-single-producer-design.md §6.2.
export module mcpp.build.hostprogram;
import std;
import mcpp.build.directives; // kProtocolVersion — the announced value has ONE source
import mcpp.platform;
import mcpp.platform.process;
import mcpp.toolchain.dialect;
import mcpp.toolchain.hostflags;
import mcpp.toolchain.model;
export namespace mcpp::build {
namespace fs = std::filesystem;
// The bundled `mcpp` build module — a typed API over the stdout wire protocol
// so build.mcpp can `import mcpp;` instead of `#include`. Its own I/O uses
// C-level primitives in the global module fragment, so the module itself
// needs no std BMI and stays buildable before one exists. (That was once also
// a limit on build.mcpp; it no longer is — a build.mcpp may `import std;` and
// the engine stages the same std module the main build uses.)
// The functions mirror the directive set 1:1; they just print the
// `mcpp:` lines the engine already parses. Embedded in the binary (not shipped as
// a file) so it always matches this mcpp's protocol.
// NOTE: the module declaration line uses a `@MODULE@` placeholder (substituted
// with `export module` when written) so mcpp's own line-based module scanner does
// not mistake this embedded string for build_program.cppm exporting a 2nd module.
inline constexpr std::string_view kMcppModuleSource = R"CPP(module;
#include <cstdio>
#include <cstdlib>
@MODULE@ mcpp;
export namespace mcpp {
inline void cxxflag(const char* flag) { std::printf("mcpp:cxxflag=%s\n", flag); }
inline void cflag(const char* flag) { std::printf("mcpp:cflag=%s\n", flag); }
inline void link_lib(const char* name) { std::printf("mcpp:link-lib=%s\n", name); }
inline void link_search(const char* dir) { std::printf("mcpp:link-search=%s\n", dir); }
inline void define(const char* name) { std::printf("mcpp:cfg=%s\n", name); }
inline void generated(const char* path) { std::printf("mcpp:generated=%s\n", path); }
inline void source(const char* path) { std::printf("mcpp:source=%s\n", path); }
inline void include_dir(const char* dir) { std::printf("mcpp:include-dir=%s\n", dir); }
inline void include_dir_after(const char* dir) { std::printf("mcpp:include-dir-after=%s\n", dir); }
// ── Build-graph nodes (mcpp 2026.8.5.1+) ────────────────────────────────
// Declare WORK instead of doing it. A build program is a good place to decide
// what the build looks like and a bad place to perform it: work done here is
// serial, whole-set, and reported as "build.mcpp exited 1". Declared as a node
// it becomes an edge in the build graph — incremental, parallel, attributable.
//
// You must name the OUTPUT FILES. mcpp fixes the source set, the fingerprint
// and the module graph during prepare, so an output whose name is unknown
// cannot be built. Content may arrive later; names may not.
struct action {
const char* id = "";
const char* role = "source"; // "source" | "check" | "object" | "artifact"
const char* description = "";
bool blocking = false; // check only: gate compilation on it
action& input(const char* p) { add(inputs_, sizeof inputs_, p); return *this; }
action& output(const char* p) { add(outputs_, sizeof outputs_, p); return *this; }
action& arg(const char* a) { add(command_, sizeof command_, a); return *this; }
// Declare what a generated MODULE INTERFACE provides/imports. Same
// "declare instead of discover" trade [modules].scan_overrides makes, and
// what lets a generated .cppm exist as a graph node at all.
action& provides(const char* n) { add(provides_, sizeof provides_, n); return *this; }
action& imports(const char* n) { add(imports_, sizeof imports_, n); return *this; }
// Object only: which link unit receives the outputs. Omit for "every image
// this package produces" — which INCLUDES test binaries, and is what you
// want: their names come from tests/*.cpp, so spelling one here breaks
// plain `mcpp build`, where that link unit does not exist. An Artifact reads
// its target out of ${mcpp.target_file:NAME}; an Object runs before the link
// and has no such handle, so it has to say the name.
action& target(const char* n) { add(targets_, sizeof targets_, n); return *this; }
void submit() const {
std::printf("mcpp:action={\"id\":"); esc(id);
std::printf(",\"role\":"); esc(role);
std::printf(",\"description\":"); esc(description);
std::printf(",\"blocking\":%s", blocking ? "true" : "false");
// A truncated argv would otherwise be INVALID rather than obviously
// wrong — the engine turns this marker into a diagnostic that names
// the limit, instead of a generic "malformed action".
if (overflow_) std::printf(",\"overflow\":true");
std::printf(",\"inputs\":[%s]", inputs_);
std::printf(",\"outputs\":[%s]", outputs_);
std::printf(",\"command\":[%s]", command_);
std::printf(",\"provides\":[%s]", provides_);
std::printf(",\"imports\":[%s]", imports_);
std::printf(",\"targets\":[%s]", targets_);
std::printf("}\n");
}
private:
// Fixed buffers because this module must stay buildable BEFORE a std BMI
// exists (it is what a build.mcpp imports, and it may be compiled first) —
// so no std::string. Sizes chosen for real generator invocations: a protoc
// command line with many -I paths runs long.
char inputs_[8192]{}, outputs_[8192]{}, command_[16384]{},
provides_[2048]{}, imports_[2048]{}, targets_[1024]{};
mutable bool overflow_ = false;
static void esc(const char* s) {
std::putchar('"');
for (const char* p = s; *p; ++p) {
unsigned char c = (unsigned char)*p;
if (c == '"' || c == '\\') { std::putchar('\\'); std::putchar(c); continue; }
// Any control character has to be escaped or the payload is not
// JSON at all. \n was handled before; \t and \r reach this code
// through ordinary Windows paths and log text.
if (c < 0x20) { std::printf("\\u%04x", c); continue; }
std::putchar(c);
}
std::putchar('"');
}
// Capacity is a PARAMETER. The previous revision hardcoded 4096 while the
// smallest buffer here was 1024 — a bound living somewhere other than next
// to the array it bounds is exactly the shape that overflows.
bool add(char* buf, unsigned long cap, const char* s) {
unsigned long o = 0; while (buf[o]) ++o;
if (o + 4 >= cap) { overflow_ = true; return false; }
if (o) buf[o++] = ',';
buf[o++] = '"';
for (const char* p = s; *p; ++p) {
if (o + 3 >= cap) { buf[o] = 0; overflow_ = true; return false; }
if (*p == '"' || *p == '\\') buf[o++] = '\\';
buf[o++] = *p;
}
buf[o++] = '"';
buf[o] = 0;
return true;
}
};
inline void rerun_if_changed(const char* path) { std::printf("mcpp:rerun-if-changed=%s\n", path); }
// mcpp#359: re-run when the SET of files matching `pattern` changes — a file
// appearing or disappearing, not its contents (declare those with
// rerun_if_changed). `pattern` is relative to the manifest directory and uses
// the same `*` / `**` grammar as `sources = [...]`, e.g. "proto/**/*.proto".
//
// Without this a build program that globs is structurally unsafe: adding a
// .proto changes no declared file's hash, so the program does not re-run and
// the new file is silently never generated. The build output tree and .git are
// never part of the set, so watching a wide pattern cannot create a re-run
// loop with the program's own outputs.
inline void rerun_if_changed_glob(const char* pattern) {
std::printf("mcpp:rerun-if-changed-glob=%s\n", pattern);
}
inline void rerun_if_env_changed(const char* var) { std::printf("mcpp:rerun-if-env-changed=%s\n", var); }
// ── environment contract (read side; values injected by the engine) ─────
inline const char* env_or(const char* n) { const char* v = std::getenv(n); return v ? v : ""; }
inline const char* target() { return env_or("MCPP_TARGET"); }
inline const char* target_os() { return env_or("MCPP_TARGET_OS"); }
inline const char* target_arch() { return env_or("MCPP_TARGET_ARCH"); }
inline const char* target_env() { return env_or("MCPP_TARGET_ENV"); }
inline const char* host() { return env_or("MCPP_HOST"); }
inline const char* profile() { return env_or("MCPP_PROFILE"); }
inline const char* out_dir() { return env_or("MCPP_OUT_DIR"); }
inline const char* manifest_dir() { return env_or("MCPP_MANIFEST_DIR"); }
inline bool has_feature(const char* name) {
char buf[256] = "MCPP_FEATURE_";
unsigned long o = 13;
for (const char* p = name; *p && o + 1 < sizeof buf; ++p, ++o) {
char c = *p;
buf[o] = (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A')
: ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_';
}
buf[o] = 0;
return std::getenv(buf) != nullptr;
}
// mcpp#241: resolved install dir of a declared dependency (by its package
// name), or "" if not found. Same sanitize as has_feature; wraps
// MCPP_DEP_<SANITIZED_NAME>_DIR.
inline const char* dep_dir(const char* name) {
char buf[256] = "MCPP_DEP_";
unsigned long o = 9;
for (const char* p = name; *p && o + 5 < sizeof buf; ++p, ++o) {
char c = *p;
buf[o] = (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A')
: ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_';
}
buf[o++] = '_'; buf[o++] = 'D'; buf[o++] = 'I'; buf[o++] = 'R'; buf[o] = 0;
return env_or(buf);
}
// mcpp#355: absolute path to a HOST tool built by a dependency — the binary
// behind one of its `kind = "bin"` targets. Returns "" unless the consumer
// declared it: <dep> = { version = "…", tools = ["protoc"] }
// The path already carries the platform's executable suffix.
inline const char* dep_bin(const char* pkg, const char* tool) {
char buf[256] = "MCPP_DEP_";
unsigned long o = 9;
auto put = [&](const char* s) {
for (const char* p = s; *p && o + 8 < sizeof buf; ++p, ++o) {
char c = *p;
buf[o] = (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A')
: ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_';
}
};
put(pkg);
buf[o++] = '_'; buf[o++] = 'B'; buf[o++] = 'I'; buf[o++] = 'N'; buf[o++] = '_';
put(tool);
buf[o] = 0;
return env_or(buf);
}
}
// ── Protocol announcement ───────────────────────────────────────────────
// Emitted before main() runs, so a program that uses `import mcpp;` never has
// to remember to declare anything. The engine uses it two ways: it refuses a
// program that speaks a NEWER protocol than it understands, and — because the
// two sides then provably agree — it treats an unrecognized directive as an
// error rather than warning and silently dropping it.
//
// A hand-written `printf("mcpp:...")` program emits no announcement, which is
// exactly right: that surface is frozen at protocol 1 and keeps the historical
// warn-and-ignore behaviour.
//
// Namespace-scope `static` in the module purview: internal linkage, one object
// in mcpp.o, whose dynamic initializer runs from .init_array. mcpp.o is always
// on the link line, so it always fires.
namespace mcpp_detail {
struct ProtocolAnnouncer {
ProtocolAnnouncer() { std::printf("mcpp:protocol=%d\n", @PROTOCOL@); }
};
static ProtocolAnnouncer mcpp_protocol_announcer;
}
)CPP";
// Compile the bundled `mcpp` module into `bdir` and return the extra flags the
// build.mcpp compile needs to import it (the object `mcpp.o` is linked alongside).
// GCC : -fmodules → gcm.cache/mcpp.gcm + mcpp.o; build.mcpp compiles from
// `bdir` (cwd) so GCC finds gcm.cache/mcpp.gcm.
// Clang : --precompile → mcpp.pcm, then -c → mcpp.o; pass -fmodule-file=mcpp=<pcm>.
// Does the source contain `import <name>;`?
//
// A plain substring search is not enough here: "import std" is a prefix of
// "import std.compat", so the naive test reports both for a program that
// only imports the latter, and mcpp would build a std BMI nobody asked for.
// Match the whole module name and require the terminating `;`, tolerating
// the whitespace the grammar allows. Occurrences inside comments or string
// literals still match — over-detection costs one cached BMI lookup, never
// a wrong build, and that is the same trade the `import mcpp` check has
// always made.
bool imports_module(std::string_view src, std::string_view name) {
constexpr std::string_view kImport = "import";
std::size_t pos = 0;
while ((pos = src.find(kImport, pos)) != std::string_view::npos) {
std::size_t i = pos + kImport.size();
// `importfoo` is not an import.
if (i >= src.size() || (src[i] != ' ' && src[i] != '\t')) { ++pos; continue; }
while (i < src.size() && (src[i] == ' ' || src[i] == '\t')) ++i;
if (src.compare(i, name.size(), name) == 0) {
std::size_t j = i + name.size();
while (j < src.size() && (src[j] == ' ' || src[j] == '\t')) ++j;
if (j < src.size() && src[j] == ';') return true;
}
++pos;
}
return false;
}
// What the bundled `mcpp` module contributes to the build.mcpp compile.
struct McppModule {
std::vector<std::string> useFlags; // how the consumer names the BMI
fs::path object; // linked alongside build.mcpp
};
// Compile ONE dependency-provided module interface for the host, into `bdir`,
// with the SAME flags build.mcpp itself gets. Returns how to name its BMI plus
// the object to link.
//
// Shares build_mcpp_module's per-family dispatch deliberately: a BMI is only
// usable by a compile that agrees with it on standard, dialect and compiler
// identity, and the cheapest way to guarantee that is to produce both from one
// set of flags rather than to check afterwards.
//
// Limitation, stated rather than hidden: the interface is compiled ALONE, so it
// may import `std` and the bundled `mcpp` module but not a third package. A
// rule package is a leaf by construction; a transitive host module graph would
// need the sub-build machinery and its own BMI-agreement story.
std::expected<McppModule, std::string>
build_host_module(const fs::path& bdir, const fs::path& compiler,
const std::vector<std::string>& base, const std::string& stdFlag,
const mcpp::toolchain::Toolchain& tc,
const std::vector<std::pair<std::string, std::string>>& env,
std::string_view logicalName, const fs::path& interfacePath,
const std::vector<std::string>& extraUseFlags);
std::expected<McppModule, std::string>
build_mcpp_module(const fs::path& bdir, const fs::path& compiler,
const std::vector<std::string>& base, const std::string& stdFlag,
const mcpp::toolchain::Toolchain& tc,
const std::vector<std::pair<std::string, std::string>>& env) {
std::error_code ec;
fs::path cppm = bdir / "mcpp.cppm";
std::string moduleSrc(kMcppModuleSource);
if (auto p = moduleSrc.find("@MODULE@"); p != std::string::npos)
moduleSrc.replace(p, std::string_view("@MODULE@").size(), "export module");
// Substituted rather than hardcoded so the announced version can never
// drift from the one the engine checks against.
if (auto p = moduleSrc.find("@PROTOCOL@"); p != std::string::npos)
moduleSrc.replace(p, std::string_view("@PROTOCOL@").size(),
std::to_string(mcpp::build::directives::kProtocolVersion));
{ std::ofstream os(cppm, std::ios::trunc);
os << moduleSrc;
if (!os) return std::unexpected(std::string("could not write mcpp module source")); }
auto run = [&](std::vector<std::string> argv, const char* what)
-> std::expected<void, std::string> {
auto r = mcpp::platform::process::capture_exec(argv, env, bdir.string());
if (r.exit_code != 0)
return std::unexpected(std::format("mcpp module {} failed (exit {}):\n{}",
what, r.exit_code, r.output));
return {};
};
auto with_base = [&](std::vector<std::string> head) {
for (auto& b : base) head.push_back(b);
return head;
};
// Dispatch on the SAME module table the main build uses (BmiTraits +
// CommandDialect), not on a local is_clang/else. That is what makes a
// toolchain family work here as soon as it works there — adding cl.exe
// needed no new pipeline, only this row.
const auto traits = mcpp::toolchain::bmi_traits(tc);
const auto& dial = mcpp::toolchain::dialect_for(tc);
McppModule out;
if (tc.compiler == mcpp::toolchain::CompilerId::MSVC) {
// cl produces the .ifc and the .obj in one step.
fs::path ifc = bdir / ("mcpp" + std::string(traits.bmiExt));
out.object = bdir / ("mcpp" + std::string(dial.objExt));
std::vector<std::string> argv{compiler.string()};
for (auto f : dial.alwaysFlagsArgv) argv.emplace_back(f);
argv.push_back(stdFlag);
argv.push_back("/interface");
for (auto f : dial.forceCxxLangArgv) argv.emplace_back(f);
argv.push_back(dial.compileOnly == std::string_view("/c") ? "/c" : "-c");
argv.push_back("mcpp.cppm");
argv.push_back("/ifcOutput"); argv.push_back(ifc.string());
argv.push_back(std::string(dial.outputObjPrefix) + out.object.string());
if (auto r = run(with_base(std::move(argv)), "compile"); !r)
return std::unexpected(r.error());
out.useFlags = mcpp::toolchain::bmi_reference_tokens(" /reference mcpp=", ifc);
return out;
}
out.object = bdir / ("mcpp" + std::string(dial.objExt));
if (mcpp::toolchain::is_clang(tc)) {
fs::path pcm = bdir / ("mcpp" + std::string(traits.bmiExt));
if (auto r = run(with_base({compiler.string(), stdFlag, "--precompile",
"mcpp.cppm", "-o", pcm.string()}), "precompile"); !r)
return std::unexpected(r.error());
if (auto r = run(with_base({compiler.string(), stdFlag, "-c",
pcm.string(), "-o", out.object.string()}), "object"); !r)
return std::unexpected(r.error());
out.useFlags = mcpp::toolchain::bmi_reference_tokens("-fmodule-file=mcpp=", pcm);
return out;
}
// GCC: BMIs are implicit under <cwd>/gcm.cache, so nothing to name.
if (auto r = run(with_base({compiler.string(), stdFlag,
std::string(mcpp::toolchain::bmi_traits(tc).compileModulesFlag).empty()
? "-fmodules" : "-fmodules",
"-c", "mcpp.cppm", "-o", out.object.string()}), "compile"); !r)
return std::unexpected(r.error());
out.useFlags = {"-fmodules"};
return out;
}
} // namespace mcpp::build
namespace mcpp::build {
std::expected<McppModule, std::string>
build_host_module(const fs::path& bdir, const fs::path& compiler,
const std::vector<std::string>& base, const std::string& stdFlag,
const mcpp::toolchain::Toolchain& tc,
const std::vector<std::pair<std::string, std::string>>& env,
std::string_view logicalName, const fs::path& interfacePath,
const std::vector<std::string>& extraUseFlags) {
std::error_code ec;
if (!fs::exists(interfacePath, ec)) {
return std::unexpected(std::format(
"host module '{}': no interface unit at {}\n"
" A package offering build rules must have a lib root "
"(src/<name>.cppm or [lib] path).",
logicalName, interfacePath.string()));
}
// A filesystem-safe stem. Partition separators and any path separator that
// sneaks into a logical name would otherwise create directories that do
// not exist. Dots are left ALONE on purpose: `a.b.rules.o` is a legal
// filename, GCC's own gcm.cache uses the dotted module name verbatim, and
// rewriting them would make the object name disagree with the BMI name for
// no gain.
std::string stem(logicalName);
for (auto& c : stem) if (c == ':' || c == '/' || c == '\\') c = '-';
auto run = [&](std::vector<std::string> argv, const char* what)
-> std::expected<void, std::string> {
auto r = mcpp::platform::process::capture_exec(argv, env, bdir.string());
if (r.exit_code != 0)
return std::unexpected(std::format(
"host module '{}' {} failed (exit {}):\n{}",
logicalName, what, r.exit_code, r.output));
return {};
};
auto with_base = [&](std::vector<std::string> head) {
for (auto& b : base) head.push_back(b);
for (auto& f : extraUseFlags) head.push_back(f);
return head;
};
const auto traits = mcpp::toolchain::bmi_traits(tc);
const auto& dial = mcpp::toolchain::dialect_for(tc);
McppModule out;
out.object = bdir / (stem + std::string(dial.objExt));
if (tc.compiler == mcpp::toolchain::CompilerId::MSVC) {
fs::path ifc = bdir / (stem + std::string(traits.bmiExt));
std::vector<std::string> argv{compiler.string()};
for (auto f : dial.alwaysFlagsArgv) argv.emplace_back(f);
argv.push_back(stdFlag);
argv.push_back("/interface");
for (auto f : dial.forceCxxLangArgv) argv.emplace_back(f);
argv.push_back("/c");
argv.push_back(interfacePath.string());
argv.push_back("/ifcOutput"); argv.push_back(ifc.string());
argv.push_back(std::string(dial.outputObjPrefix) + out.object.string());
if (auto r = run(with_base(std::move(argv)), "compile"); !r)
return std::unexpected(r.error());
out.useFlags = mcpp::toolchain::bmi_reference_tokens(
std::format(" /reference {}=", logicalName), ifc);
return out;
}
// The interface's LANGUAGE, stated rather than inferred from its extension.
//
// ⚠️ Measured on macOS CI: a rule package whose lib root is `rulepkg.ixx`
// made `clang++ --precompile rulepkg.ixx -o rulepkg.pcm` EXIT 0 AND WRITE
// NOTHING — clang's driver does not recognise `.ixx`, so it treated the file
// as a linker input, warned that it was unused, and succeeded. The failure
// surfaced one step later as `no such file or directory: …/rulepkg.pcm`,
// naming an output rather than the input that was never read.
//
// Every other module compile in mcpp already says this (BmiTraits::
// moduleInterfaceLangFlag — `/interface /TP`, `-x c++-module`, `-x c++`);
// the host-module path was the one place that still let the driver guess.
// It is positional on GNU-style drivers, so it goes immediately before the
// input.
std::vector<std::string> langArgv;
{
std::string_view lang = traits.moduleInterfaceLangFlag;
for (std::size_t i = 0; i < lang.size(); ) {
while (i < lang.size() && lang[i] == ' ') ++i;
auto j = lang.find(' ', i);
if (j == std::string_view::npos) j = lang.size();
if (j > i) langArgv.emplace_back(lang.substr(i, j - i));
i = j;
}
}
if (mcpp::toolchain::is_clang(tc)) {
fs::path pcm = bdir / (stem + std::string(traits.bmiExt));
std::vector<std::string> pre{compiler.string(), stdFlag, "--precompile"};
for (auto const& l : langArgv) pre.push_back(l);
pre.push_back(interfacePath.string());
pre.push_back("-o"); pre.push_back(pcm.string());
if (auto r = run(with_base(std::move(pre)), "precompile"); !r)
return std::unexpected(r.error());
// The precompile can succeed and write nothing when the driver ignored
// the input, which is exactly what happened above. Checked here so the
// diagnostic names the interface rather than a missing output.
if (!fs::exists(pcm, ec)) {
return std::unexpected(std::format(
"host module '{}': the compiler accepted '{}' and produced no "
"BMI.\n"
" The interface's language is passed explicitly, so this "
"is not an extension\n"
" the driver failed to recognise — check that the file "
"really is a module interface.",
logicalName, interfacePath.string()));
}
if (auto r = run(with_base({compiler.string(), stdFlag, "-c",
pcm.string(), "-o", out.object.string()}),
"object"); !r)
return std::unexpected(r.error());
out.useFlags = mcpp::toolchain::bmi_reference_tokens(
std::format("-fmodule-file={}=", logicalName), pcm);
return out;
}
// GCC: BMIs are implicit under <cwd>/gcm.cache, so nothing to name — which
// is also why the compile has to happen in bdir (it already does).
std::vector<std::string> gccArgv{compiler.string(), stdFlag, "-fmodules", "-c"};
for (auto const& l : langArgv) gccArgv.push_back(l);
gccArgv.push_back(interfacePath.string());
gccArgv.push_back("-o"); gccArgv.push_back(out.object.string());
if (auto r = run(with_base(std::move(gccArgv)), "compile"); !r)
return std::unexpected(r.error());
out.useFlags = {"-fmodules"};
return out;
}
} // namespace mcpp::build