-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathhostprogram.cppm
More file actions
949 lines (905 loc) · 50.8 KB
/
Copy pathhostprogram.cppm
File metadata and controls
949 lines (905 loc) · 50.8 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
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
// 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.log;
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); }
// One argv token of the command that EXECUTES this build's artifact, when the
// host cannot run it itself (a freestanding image: wrong ISA, no loader).
//
// Called once per token, in order — argv is an ordered list and a directive
// carries one value per line. The artifact path is appended by mcpp, or
// substituted for a `{}` token if one is present.
//
// Emit the executable as an ABSOLUTE path. A bare name resolves through
// PATH to a shim that dispatches against its OWNER home, which is not
// necessarily the home this build uses; measured in CI as
// `xlings: 'qemu-system-riscv64' is not installed` from a job where the same
// bare name had answered `--version` two steps earlier. `xpkg_dir()` is how a
// package finds the payload it declared.
//
// Exactly one dependency may supply this. Two board-support packages both
// claiming to know how to run the artifact is a configuration error, and mcpp
// reports it naming both rather than merging them.
inline void runner(const char* token) { std::printf("mcpp:runner=%s\n", token); }
// A NAMED way of reaching the artefact. The engine learns the name from
// here and knows nothing else about it, so `flash`, `serve`, `deploy`,
// `submit` and `logcat` cost the same: nothing.
//
// One token per call, because argv is ordered and a single string cannot say
// where its boundaries are. The user reaches it with `mcpp run --runner <name>`.
inline void runner(const char* name, const char* token) {
std::printf("mcpp:runner-named=%s:%s\n", name, token);
}
// This named runner has no natural end — a console monitor, a debug server.
// DECLARED RATHER THAN DERIVED FROM THE NAME: the engine has no list of
// names to derive it from, which is the point.
inline void runner_longlived(const char* name) {
std::printf("mcpp:runner-longlived=%s\n", name);
}
inline void run_exclusive() { std::printf("mcpp:run-exclusive=1\n"); }
// Say something to the user and keep going.
//
// THIS IS THE ONLY WAY A BUILD PROGRAM CAN SUCCEED AND STILL BE HEARD.
// mcpp prints what it captured from a build program only when that program
// EXITS NON-ZERO, so a `std::printf` or `std::fprintf(stderr, ...)` note is
// invisible on precisely the successful builds that needed it.
//
// Use it for a condition the program handled correctly but the user would
// want to know about — most often "I could not find X, so I configured
// nothing that depends on it". Do not use it for an error: exit non-zero
// instead, and the output is printed already.
//
// It survives the build cache. A cached run does not re-execute the
// program, and an advisory that appeared once and then vanished would read as
// "resolved". mcpp replays it on every hit.
inline void warning(const char* message) { std::printf("mcpp:warning=%s\n", message); }
// ── The probe channel (mcpp 2026.9.5.2+) ────────────────────────────────────
//
// A rule package is the thing that knows how to ask a machine what it has --
// which library to open, which function to call -- and the engine is the
// thing that must not. So the package MEASURES and the engine COMPARES:
//
// mcpp::fact("cuda.driver", "12.4"); // what this machine has
// mcpp::floor("cuda.driver >= 12.0"); // what this package needs
//
// Before anything is compiled the engine refuses when a floor is unmet and
// names both values (`mcpp why toolchain --format json` reports the reason
// `version-floor-unmet`). A floor nobody stated a fact for is silent: not
// knowing is not failing.
//
// A fact is persisted with the program's other output and replayed on a
// cache hit. State what would change it -- `rerun_if_changed` on the library
// the version was read from -- or the fact outlives the machine it described.
inline void fact(const char* name, const char* version) {
std::printf("mcpp:fact=%s=%s\n", name, version);
}
inline void floor(const char* spec) { std::printf("mcpp:floor=%s\n", spec); }
// ── The distributable channel (mcpp 2026.9.11.1+) ───────────────────────────
//
// `mcpp pack --format <name>` dispatches to whichever package provides
// `<name>`, exactly as `--target` reaches a triple the engine did not have to
// know individually. This is how a package says which name it answers for:
//
// mcpp::provides_pack_format("appimage");
//
// DECLARE UNCONDITIONALLY, SUBMIT CONDITIONALLY. The declaration must not be
// gated on `pack_format()`, and the reason is that the engine has to be able to
// answer a question the requesting build cannot: `mcpp pack --format bogus`
// names what IS available, and `--help` says "plus any format the resolved
// graph provides". Both read the set collected from this outlet, on a build
// that asked for nothing. A member that declared only when asked would still
// work for its author -- they always pass their own format -- and would make
// the set unknowable for everyone else.
//
// The work itself is the other half:
//
// if (std::string_view(mcpp::pack_format()) == "appimage") { ... submit ... }
//
// Two names are reserved for the engine's own archive shapes and are refused
// here: `tar` and `dir`.
inline void provides_pack_format(const char* name) {
std::printf("mcpp:pack-format=%s\n", name);
}
// The memory layout for a freestanding link. Reaches the CONSUMER's link line
// (like link_lib/link_search, unlike include_dir), because the package that
// knows a board's layout is not the package being built.
inline void link_script(const char* path) { std::printf("mcpp:link-script=%s\n", path); }
// A linker flag this program COMPUTED. The outlet `link_lib` / `link_search` /
// `link_script` leave open: a generated version script, `--wrap`,
// `--exclude-libs`. Reaches the consumer's link line, as `[build] ldflags`
// already does -- see the table row for why a private form is not offered.
inline void link_flag(const char* flag) { std::printf("mcpp:link-flag=%s\n", flag); }
// The PE subsystem ("console" | "windows") and the entry function ("main" |
// "wmain" | "WinMain" | "wWinMain") of an executable target of THIS package,
// named by `target` (#618). The same fields as `[targets.<name>]
// windows_subsystem` / `windows_entry`: they reach that target's link and no
// other, and are inert on every target that is not PE.
inline void windows_subsystem(const char* target, const char* value) {
std::printf("mcpp:windows-subsystem=%s:%s\n", target, value);
}
inline void windows_entry(const char* target, const char* value) {
std::printf("mcpp:windows-entry=%s:%s\n", target, value);
}
// A file THIS PROGRAM produced or selected, placed beside the artifact at a
// path relative to the executable's directory (#622 A4) — the build-program
// form of `[runtime] deploy` (docs/04). `from` may be absolute (an action's
// own declared output; see mcpp::action) or relative to this package's root;
// `to` follows the manifest key's own rule: `/`-separated, no `..`
// component, and `"."` names the executable's own directory. TAB-separated
// on the wire because an absolute `from` is a Windows path on that platform,
// and `windows_subsystem`'s `:` splitter would misparse `C:\...`.
inline void deploy(const char* from, const char* to) {
std::printf("mcpp:deploy=%s\t%s\n", from, to);
}
// ── 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
// A Make-style dependency file the COMMAND writes as a side effect (gcc/
// clang `-MD -MF`, glslangValidator `--depfile`, glslc `-MD -MF`, slangc
// `-depfile`). Empty (the default) means the rule emits none, and the
// action's re-run set is exactly its declared `inputs`, as before this
// field existed. See BuildAction::depfile (modules/manifest/src/types.cppm)
// for why `inputs` alone cannot express what this covers.
const char* depfile = "";
action& input(const char* p) { add(inputs_, p); return *this; }
action& output(const char* p) { add(outputs_, p); return *this; }
action& arg(const char* a) { add(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_, n); return *this; }
action& imports(const char* n) { add(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_, 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");
// Optional and omitted rather than sent empty: an action that never
// sets this must serialise to the SAME bytes it did before the field
// existed, because this payload is the cache key `apply()` stores
// verbatim (see the comment there) — an unconditional `"depfile":""`
// on every action would perturb the cache for every build.mcpp that
// has nothing to do with depfiles. The decoder's default (empty
// string) is identical either way, so omission costs nothing on read.
if (depfile[0]) { std::printf(",\"depfile\":"); esc(depfile); }
// Set only when the process could not allocate memory for a list.
// A declaration cut short would otherwise be INVALID rather than
// obviously wrong -- the engine turns this marker into a diagnostic
// that names the cause, instead of a generic "malformed action".
if (overflow_) std::printf(",\"overflow\":true");
std::printf(",\"inputs\":[%s]", inputs_.c_str());
std::printf(",\"outputs\":[%s]", outputs_.c_str());
std::printf(",\"command\":[%s]", command_.c_str());
std::printf(",\"provides\":[%s]", provides_.c_str());
std::printf(",\"imports\":[%s]", imports_.c_str());
std::printf(",\"targets\":[%s]", targets_.c_str());
std::printf("}\n");
}
private:
// One list field, held already serialised (`"a","b"`) so submit() prints
// it as it is. Owning and std-free, and both words are constraints this
// module carries: it may be compiled before a std BMI exists, so it must
// not `import std;`, and its exported interface must name no std type, so
// `std::string` may not appear in a signature. Neither forbids the heap:
// storage is `realloc` from the `<cstdlib>` already in the global module
// fragment, and no exported signature mentions this type.
//
// An earlier revision held six fixed arrays (8192 bytes for `inputs` and
// `outputs`, chosen for a protoc command line) and a declaration that did
// not fit was refused. The bound was in bytes of serialised JSON, so a
// consumer's checkout depth decided whether a resource list of 44 files
// fit (HuxerUI#130 measured the margin at 45 bytes), and `outputs` is the
// one list an author cannot shorten: an output the program does not name
// cannot be built, and there is no depfile for outputs. See
// .agents/docs/2026-09-13-four-upstream-asks-from-a-ui-framework.md.
struct list {
char* p = nullptr;
unsigned long len = 0, cap = 0;
list() = default;
list(const list& o) { take(o); }
list& operator=(const list& o) { if (this != &o) { len = 0; take(o); } return *this; }
~list() { std::free(p); }
const char* c_str() const { return p ? p : ""; }
// Grows by doubling. False only when the allocator refuses.
bool reserve(unsigned long need) {
if (need <= cap) return true;
unsigned long c = cap ? cap : 256;
while (c < need) c *= 2;
void* q = std::realloc(p, c);
if (!q) return false;
p = static_cast<char*>(q);
cap = c;
return true;
}
bool put(char c) {
if (!reserve(len + 2)) return false;
p[len++] = c;
p[len] = 0;
return true;
}
void take(const list& o) {
if (!o.len) { if (p) p[0] = 0; return; }
if (!reserve(o.len + 1)) return;
for (unsigned long i = 0; i <= o.len; ++i) p[i] = o.p[i];
len = o.len;
}
};
list inputs_, outputs_, command_, provides_, imports_, targets_;
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('"');
}
// Appends one JSON string literal, with the escaping `esc` applies, so a
// list entry and a scalar field are encoded by one rule. A payload that
// decoded under the fixed-array revision is encoded to the same bytes
// here: that revision escaped `"` and `\\` and passed control characters
// through, and a control character passed through was not JSON, so no
// payload the engine accepted contained one.
bool add(list& l, const char* s) {
bool ok = true;
if (l.len) ok = ok && l.put(',');
ok = ok && l.put('"');
for (const char* p = s; ok && *p; ++p) {
unsigned char c = (unsigned char)*p;
if (c == '"' || c == '\\') { ok = l.put('\\') && l.put((char)c); continue; }
if (c < 0x20) {
static const char hex[] = "0123456789abcdef";
ok = l.put('\\') && l.put('u') && l.put('0') && l.put('0')
&& l.put(hex[c >> 4]) && l.put(hex[c & 0xf]);
continue;
}
ok = l.put((char)c);
}
ok = ok && l.put('"');
if (!ok) overflow_ = true;
return ok;
}
};
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"); }
// The device axis of this build: `cuda12.9+{sm_89} ptx>=89`, or "" when the
// build asks for no accelerator. Already resolved (`--accel` / `--no-accel`
// over `[build] accel`), so a rule package derives its architecture flags
// from HERE and the set is written once, in the manifest. What the string
// means beyond "backend, version, architectures, floor" is the package's
// business: the engine never learns what `sm_89` is.
inline const char* accel() { return env_or("MCPP_ACCEL"); }
// The device-kind sources (`.cu`, `.hip`, ...) this package's `sources` match
// under the current accel, package-root-relative, one per line, "" when there
// are none. The engine compiles none of them; the rule package this program
// imports turns each into an `mcpp::action`. Already narrowed: a glob written
// as `{ glob = "...", accel = "..." }` whose constraint the build does not
// satisfy contributes nothing, so `--no-accel` yields an empty list.
inline const char* device_sources() { return env_or("MCPP_DEVICE_SOURCES"); }
inline const char* out_dir() { return env_or("MCPP_OUT_DIR"); }
// Where the TOOLCHAIN mcpp resolved for this build lives — the payload root,
// the directory whose `bin/` holds the driver.
//
// This exists so a package never has to DECLARE a toolchain. A package that
// needs headers the toolchain ships (libc++'s, for a freestanding standard
// library subset) previously had to put `xim:llvm` in `[xlings] deps`, which
// pinned it to one implementation — and the measured fact is that the same
// subset works over libstdc++'s freestanding mode too, so pinning was not
// merely inelegant, it closed a road. Asking here follows whatever
// `[toolchain]` actually resolved.
inline const char* toolchain_dir() { return env_or("MCPP_TOOLCHAIN_DIR"); }
// The two flags mcpp passes to its own compiler: the `--sysroot` and the
// directory it names with `-B`. Either is empty when mcpp passes none.
//
// These are for A SECOND COMPILER — one this rule package runs and mcpp did
// not resolve. Such a compiler starts with no idea where anything is, and on a
// subos the C library is not at `/usr/include` and the assembler is not at
// `/usr/bin`; the first `#include` it reaches then fails on `features.h`.
// Forwarding these two makes it see what mcpp's own compiler sees.
//
// NOT `sysroot_dir()`, four lines down. That one answers a question about
// the TARGET's tier and is empty on a hosted target, which is exactly the case
// this pair exists for.
inline const char* toolchain_sysroot() { return env_or("MCPP_TOOLCHAIN_SYSROOT"); }
inline const char* toolchain_binutils_dir() { return env_or("MCPP_TOOLCHAIN_BINUTILS_DIR"); }
// Which compiler resolved: "gcc", "clang", "msvc", or "" if none did.
//
// Ask this rather than inferring it from `toolchain_dir()`. The two questions
// a package has actually needed it for are which runtime library holds the
// routines the compiler emits calls to, and which spelling of a binutils tool
// exists beside the driver — and both have a different right answer per family
// rather than per version or per payload.
inline const char* compiler() { return env_or("MCPP_COMPILER"); }
// Which C++ standard library resolved: "libstdc++", "libc++", "msvc-stl", or
// "" if no toolchain did.
//
// A SEPARATE QUESTION FROM `compiler()`, and the reason this exists. clang
// links libc++ on one machine and libstdc++ on another and reports "clang"
// either way, while the two implementations differ in what they accept: a
// `unique_ptr` to an incomplete type destroyed in a header compiles under
// libstdc++ and does not under libc++. A package that must refuse such a
// configuration by name cannot ask `compiler()`, because that answer would
// also refuse the configuration that works.
//
// "cxx" is in the name deliberately. `MCPP_TARGET_LIBC` is the C library; this
// is the C++ one, and in an ecosystem that names glibc and musl constantly the
// two must not share a word.
inline const char* cxx_stdlib() { return env_or("MCPP_CXX_STDLIB"); }
// Where the TARGET's C library lives, for targets that have one of their own
// (today: bare metal). Same argument one line up: the libc is a property of
// the target, mcpp resolves it from the target's own row, and a package that
// needs to name a FILE inside it (a linker script) asks rather than declares.
//
// Empty on a hosted target — there the libc comes with the compiler payload or
// through the runtime binding, and nothing has to look for it.
inline const char* sysroot_dir() { return env_or("MCPP_TARGET_SYSROOT"); }
// ── Three answers a board-support package would otherwise hardcode ───────────
//
// The coupling these remove does not appear in any manifest. A board package
// can declare no dependency on LLVM and none on picolibc — and still be unable
// to serve a second toolchain or a second C library, because it wrote their
// names into its `build.mcpp`. A declared dependency is visible and reviewable;
// a hardcoded name fails only when something is swapped, which is exactly when
// nobody is looking for it.
//
// The rule that decides what belongs here is the one the layering already
// uses: LOCATION IS A TARGET FACT, SELECTION IS A BOARD FACT.
// The compiler's builtins library, by bare name: `clang_rt.builtins-riscv64`
// for an LLVM payload, `gcc` for a GCC one.
//
// A board does not choose whether to have builtins — every freestanding link
// needs them, and on rv64 the trigger is picolibc's printf doing 128-bit
// shifts, which the ISA has no instruction for. What varies is only which
// implementation the resolved toolchain ships, and that is not a board fact.
// Empty on a hosted target, where the driver links them without being asked.
inline const char* target_builtins_lib() { return env_or("MCPP_TARGET_BUILTINS_LIB"); }
// The C library's sub-directory for this target's ISA profile, e.g.
// `rv64gc/lp64d`. It is the multilib convention of whichever C library the
// target resolved, with no board input at all — a board that wanted a
// different layout would be using a different C library.
//
// Empty when the target has no C library of its own (the zero-libc tier, or a
// hosted target).
inline const char* target_libc_profile() { return env_or("MCPP_TARGET_LIBC_PROFILE"); }
// The C library's package name, e.g. `picolibc-riscv`; empty on the zero-libc
// tier and on hosted targets.
//
// This one does NOT remove a coupling — it makes one visible. A board package
// that genuinely must differ between picolibc and newlib (the crt0 object is
// named differently, and that IS a board choice) can branch on this instead of
// assuming. An explicit branch can be read and can be extended; an assumption
// baked into a string literal can be neither.
inline const char* target_libc() { return env_or("MCPP_TARGET_LIBC"); }
inline const char* manifest_dir() { return env_or("MCPP_MANIFEST_DIR"); }
// THE PACKAGE THIS PROGRAM IS BUILDING, BY NAME.
//
// A rule package that generates a consumer-facing declaration has to name it,
// and every name it produces is derived from this one: the module a project
// imports, the namespace the accessors sit in, the symbols in a generated
// header. Before these existed the closest thing available was the leaf of
// `manifest_dir()`, which is a directory name rather than a package name --
// so a package called `vulkan-saxpy` in a directory called `app` generated
// `app.shaders`, and every `<something>/app/` in a workspace claimed it.
//
// Empty under an engine older than 2026.9.7.1, which a rule reads as "fall
// back to whatever you did before". That is what keeps an already-published
// rule package working unchanged.
inline const char* package_name() { return env_or("MCPP_PKG_NAME"); }
inline const char* package_namespace() { return env_or("MCPP_PKG_NAMESPACE"); }
// THE REST OF `[package]`, BECAUSE A DISTRIBUTABLE CARRIES IT.
//
// `package_name()` above exists so a generated declaration can be named. These
// exist for the other member of the collection: every installer format states a
// version, and most state a description, a licence and a maintainer. A member
// without them has to ask the PROJECT to restate values mcpp has already
// parsed, in the member's own options, where the copy drifts from `[package]`
// and nothing can detect that it has.
//
// `package_authors()` is a ';'-separated list -- not ',', because an author is
// conventionally `Name <mail@host>` and a name may carry a comma.
//
// Empty under an engine older than 2026.9.11.1. A member that needs one must
// say so itself when it is empty, naming the value it wanted: only the member
// knows whether the absence is fatal.
inline const char* package_version() { return env_or("MCPP_PKG_VERSION"); }
// THE PROJECT'S FLOOR FOR THIS TRIPLE, IN THE PLATFORM'S OWN WORDS (#622
// A11): `14.0` on macOS, `18.0` on iOS, an API level on Android, empty
// everywhere else. One function on the engine side, `min_platform_version`,
// already answers this for the compiler flag and the fingerprint slot; before
// this a member (`dist-apple`'s `minimum_system_version`, `dist-apk`'s
// `minSdkVersion`) had no channel to it and restated the value in its own
// options, where it silently drifted from the manifest's actual answer.
// Empty under an engine older than this, which a member reads as "restate the
// value yourself", the behaviour every consumer had before.
inline const char* min_platform_version() { return env_or("MCPP_TARGET_MIN_PLATFORM_VERSION"); }
inline const char* package_description() { return env_or("MCPP_PKG_DESCRIPTION"); }
inline const char* package_license() { return env_or("MCPP_PKG_LICENSE"); }
inline const char* package_authors() { return env_or("MCPP_PKG_AUTHORS"); }
inline const char* package_repo() { return env_or("MCPP_PKG_REPO"); }
// WHICH DISTRIBUTABLE THIS PASS WAS ASKED FOR, or "" for every ordinary build.
//
// The empty value is the one that carries the meaning: a member gates its
// submission on this, so `mcpp build` has the graph it always had and a dist
// edge exists only in the pass that wants one. See `provides_pack_format` for
// the half that must NOT be gated.
inline const char* pack_format() { return env_or("MCPP_PACK_FORMAT"); }
// WHERE `mcpp pack` HAS ALREADY STAGED THE CLOSURE, absolute; "" when this
// build is not packing.
//
// The tree is what `mcpp pack` computes and then, until this existed, threw
// away: the dependency closure after the strip policy, the debug-symbol split
// and `include`/`exclude`. It is a BUNDLE tree -- `bin/`, `lib/`, relocatable,
// rooted anywhere -- which is what an AppImage, a `.app` and an `.msi` want as
// it stands. A format that wants a root filesystem instead (`.deb`, `.rpm`)
// owns the re-layout, because which directory a file belongs in is that
// format's knowledge and not the engine's.
//
// READ IT HERE TO DECIDE, WRITE `${mcpp.stage_dir}` INTO THE ACTION. The
// directory exists while this program runs, so a member enumerates it to learn
// which of `bin/`, `lib/`, `share/` the tree actually has; the action's command
// then names it through the placeholder, so the path in the graph and the path
// this program read cannot disagree.
inline const char* pack_stage_dir() { return env_or("MCPP_PACK_STAGE_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);
}
// The payload directory of a package declared in `[xlings] deps`.
//
// An INTERFACE, not a naming convention. `dep_dir` answers for mcpp
// dependencies and cannot answer for xlings ones: they are a different
// namespace with a different store layout, and a build.mcpp that reconstructed
// `<home>/data/xpkgs/<ns>-x-<name>/<version>` itself would be encoding store
// internals that mcpp is free to change — the exact thing `dep_dir` exists to
// avoid ("instead of reverse-engineering the store layout").
//
// Ask with the spelling the manifest used:
//
// deps = ["xim:picolibc-riscv@1.8.12"]
// xpkg_dir("xim", "picolibc-riscv") // exact, and preferred
// xpkg_dir("picolibc-riscv") // bare name
//
// The namespaced form is tried first and answers only for a package declared
// under that namespace. The bare form is a convenience for the common single
// declaration; when two namespaces declare the same name, only the namespaced
// form can say which one is meant, and the bare one answers for the first
// declared. Returns "" when the package was not declared or is not installed —
// a caller that needs it should say so itself, because only it knows whether
// the absence is fatal.
inline const char* xpkg_dir(const char* ns, const char* name) {
char buf[256] = "MCPP_XPKG_";
unsigned long o = 10;
auto put = [&](const char* s) {
for (const char* p = s; *p && o + 6 < 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 : '_';
}
};
if (ns && *ns) { put(ns); if (o + 6 < sizeof buf) buf[o++] = '_'; }
put(name);
buf[o++] = '_'; buf[o++] = 'D'; buf[o++] = 'I'; buf[o++] = 'R'; buf[o] = 0;
return env_or(buf);
}
inline const char* xpkg_dir(const char* name) { return xpkg_dir("", name); }
// 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> {
// THE ONLY PLACE THIS ARGV IS EVER OBSERVABLE ON A SUCCESSFUL RUN.
// A failing compile shows its own command implicitly (the compiler's
// diagnostics name the headers it looked for and did not find); a
// passing one otherwise leaves no trace of which `-isystem` rows it
// carried — which is exactly the fact that distinguishes a host
// toolchain whose C library was attached from one whose wasn't (#622,
// `host_tc_for_build_program`'s cross branch). `verbose()` always logs
// it; MCPP_VERBOSE=1 additionally echoes it to stderr.
std::string joined;
for (auto const& a : argv) { if (!joined.empty()) joined += ' '; joined += a; }
mcpp::log::verbose("buildmcpp-host",
std::format("mcpp module {}: {}", what, joined));
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