-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathprepare_inputs.cppm
More file actions
788 lines (762 loc) · 39 KB
/
Copy pathprepare_inputs.cppm
File metadata and controls
788 lines (762 loc) · 39 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
// mcpp.build.prepare_inputs — the inputs a build plan is derived FROM, split out
// of mcpp.build.prepare.
//
// WHY. `prepare.cppm` was 6521 lines and 16.4s to compile — 22% of this
// repository's critical build path and its only real outlier. A module that
// large is worth splitting on architecture grounds alone; the build-time effect
// is a bonus and, since the split schedule landed, a smaller one (an interface
// now blocks importers for ~22% of its compile rather than all of it).
//
// WHAT A SPLIT HAS TO BE TO HELP THE CRITICAL PATH. Extracting a piece that
// `prepare` then imports makes the chain LONGER, not shorter: `... -> this ->
// prepare -> ...` is still serial, and prepare only sheds the cost this module
// now pays. It shortens the path only for consumers that can import THIS
// instead of prepare — which is why the pieces chosen here are the ones with no
// dependency on the rest of prepare: cfg() predicate evaluation and the
// fingerprint canonicalisers.
//
// They are re-exported from `mcpp.build.prepare`, so no caller had to change.
export module mcpp.build.prepare_inputs;
import std;
import mcpp.diag;
import mcpp.manifest;
import mcpp.modgraph.graph;
import mcpp.modgraph.scanner;
import mcpp.platform;
import mcpp.toolchain.model;
import mcpp.toolchain.fingerprint;
import mcpp.toolchain.triple;
import mcpp.ui;
export namespace mcpp::build {
// ── L1 platform-conditional config: cfg() predicate evaluation ──────────────
// Context = the RESOLVED target's coordinates. A `[target.'cfg(...)'.build]`
// predicate is evaluated against this (target triple for a cross build, host
// for a native build), so conditional flags follow what the binary will run on
// — not the build host. See the manifest design doc.
namespace cfgpred {
// `triple` is the RESOLVED target — the host's for a native build, the
// --target one for a cross build. It is a member rather than a parameter of
// `matches()` because a bare-triple predicate and a `cfg(...)` predicate are
// two spellings of ONE question, and they must be answered from one value.
//
// They were not. `matches()` used to take the raw `--target` string alongside
// this context and short-circuit on `if (triple.empty()) return false;`, while
// `context_for()` below fell back to the host. So `cfg(linux)` matched a native
// build and `[target.'x86_64-linux-gnu'.build]` — the same statement about the
// same machine — did not, silently. That shape is the worst kind: CI passes
// `--target` and is green, the developer's plain `mcpp build` drops the flags,
// and the failure surfaces at link time naming a symbol instead of a predicate.
// manifest/types.cppm's ConditionalConfig has documented the fallback since it
// was written; this makes the bare-triple branch honour it.
// The five target-side layers (docs/14) join the triple coordinates here, but
// they arrive LATER and from a different place: the triple is known before
// dependency resolution, a layer only after it, because a package in the graph
// may supply the C library. `layersKnown` is the difference, and it is a member
// rather than an inference from emptiness because "no layer resolved" and "not
// resolved yet" are different answers and only one of them may be reported.
//
// `compiler` carries the FAMILY (`llvm`), never the driver (`clang`) — #494
// settled that, on the grounds that every place a user writes the name they
// write the family, and reporting the driver would make
// `requires = ["mcpp:compiler=llvm"]` permanently unsatisfiable.
struct Ctx {
std::string os, arch, family, env, triple;
bool layersKnown = false;
std::string compiler, compilerRuntime, kernelAbi, cAbi, cxxAbi;
// The first MULTI-VALUED layer. One build can enable several accelerator
// backends at once, which is what an inference framework shipping CUDA and
// ROCm device code in one artifact requires, so this layer holds a set
// rather than the single answer the other five hold.
std::vector<std::string> accelerators;
std::string_view layer_value(std::string_view k) const {
if (k == "compiler") return compiler;
if (k == "compiler-runtime") return compilerRuntime;
if (k == "kernel-abi") return kernelAbi;
if (k == "c-abi") return cAbi;
if (k == "c++-abi") return cxxAbi;
return {};
}
// THE SINGLE PLACE THE MULTI-VALUED CASE DIFFERS.
//
// A multi-valued layer compares by MEMBERSHIP, and it does so everywhere —
// not only inside `any(...)`. The alternative, letting `any(...)` mean
// membership while a bare key meant set equality, would make a combinator
// change the meaning of its operand: `all(accelerator = "cuda",
// accelerator = "rocm")` would then be unsatisfiable rather than "both
// backends are enabled". Membership everywhere keeps `any`/`all`/`not`
// pure boolean combinators, and a single-backend build still answers
// `accelerator = "cuda"` true and `accelerator = "rocm"` false.
// `none` IS THE EMPTY SET, AND AN OPEN VOCABULARY CANNOT SAY THAT BY
// ENUMERATION.
//
// A CPU fallback used to be written `not(any(accelerator = "cuda",
// accelerator = "vulkan"))`. `accelerator`'s vocabulary is OPEN by design
// -- docs/20 states that a fifth backend is a package rather than an
// engine change -- so that predicate's meaning changes the day a fifth one
// exists: every fallback already written silently starts matching a build
// that named the new backend. The failure is that the CPU implementation
// and the device implementation compile together, or that neither does.
//
// The spelling is the one this manifest already uses for the same idea:
// `os = "none"` is bare metal (docs/05 section 2.7.2). One word, one
// meaning, no new vocabulary.
//
// NOT `cpu`. That would put a second question on this axis -- the axis
// answers "which device compiler, which architecture", and the CPU needs
// neither -- and it would leave `cfg(accelerator = "cpu")` undecided under
// `accel = "cuda"`: true makes the fallback compile alongside the device
// path and destroys the mutual exclusion the seam exists for; false forces
// every existing manifest to write `accel = "cuda, cpu"`.
//
// A build where BOTH a CPU path and a device path are wanted needs none of
// this: the CPU sources go in the unconditional `[build] sources` and the
// device sources under `cfg(accelerator = "x")`. `not(...)` was only ever
// needed for a mutually exclusive seam, which is the case this repairs.
bool layer_matches(std::string_view k, std::string_view v) const {
if (k == "accelerator") {
if (v == "none") return accelerators.empty();
return std::ranges::find(accelerators, v) != accelerators.end();
}
return layer_value(k) == v;
}
};
// Derive the cfg context from the resolved --target triple, falling back to
// the host for a native build. Parsing goes through triple.cppm — the single
// triple parser — so the cfg vocabulary IS the canonical triple vocabulary
// (os: linux|macos|windows, arch: GNU spellings, env: gnu|musl|msvc), and
// alias spellings ("x86_64-w64-mingw32") evaluate identically to canonical.
inline Ctx context_for(std::string_view targetTriple) {
namespace triple = mcpp::toolchain::triple;
Ctx c;
auto t = targetTriple.empty()
? std::optional<triple::Triple>(triple::host_triple())
: triple::parse(targetTriple);
if (t) {
c.os = t->os;
c.arch = t->arch;
c.env = t->env;
c.family = t->family();
// Canonical spelling on BOTH sides of the later comparison, so an
// `x86_64-w64-mingw32` key and an `x86_64-windows-gnu` build agree.
c.triple = t->str();
} else {
// Escape-hatch triple outside the language: only the leading arch
// segment is derivable; other dimensions stay empty (never match).
auto dash = targetTriple.find('-');
c.arch = std::string(dash == std::string_view::npos ? targetTriple
: targetTriple.substr(0, dash));
// Unparseable: keep it verbatim so the exact-string fallback in
// `matches()` can still hit an explicit escape-hatch section.
c.triple = std::string(targetTriple);
}
return c;
}
// ── The cfg() vocabulary ────────────────────────────────────────────────────
//
// ONE LIST PER CATEGORY, AND EVERY READER READS IT. #540 found four
// hand-written copies of other vocabularies in this repository, all drifted;
// the diagnostic added below would have been the fifth if it had transcribed
// these names instead of sharing them.
//
// TRIPLE keys are answerable from the target triple alone, which is what the
// conditional merge has before dependency resolution. LAYER keys name a
// target-side layer (docs/14) and are answerable only after the graph is
// resolved — see `merge_layer_conditional_config` in prepare.cppm for the
// second pass that evaluates them.
inline constexpr std::string_view kCfgTripleKeys[] = {
"arch", "env", "family", "os",
};
// THE LAYER KEYS SPLIT BY SCHEDULE, not by subject matter.
//
// The five in `kCfgLayerKeys` are answered BY dependency resolution: which C
// library, which compiler, which compiler runtime the graph settled on. A
// predicate naming one cannot be evaluated before the graph exists, which is
// why the second merge pass owns them and why a dependency conditioned on one
// is refused -- it would decide the answer it is asking for.
//
// `accelerator` is not like them. It is an INPUT: `--accel`, or `[build]
// accel`, read near the top of prepare() and known before the first package is
// resolved. Grouping it with the five made three things wrong at once. A
// payload could not be gated on the device it is for, so a CPU-only build of a
// project that also has a CUDA island downloaded the whole vendor toolkit. A
// dependency under `cfg(accelerator = ...)` was warned about and dropped,
// though nothing about it is circular. And the section was carried to the late
// pass for no reason at all.
//
// Both sets are the cfg VOCABULARY, so `is_cfg_layer_key` still answers for
// either; only the schedule question (`uses_layer`) distinguishes them.
inline constexpr std::string_view kCfgEarlyLayerKeys[] = {
"accelerator",
};
inline constexpr std::string_view kCfgLayerKeys[] = {
"c++-abi", "c-abi", "compiler", "compiler-runtime",
"kernel-abi",
};
inline constexpr std::string_view kCfgBarewords[] = {
"linux", "macos", "unix", "windows",
};
// Answerable before resolution. Its value comes from the build's own accel.
inline bool is_cfg_early_layer_key(std::string_view k) {
return std::ranges::find(kCfgEarlyLayerKeys, k) != std::end(kCfgEarlyLayerKeys);
}
// Answerable only after resolution -- what the second merge pass owns.
inline bool is_cfg_late_layer_key(std::string_view k) {
return std::ranges::find(kCfgLayerKeys, k) != std::end(kCfgLayerKeys);
}
// The vocabulary question: is this a layer key at all. Both sets, because an
// unknown token must stay unknown and `accelerator` is not one.
inline bool is_cfg_layer_key(std::string_view k) {
return is_cfg_early_layer_key(k) || is_cfg_late_layer_key(k);
}
// Recursive-descent evaluator over the inside of `cfg(...)`:
// expr := all(list) | any(list) | not(expr) | key="value" | bareword
// key ∈ kCfgTripleKeys ∪ kCfgLayerKeys bareword ∈ kCfgBarewords
//
// THE EVALUATOR IS ALSO THE VALIDATOR. `seenKeys`/`seenWords` let one
// traversal answer three questions — does it match, does it name a layer, does
// it name anything at all — because a separate validator would be a SECOND
// parser of the same grammar, and this repository has already paid for one of
// those (`[hooks]` re-parsing mcpp.toml and reporting every TOML error as an
// invalid hook configuration).
struct Parser {
std::string_view s; std::size_t i = 0; const Ctx& c;
std::vector<std::string>* seenKeys = nullptr; // every `key=` key, in order
std::vector<std::string>* seenWords = nullptr; // every bareword
// Two more optional taps, used only by `os_only_platforms` below and left
// null for `scan_predicate`'s callers so this is additive, not a rewrite.
// `seenKV` carries the VALUE a `seenKeys` entry does not, and `combinator`
// records whether `all`/`any`/`not` fired anywhere in the traversal — an
// OS-only predicate is a single term, and a combinator, even one built
// entirely from OS terms, is not the platform it happens to reduce to on
// this one evaluation.
std::vector<std::pair<std::string, std::string>>* seenKV = nullptr;
bool* combinator = nullptr;
void ws() { while (i < s.size() && std::isspace((unsigned char)s[i])) ++i; }
bool eat(char ch) { ws(); if (i < s.size() && s[i] == ch) { ++i; return true; } return false; }
std::string ident() {
ws(); std::size_t b = i;
// `-` and `+` ARE IDENTIFIER CHARACTERS, because the layer names are
// `c-abi`, `c++-abi`, `compiler-runtime` and `kernel-abi`. Without them
// `cfg(c-abi = "musl")` scanned as the bareword `c` followed by
// garbage, so the one thing a diagnostic could report was the letter
// `c`. No valid pre-existing predicate contains either character
// outside a quoted value, so widening the scanner changes nothing that
// used to parse.
while (i < s.size() && (std::isalnum((unsigned char)s[i])
|| s[i] == '_' || s[i] == '-' || s[i] == '+')) ++i;
return std::string(s.substr(b, i - b));
}
std::string str() {
ws(); if (i >= s.size() || s[i] != '"') return {};
++i; std::size_t b = i; while (i < s.size() && s[i] != '"') ++i;
auto v = std::string(s.substr(b, i - b)); if (i < s.size()) ++i; return v;
}
bool match_alias(const std::string& a) {
if (seenWords) seenWords->push_back(a);
if (a == "windows") return c.os == "windows";
if (a == "linux") return c.os == "linux";
if (a == "macos") return c.os == "macos";
if (a == "unix") return c.family == "unix";
return false; // unknown bareword → no match, and `seenWords` reports it
}
bool match_kv(const std::string& k, const std::string& v) {
if (seenKeys) seenKeys->push_back(k);
if (seenKV) seenKV->emplace_back(k, v);
if (k == "os") return c.os == v;
if (k == "arch") return c.arch == v;
if (k == "family") return c.family == v;
if (k == "env") return c.env == v;
// `accelerator` is answerable whenever the context carries the build's
// accel, which is from the first pass onward -- see kCfgEarlyLayerKeys.
if (is_cfg_early_layer_key(k)) return c.layer_matches(k, v);
// The other layer keys are not answerable until the target side is
// resolved. In the first (triple-only) pass this returns false and the
// section is skipped — which is correct, because the second pass owns
// it and would otherwise append the same inputs twice through
// `append()`.
if (is_cfg_late_layer_key(k))
return c.layersKnown && c.layer_matches(k, v);
return false;
}
bool expr() {
std::string id = ident();
if (id == "all" || id == "any") {
if (combinator) *combinator = true;
eat('(');
bool acc = (id == "all");
ws();
if (!(i < s.size() && s[i] == ')')) {
do { bool r = expr(); acc = (id == "all") ? (acc && r) : (acc || r); }
while (eat(','));
}
eat(')');
return acc;
}
if (id == "not") {
if (combinator) *combinator = true;
eat('('); bool r = expr(); eat(')'); return !r;
}
ws();
if (i < s.size() && s[i] == '=') { ++i; return match_kv(id, str()); }
return match_alias(id);
}
};
// Evaluate a `[target.<predicate>]` key. Returns the cfg() result, or — for a
// non-cfg key (a bare triple) — an exact match against the resolved triple.
//
// The resolved triple comes from `c`, never from a second parameter: see the
// note on Ctx for what having two of them cost.
inline bool matches(const std::string& predicate, const Ctx& c) {
const std::string_view triple = c.triple;
std::string_view k = predicate;
if (k.starts_with("cfg(") && k.ends_with(")")) {
Parser p{ k.substr(4, k.size() - 5), 0, c };
return p.expr();
}
// Bare OS/family alias sugar: `[target.linux]` ≡ `[target.'cfg(linux)']`.
// These aliases are never valid triples (no dash), so there is no ambiguity
// with the exact-triple namespace. Evaluated as the cfg bareword.
if (predicate == "windows" || predicate == "linux" ||
predicate == "macos" || predicate == "unix") {
Parser p{ predicate, 0, c };
return p.expr();
}
// Bare-triple match, spelling-independent: a `[target.x86_64-w64-mingw32]`
// key matches a resolved `x86_64-windows-gnu` build (and vice versa) —
// both normalize through triple::parse. Unparseable keys (the explicit-
// section escape hatch) fall back to exact string comparison.
//
// `c.triple` is populated for every build, native included, so this is a
// guard and no longer a behaviour: it used to be the line that made a
// bare-triple section silently inert without `--target`.
if (triple.empty()) return false;
if (auto p = mcpp::toolchain::triple::parse(predicate)) {
if (auto rt = mcpp::toolchain::triple::parse(triple))
return p->str() == rt->str();
}
return predicate == triple;
}
// ── One traversal, three answers ────────────────────────────────────────────
//
// `scan_predicate` runs the REAL evaluator with a throwaway context purely to
// record which tokens the predicate names. Everything below derives from it, so
// the grammar has exactly one implementation and a predicate that the evaluator
// cannot answer is, by construction, a predicate the diagnostic reports.
struct PredicateScan {
std::vector<std::string> keys; // every `key=` key, in order
std::vector<std::string> barewords; // every bareword
};
inline PredicateScan scan_predicate(const std::string& predicate) {
PredicateScan out;
std::string_view k = predicate;
// Only the `cfg(...)` namespace. A bare alias or a bare triple is the
// documented escape hatch — `matches()` falls back to an exact string
// comparison for keys it cannot parse — and validating it would reject the
// explicit-section spelling that hatch exists to allow.
if (k.starts_with("cfg(") && k.ends_with(")")) {
Ctx scratch;
Parser p{ k.substr(4, k.size() - 5), 0, scratch, &out.keys, &out.barewords };
(void)p.expr();
}
return out;
}
// ── #630 item 7: an OS-only selector is a platform ──────────────────────────
//
// `mcpp emit xpkg`'s descriptor has exactly three blocks — `linux`, `macosx`,
// `windows` (`mcpp::pm::emit_xpkg`) — and a `[target.<selector>]` predicate is,
// in general, a question about more axes than the descriptor has (arch, env, a
// target-side layer, a feature gate via a combinator). But a predicate that
// asks about NOTHING but the operating system answers a question the
// descriptor already has a block for, so it can be folded into that block
// instead of only producing the `publish/target-axis-tools` advisory.
//
// Built on the SAME `Parser` the evaluator (`matches`) and the diagnostic scan
// (`scan_predicate`) use, not a second reading of the predicate text: it runs
// the one grammar with two more optional taps (`seenKV`, `combinator`) and
// then asks a structural question of the result, rather than pattern-matching
// the source string. A hand-rolled string check here would be a second parser
// of `cfg(...)`, which is the shape this repository has already paid for once
// (`[hooks]` re-parsing mcpp.toml; see the comment on `Parser` above).
//
// Deliberately conservative: a combinator disqualifies the predicate even when
// every operand it combines is itself an OS term. `cfg(any(linux, macos))` is
// true on a broader set of machines than "linux" or "macosx" alone, but the
// descriptor's blocks are per platform, and folding a compound predicate into
// two of them would silently say "installed on this platform" for a predicate
// whose truth also depends on how it combines — `cfg(not(windows))` is the
// case that makes this concrete: it is exactly as OS-only as `cfg(windows)`
// syntactically, and answers a different, unbounded set of platforms (every
// platform this vocabulary does not yet name, not just "macosx and linux").
// Keeping the warning for every combinator, `not` included, means a predicate
// this function accepts is always a single OS term with no combinator wrapped
// around it — the same seven forms design record 2026-09-13-630 §8.2 lists.
//
// Returns the descriptor block names (`"linux"`, `"macosx"`, `"windows"`) an
// OS-only predicate maps onto, in the same spelling `emit_xpkg` and
// `XlingsConfig::workspaceByPlatform` use; empty for anything else, including
// a predicate this function cannot classify as OS-only at all.
inline std::vector<std::string> os_only_platforms(const std::string& predicate) {
std::string_view text = predicate;
const bool wrapped = text.starts_with("cfg(") && text.ends_with(")");
// The bare-alias sugar (`[target.windows]` ≡ `[target.'cfg(windows)']`,
// docs/22) shares the grammar with `cfg(...)` — both are read by the same
// `Parser::expr()` in `matches()` — so both are eligible here. Anything
// else (an exact triple, or the unparsed escape hatch) names neither an
// OS nor a platform on its own.
static constexpr std::string_view kBareAliases[] = { "linux", "macos", "unix", "windows" };
if (!wrapped && std::ranges::find(kBareAliases, predicate) == std::end(kBareAliases))
return {};
std::string_view inner = wrapped ? text.substr(4, text.size() - 5) : text;
Ctx scratch;
std::vector<std::pair<std::string, std::string>> kv;
std::vector<std::string> words;
bool combinator = false;
Parser p{ inner, 0, scratch, nullptr, &words, &kv, &combinator };
(void)p.expr();
// A combinator, or more than one term, disqualifies the predicate — see
// the comment above for why even an all-OS combinator does.
if (combinator || kv.size() + words.size() != 1) return {};
if (words.size() == 1) {
// `unix` is the one bareword naming TWO platforms (docs/22: it means
// `c.family == "unix"`, which macOS and Linux both satisfy and Windows
// does not) — matching `matches()`'s own `match_alias`.
if (words[0] == "unix") return { "linux", "macosx" };
if (words[0] == "linux") return { "linux" };
if (words[0] == "windows") return { "windows" };
if (words[0] == "macos") return { "macosx" };
return {}; // an unrecognised bareword names no platform
}
// kv.size() == 1: only `os = "<value>"` answers a platform; any other key
// (arch, env, a layer, an unrecognised one) is not an OS question.
auto const& [key, value] = kv.front();
if (key != "os") return {};
if (value == "linux") return { "linux" };
if (value == "windows") return { "windows" };
if (value == "macos") return { "macosx" };
return {}; // `os = "<something this vocabulary does not name>"`
}
// True when the predicate names a target-side layer and therefore cannot be
// answered before dependency resolution. This is the classifier that keeps the
// two merge passes disjoint: `append()` is additive, so a section evaluated by
// both would contribute its inputs twice.
inline bool uses_layer(const std::string& predicate) {
auto scan = scan_predicate(predicate);
// The LATE keys only. A predicate naming `accelerator` is answered in the
// first pass, so claiming it here would move it to a pass that adds
// nothing and takes away the ability to gate a payload or a dependency on
// the device it is for.
return std::ranges::any_of(scan.keys,
[](auto const& k) { return is_cfg_late_layer_key(k); });
}
// Tokens outside the vocabulary. A predicate naming one of these used to
// evaluate to false in silence, which is indistinguishable from a predicate
// that correctly did not apply — so `[target.'cfg(c-abi = "musl")'.build]` was
// dropped without a word for the entire time docs/14 documented it.
inline std::vector<std::string> unknown_tokens(const std::string& predicate) {
auto scan = scan_predicate(predicate);
std::vector<std::string> out;
auto add = [&](const std::string& t) {
if (t.empty()) return;
if (std::ranges::find(out, t) == out.end()) out.push_back(t);
};
for (auto const& k : scan.keys)
if (std::ranges::find(kCfgTripleKeys, k) == std::end(kCfgTripleKeys)
&& !is_cfg_layer_key(k))
add(k);
for (auto const& w : scan.barewords)
if (std::ranges::find(kCfgBarewords, w) == std::end(kCfgBarewords))
add(w);
return out;
}
// The message body, built FROM the vocabulary rather than beside it.
inline std::string vocabulary_sentence() {
std::string keys, words;
for (auto k : kCfgTripleKeys) { if (!keys.empty()) keys += ", "; keys += k; }
for (auto k : kCfgLayerKeys) { if (!keys.empty()) keys += ", "; keys += k; }
for (auto w : kCfgBarewords) { if (!words.empty()) words += ", "; words += w; }
return std::format("Supported keys: {}. Supported barewords: {}.", keys, words);
}
} // namespace cfgpred
std::filesystem::path target_dir(const mcpp::toolchain::Toolchain& tc,
const mcpp::toolchain::Fingerprint& fp,
const std::filesystem::path& root)
{
// Canonical triple names the output directory (D1: `target/
// x86_64-windows-gnu/`, not the GNU spelling the compiler reports via
// -dumpmachine) — alias inputs land in the same directory. Triples
// outside the language keep their raw spelling.
auto triple = tc.targetTriple.empty() ? std::string{"unknown"} : tc.targetTriple;
if (auto t = mcpp::toolchain::triple::parse(triple)) triple = t->str();
return root / "target" / triple / fp.hex;
}
// Compose a stable canonical compile-flags string for fingerprinting.
// Exported so the "every build-variant knob is in here" invariant is machine-
// checkable: the profile knobs were absent for a long time precisely because
// nothing could assert on this string.
std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) {
std::string s;
s += "-std="; s += m.package.standard;
s += " -fmodules";
// macOS deployment target changes the effective compile triple
// (arm64-apple-macosxNN) — a std.pcm built for one target cannot be
// loaded by a TU compiled for another. Fold the resolved value
// (env override > [build] macos_deployment_target manifest default)
// into the fingerprint so switching targets rebuilds the BMI cache
// instead of dying with a module config mismatch.
//
// The built-in default floor (rustc-style) lives in the single
// resolver (platform::macos::deployment_target), so this rule, the
// flags and the std-module prebuild always agree — the 0.0.50-era
// attempt to inject a default here alone left the test build's
// std.pcm unstaged (import std failed wholesale on macos CI).
if constexpr (mcpp::platform::is_macos) {
auto dtv = mcpp::platform::macos::deployment_target(
m.buildConfig.macosDeploymentTarget);
if (!dtv.empty()) {
s += " macos_deployment_target=";
s += dtv;
}
}
if (!m.buildConfig.cStandard.empty()) {
s += " c_standard=";
s += m.buildConfig.cStandard;
}
for (auto const& flag : m.buildConfig.cflags) {
s += " cflag:";
s += flag;
}
for (auto const& flag : m.buildConfig.cxxflags) {
s += " cxxflag:";
s += flag;
}
// Explicit [build] dialect_cxxflags (auto-promoted ones are already in
// cxxflags above) — they change every BMI in the graph.
for (auto const& flag : m.buildConfig.dialectCxxflags) {
s += " dialect:";
s += flag;
}
for (auto const& flag : m.buildConfig.ldflags) {
s += " ldflag:";
s += flag;
}
// Per-glob flags (G4): full ordered serialization — glob + every list —
// so editing any entry (or reordering) re-fingerprints the output dir.
for (auto const& gf : m.buildConfig.globFlags) {
s += " globflags:"; s += gf.glob;
for (auto const& f : gf.cflags) { s += " gc:"; s += f; }
for (auto const& f : gf.cxxflags) { s += " gxx:"; s += f; }
for (auto const& f : gf.asmflags) { s += " gas:"; s += f; }
for (auto const& f : gf.defines) { s += " gd:"; s += f; }
}
// [build] module_extensions changes WHICH FILES ARE MODULE INTERFACES,
// i.e. the shape of the graph: which units emit a BMI, which objects link
// unconditionally, which ninja rule each unit gets. That is a build
// variant, so it belongs in the fingerprint — mcpp.toml's mtime alone only
// protects the fast path within one output dir, not the BMI cache.
//
// Contrast [build] build_program_timeout, which is deliberately absent:
// it changes no edge. See BuildConfig::buildProgramTimeoutSecs.
for (auto const& e : m.buildConfig.moduleExtensions) {
s += " modext:";
s += e;
}
// The resolved [profile] knobs. These are NOT in cflags/cxxflags: the
// profile block (see the profile resolution below) lands them in
// buildConfig.optLevel/debug/lto/strip and flags.cppm turns them into
// -O<n>/-g/-flto at command-construction time. Leaving them out made
// `--dev`, `--release` and `--profile dist` share ONE fingerprint, hence
// one target/<triple>/<fp>/ directory AND one global cache entry — so a
// release build could be served -O0 -g dependency objects. They are
// build-variant by definition; they belong here.
s += " opt="; s += m.buildConfig.optLevel;
s += " debug="; s += m.buildConfig.debug ? "1" : "0";
s += " lto="; s += m.buildConfig.lto ? "1" : "0";
s += " strip="; s += m.buildConfig.strip ? "1" : "0";
// #519 — the same reasoning as the profile knobs above, one axis later.
// The REQUEST is folded in rather than the derived `-fPIC`, because this
// string is built before the plan exists; the request is what a user
// edits and the flag is a function of it. Without this, flipping
// `dependency_linkage` reuses the previous configuration's output
// directory — measured on a two-package fixture, where both builds landed
// in `target/x86_64-linux-gnu/5d4a4a8a584ba471/` and the shared build's
// `libcore.so` was left sitting in the static build's `bin/`.
//
// Only appended when non-empty, so every existing build directory keeps
// its identity and this release rebuilds nothing.
if (!m.buildConfig.dependencyLinkage.empty()) {
s += " deplinkage=";
s += m.buildConfig.dependencyLinkage;
}
return s;
}
std::string canonical_package_build_metadata(
const std::vector<mcpp::modgraph::PackageRoot>& packages)
{
std::string s;
for (auto const& pkg : packages) {
s += "\npackage:";
s += pkg.manifest.package.namespace_;
s += "/";
s += pkg.manifest.package.name;
s += "@";
s += pkg.manifest.package.version;
s += " source=";
s += pkg.manifest.package.sourceProvenance;
// WHAT THIS PACKAGE IS BUILT WITH, AND NOT ONLY WHAT IT ASKS THE
// RUNTIME FOR.
//
// Only the root's compile inputs used to reach the fingerprint, through
// `canonical_compile_flags` on the root manifest. A DEPENDENCY's
// `[build] cflags` / `defines` / `sources` / per-glob flags reached
// nothing — so editing one left the fingerprint unchanged, the consumer
// kept the same output directory, and the fast path replayed a
// build.ninja generated before the edit.
//
// AND THE WAY THAT SHOWS IS THAT THE EDIT APPEARS TO HAVE HAD NO
// EFFECT. Measured 2026-08-23 on a path dependency: a flag added to
// `[build] cflags` was absent from the generated `unit_cflags` after a
// rebuild, absent after touching the sources, and present the moment
// `target/` was removed. The first two observations are what a reader
// uses to conclude the flag is being filtered, and one was concluded
// and written down before the third measurement was taken.
//
// The comment beside the root-flag tail merge in prepare.cppm has said
// "canonical_package_build_metadata folds packages[].manifest.
// buildConfig" since before this fix. It now does.
//
// packages[0] is the root, whose flags `canonical_compile_flags`
// already folds; serialising it twice is harmless and keeps this loop
// one rule rather than one rule and an exception.
s += ' ';
s += canonical_compile_flags(pkg.manifest);
// The level a C++-layer provider compiles its implementation units at
// (`make_plan`). Appended only when there is one, so every other
// output directory keeps its identity.
if (auto own = mcpp::manifest::cxx_layer_implementation_standard(pkg.manifest)) {
s += " implementation-standard=";
s += own->canonical;
}
for (auto const& src : pkg.manifest.buildConfig.sources) {
s += " src:";
s += src;
}
for (auto const& dir : pkg.manifest.buildConfig.includeDirs) {
s += " inc:";
s += dir.generic_string();
}
for (auto const& dir : pkg.manifest.buildConfig.includeDirsAfter) {
s += " inca:";
s += dir.generic_string();
}
auto const& runtime = pkg.manifest.runtimeConfig;
for (auto const& requirement : runtime.requirements) {
s += " runtime-need:";
s += requirement.kind;
s += ':';
s += requirement.value;
s += ':';
s += requirement.phase;
s += requirement.required ? ":required" : ":optional";
}
for (auto const& artifact : runtime.artifacts) {
s += " runtime-artifact:";
s += artifact.role;
s += ':';
s += artifact.path.generic_string();
s += ':';
s += artifact.provenance;
s += ':';
s += artifact.abi;
s += ':';
s += artifact.digest;
s += ':';
s += artifact.hostFingerprint;
}
for (auto const& value : runtime.linkIntent.libraries)
s += " link-library:" + value;
for (auto const& value : runtime.linkIntent.linkLibraryDirs)
s += " link-dir:" + value.generic_string();
for (auto const& value : runtime.linkIntent.transitiveNeededDirs)
s += " needed-dir:" + value.generic_string();
for (auto const& value : runtime.linkIntent.runtimeSearchDirs)
s += " runtime-dir:" + value.generic_string();
for (auto const& value : runtime.linkIntent.frameworks)
s += " framework:" + value;
for (auto const& value : runtime.linkIntent.deployFiles)
s += " deploy:" + value.generic_string();
// Legacy fields remain fingerprinted while they are readable.
for (auto const& value : runtime.libraryDirs)
s += " legacy-runtime-dir:" + value.generic_string();
for (auto const& value : runtime.dlopenLibs)
s += " legacy-soname:" + value;
for (auto const& value : runtime.capabilities)
s += " legacy-capability:" + value;
for (auto const& value : runtime.provides)
s += " legacy-provides:" + value;
for (auto const& [capability, provider] : runtime.providerOverrides)
s += " provider-override:" + capability + '=' + provider;
if (!pkg.manifest.buildConfig.cStandard.empty()) {
s += " c_standard=";
s += pkg.manifest.buildConfig.cStandard;
}
for (auto const& flag : pkg.manifest.buildConfig.cflags) {
s += " cflag:";
s += flag;
}
for (auto const& flag : pkg.manifest.buildConfig.cxxflags) {
s += " cxxflag:";
s += flag;
}
for (auto const& flag : pkg.manifest.buildConfig.ldflags) {
s += " ldflag:";
s += flag;
}
// Per-glob flags — same full ordered serialization as the root-side
// block above. Until #253 dependency globFlags were unfingerprinted
// (held only by "descriptor frozen per version" + "feature toggles
// always change cflags via -DMCPP_FEATURE_*"); feature-folded entries
// make the vector build-variant, so fingerprint it directly.
// featureOrigin is diagnostic-only and deliberately NOT serialized
// (the active feature set is already in cflags above).
for (auto const& gf : pkg.manifest.buildConfig.globFlags) {
s += " globflags:"; s += gf.glob;
for (auto const& f : gf.cflags) { s += " gc:"; s += f; }
for (auto const& f : gf.cxxflags) { s += " gxx:"; s += f; }
for (auto const& f : gf.asmflags) { s += " gas:"; s += f; }
for (auto const& f : gf.defines) { s += " gd:"; s += f; }
}
// Same reason as the root block, and it cannot be skipped on the
// grounds that "a descriptor is frozen per version": path and git
// dependencies are not frozen, and this key changes their products.
for (auto const& e : pkg.manifest.buildConfig.moduleExtensions) {
s += " modext:";
s += e;
}
if (pkg.usageResolved) {
for (auto const& dir : pkg.privateBuild.includeDirs) {
s += " private_include:";
s += dir.generic_string();
}
for (auto const& dir : pkg.publicUsage.includeDirs) {
s += " public_include:";
s += dir.generic_string();
}
for (auto const& dir : pkg.privateBuild.includeDirsAfter) {
s += " private_include_after:";
s += dir.generic_string();
}
for (auto const& dir : pkg.publicUsage.includeDirsAfter) {
s += " public_include_after:";
s += dir.generic_string();
}
}
for (auto const& [path, content] : pkg.manifest.buildConfig.generatedFiles) {
s += " genfile:";
s += path.generic_string();
s += "=";
s += content;
}
}
return s;
}
} // namespace mcpp::build