-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathflags.cppm
More file actions
1238 lines (1164 loc) · 64.2 KB
/
Copy pathflags.cppm
File metadata and controls
1238 lines (1164 loc) · 64.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
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
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// mcpp.build.flags — shared compile/link flag computation.
//
// Extracts all flag logic from ninja_backend.cppm into a single point
// of truth so both the ninja backend and compile_commands.json emitter
// (and future backends) share identical flag sets.
//
// See .agents/docs/2026-05-12-compile-commands-design.md.
module;
#include <cstdlib>
export module mcpp.build.flags;
import std;
import mcpp.build.distribution;
import mcpp.build.plan;
import mcpp.manifest.types;
import mcpp.modgraph.scanner;
import mcpp.platform;
import mcpp.platform.runtime_search;
import mcpp.toolchain.clang;
import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.triple;
import mcpp.toolchain.hostflags;
import mcpp.toolchain.linkmodel;
import mcpp.toolchain.model;
import mcpp.toolchain.provider;
import mcpp.toolchain.registry;
export namespace mcpp::build {
struct CompileFlags {
std::string cxx; // full cxxflags string
std::string cc; // full cflags string
std::string as; // asm-safe subset for .S/.s via the C driver
std::string nasm; // NASM global flags (.asm; own spelling)
std::string ld; // ldflags string
// The same link line for a unit with NO C++ in it (mcpp#426). Linking a
// pure-C library with the C++ driver gave it `NEEDED libstdc++.so.6`,
// `libm.so.6` and `libgcc_s.so.1` with not one symbol referencing them —
// measured: the C driver leaves exactly `libc.so.6`.
//
// Produced in the SAME expression as `ld`, with only the C++ runtime
// tokens elided, so `ld` itself is unchanged by construction rather than
// by testing. Swapping the driver alone is not enough: `-lstdc++exp` is
// named explicitly and would survive it.
std::string ldC;
// The LAST-RESORT run-time search path (today: the SubOS library view).
// NOT part of `ld`, and that is the whole point: `ld` is rendered BEFORE
// the per-unit flags, and the per-unit flags are where the artifact's own
// directory (`$ORIGIN`) lives. Emitted here it outranked `$ORIGIN`, so an
// artifact loaded a different build of a library than it linked against.
// It reaches the line through `link_line::UnitTail::runtimeFallback`.
std::string ldRuntimeFallback;
std::filesystem::path cxxBinary; // g++ / clang++ / cl.exe
std::filesystem::path ccBinary; // gcc / clang (derived; cl.exe = same)
std::filesystem::path arBinary; // ar / llvm-ar / lib.exe (empty → PATH)
std::filesystem::path ldBinary; // link.exe (SeparateLinker dialects only)
std::string sysroot; // --sysroot=... (for ninja ldflags)
std::string bFlag; // -B<binutils> (for ninja ldflags)
bool staticStdlib = true;
std::string linkage; // "static" or ""
// Per-link-unit C++ runtime flags, indexed by dist::Role. EVERY platform
// routes through here now (`-static-libstdc++`, MinGW's `-static`, macOS's
// `-load_hidden` archives): the channel has to be per-unit because two
// roles in one build may hold different contracts, which is precisely what
// `static_stdlib = false` could not express for test binaries before #336.
// Produced by exactly one call to `dist::resolve` per role.
std::array<std::string, mcpp::build::dist::kRoleCount> ldStdlibByRole{};
// The same, for a link unit with no C++ in it (mcpp#426). Comes from the
// contract table's own `unitFlagsC`, so "is this flag a C++ decision" is
// answered where the flag is written.
std::array<std::string, mcpp::build::dist::kRoleCount> ldStdlibCByRole{};
// The contract each role actually got (after any degradation).
std::array<mcpp::build::dist::Contract,
mcpp::build::dist::kRoleCount> contractByRole{};
// macOS + self-contained: link units need the initializer-ordering shim
// object prepended to their inputs (issue #336).
bool needsStreamInitShim = false;
// PE + `toolchain-coupled`: the toolset's own CRT DLLs, to be staged
// beside the artifact. Resolved HERE rather than in the emitter because
// "which files does this contract imply" is a contract question; the
// backend only knows how to spell a copy edge.
//
// A whole-BUILD list, not a per-role one, and that is a property of the
// format rather than a simplification: a PE artifact resolves a DLL from
// its own directory, so one directory holds one answer and two roles in
// one output tree cannot disagree about it. Any built role asking for the
// contract is enough to populate it.
//
// The DIRECTORY comes from `msvc::vc_redist_dir()` via
// `Toolchain::linkRuntimeDirs`, which is what keeps `debug_nonredist\`
// (vcruntime140d.dll & friends — NOT redistributable) out of the list. The
// criterion lives in exactly one place on purpose: a second name-shaped
// rule here could disagree with it, and a copy step that disagrees about
// what may be redistributed is a licensing defect, not a bug.
//
// Already deduped against the plan's own deploy files, so the emitter can
// append without deciding anything: a name the manifest already claims
// stays the manifest's and the conflict is reported through `diagnostics`.
std::vector<BuildPlan::DeployFile> toolchainRuntimeDeploy;
// Non-empty when a requested contract could not be honored. The caller
// MUST surface these — a silent downgrade is the failure mode this whole
// model exists to prevent. Emitted once by the backend, not here, because
// compute_flags runs twice per build (ninja + compile_commands).
std::vector<std::string> diagnostics;
const std::string& ldStdlibFor(mcpp::build::dist::Role r) const {
return ldStdlibByRole[static_cast<std::size_t>(r)];
}
const std::string& ldStdlibCFor(mcpp::build::dist::Role r) const {
return ldStdlibCByRole[static_cast<std::size_t>(r)];
}
};
enum class LinkIntentFlavor { Elf, MachO, PeGnu, PeMsvc };
// Spell a provider-neutral LinkIntent for one output format. Kept pure so
// every platform contract can be asserted on every CI host. deployFiles are
// intentionally absent: the backend emits copy edges, never linker flags.
std::string render_link_intent_flags(
const mcpp::manifest::LinkIntent& intent,
LinkIntentFlavor flavor);
CompileFlags compute_flags(const BuildPlan& plan);
// The kind → role map. One line of policy, in one place: a test binary runs on
// the build machine and is then thrown away; an archive embeds no runtime at
// all; everything else leaves this machine. Backends ask this, never the kind.
constexpr mcpp::build::dist::Role role_of(LinkUnit::Kind k) {
switch (k) {
case LinkUnit::TestBinary: return mcpp::build::dist::Role::Test;
case LinkUnit::StaticLibrary: return mcpp::build::dist::Role::Intermediate;
// A shared library leaves this machine too, but it is LOADED INTO a
// process that already has a C++ runtime rather than being one. That
// is a different contract, not a different flavour of the same one —
// sharing `Distributable` with executables is what let a .so publish
// a whole static libstdc++ and take over the executable's runtime.
case LinkUnit::SharedLibrary: return mcpp::build::dist::Role::SharedLibrary;
case LinkUnit::Binary: break;
}
return mcpp::build::dist::Role::Distributable;
}
// Return the linker flag that pulls in libatomic, or "" when it should be
// omitted. libatomic carries the out-of-line __atomic_* libcalls that
// 16-byte / oversized std::atomic lowers to (a GCC runtime lib — LLVM ships
// no equivalent, and compiler drivers don't auto-link it), so a genuine
// atomic user otherwise fails at link with `undefined __atomic_*`. We guard
// it with --as-needed so binaries that don't use it get no dependency. But
// --as-needed does NOT skip a missing library (the linker still has to open
// it), so the flag is emitted ONLY when a link-resolvable libatomic actually
// exists on one of the toolchain's link dirs — otherwise it would break
// toolchains that ship no libatomic at all. `staticLink` (a `-static` build,
// e.g. musl targets) narrows the resolvable form to `libatomic.a`; a dynamic
// link also accepts `libatomic.so`.
std::string atomic_link_flag(const std::vector<std::filesystem::path>& linkDirs,
bool staticLink);
// mcpp#234: quote a single flag-vector token for safe embedding in a shell
// command line. Every element of a flags `vector<string>` is already one
// argv token (e.g. `apply_glob_flags` pushes `"-D" + d`, so a define like
// `T=long long` arrives as the single element `-DT=long long`) — but the
// emission choke points (`join_flags` in ninja_backend.cppm, and the global
// blob assembly below) historically joined tokens with a bare space and no
// quoting, so a token containing a space silently split into two shell
// words once ninja handed the resolved command line to the shell. Only
// tokens that actually contain whitespace or a shell-significant character
// are quoted — plain framework flags (`-std=c++23`, `-O2`, `-I/abs/path`)
// come back unchanged, byte-for-byte. POSIX: wrap in single quotes (embedded
// `'` escaped as `'\''`). Windows: wrap in double quotes (embedded `"`
// escaped as `\"`) — cmd.exe/CreateProcess argv convention.
std::string shell_quote_arg(std::string_view arg);
// Ninja's own escaping for a value that will sit on a `command = ` line:
// ` `, `$` and `:` get a leading `$`. Exported because it is needed WITH
// shell_quote_arg, not instead of it — quoting stops the SHELL from splitting
// a token, but ninja expands `$foo` before the shell is ever invoked, so a
// token carrying a literal `$` needs both. Callers apply ninja escaping first,
// then shell quoting (see include_dir_token).
std::string escape_ninja_chars(std::string_view s);
// One include-directory token, fully prepared for a ninja command line:
// dialect prefix, ninja `$` escaping, and shell quoting — in that order.
//
// #331: the same manifest `[build] include_dirs` reaches the compiler through
// two channels — the global blob assembled below, and the per-translation-unit
// `$local_includes` emitted by ninja_backend. Only the first one quoted, so an
// include dir containing a space (`C:\Program Files\...`, or `/home/my dir` on
// Linux) survived one path and split into separate shell words on the other.
// Both channels call this now; adding a third one and forgetting to quote is
// how the bug happened, and a shared helper is the only fix that also covers
// the fourth.
//
// `prefixOverride` replaces `d.includePrefix` for the callers that need a
// different flag for the same kind of path (`-idirafter` for #249's
// after-dirs, plain `-I` for NASM units which would parse `-idirafter<p>` as
// `-i dirafter<p>`).
//
// `form` picks the separator, and the two channels genuinely need different
// ones (#261): tokens that stay on the command line keep native separators,
// while tokens ninja copies into a RESPONSE FILE must be forward-slashed,
// because the drivers tokenize response files GNU-style — there a backslash
// is an ESCAPE character and `C:\src\inc` loses its separators. Quoting
// alone does not save it; the escape happens inside quotes too.
enum class PathForm {
Native, // command line — a backslash is just a character
Generic, // response file — forward slashes, see above
};
std::string include_token(const mcpp::toolchain::CommandDialect& d,
const std::filesystem::path& dir,
std::string_view prefixOverride = {},
PathForm form = PathForm::Native);
} // namespace mcpp::build
namespace mcpp::build {
// Escape a string for embedding in ninja rule strings. Takes the text, not a
// path: round-tripping through std::filesystem::path would re-normalize the
// separators on Windows, which silently undoes a caller that deliberately
// chose generic_string() for a response-file token (#261).
//
// Deliberately OUTSIDE the anonymous namespace below: it is declared in this
// module's export block so ninja_backend can pair it with shell_quote_arg for
// action command tokens. Leaving it internal would mean a fourth hand-written
// copy of ninja's escaping rules, which is how they drift.
std::string escape_ninja_chars(std::string_view s) {
std::string out;
out.reserve(s.size());
for (char c : s) {
if (c == ' ' || c == '$' || c == ':')
out.push_back('$');
out.push_back(c);
}
return out;
}
namespace {
std::filesystem::path staged_std_bmi_path(const BuildPlan& plan) {
return mcpp::toolchain::staged_std_bmi_path(plan.toolchain, plan.outputDir);
}
// Escape a path for embedding in ninja rule strings (native separators).
std::string escape_path(const std::filesystem::path& p) {
return escape_ninja_chars(p.string());
}
std::string normalize_ldflag(const std::filesystem::path& root, const std::string& flag) {
auto absolute_path = [&](std::string_view raw) {
std::filesystem::path p{std::string(raw)};
if (p.is_absolute() || raw.starts_with("$")) return p;
return root / p;
};
if (flag.starts_with("-L") && flag.size() > 2) {
return "-L" + escape_path(absolute_path(std::string_view(flag).substr(2)));
}
constexpr std::string_view rpathPrefix = "-Wl,-rpath,";
if (flag.starts_with(rpathPrefix) && flag.size() > rpathPrefix.size()) {
return std::string(rpathPrefix)
+ escape_path(absolute_path(std::string_view(flag).substr(rpathPrefix.size())));
}
return flag;
}
} // namespace
std::string atomic_link_flag(const std::vector<std::filesystem::path>& linkDirs,
bool staticLink) {
for (auto& dir : linkDirs) {
std::error_code ec;
if (std::filesystem::exists(dir / "libatomic.a", ec)
|| (!staticLink && std::filesystem::exists(dir / "libatomic.so", ec))) {
return " -Wl,--push-state,--as-needed -latomic -Wl,--pop-state";
}
}
return {};
}
std::string include_token(const mcpp::toolchain::CommandDialect& d,
const std::filesystem::path& dir,
std::string_view prefixOverride,
PathForm form) {
std::string_view prefix =
prefixOverride.empty() ? d.includePrefix : prefixOverride;
std::string path = form == PathForm::Generic ? dir.generic_string()
: dir.string();
// Prefix first, then escape+quote the whole token: the prefix and the
// path are ONE argv word, so quoting them separately would put the
// opening quote in the wrong place and re-split exactly what we came to
// join. `escape_path` only adds ninja's `$` escapes and never touches
// separators, so the form chosen above survives it.
return shell_quote_arg(escape_ninja_chars(std::string(prefix) + path));
}
std::string shell_quote_arg(std::string_view arg) {
// Characters that split/alter a word when unquoted in POSIX sh or
// cmd.exe: whitespace plus the common shell metacharacters. Anything
// NOT in this set (e.g. `-std=c++23`, `-O2`, `-I/abs/path`, `-DFOO=1`)
// returns untouched — no quoting where none is needed.
constexpr std::string_view kNeedsQuote = " \t\n\"'\\$`;&|<>()*?[]#~!{}";
if (arg.find_first_of(kNeedsQuote) == std::string_view::npos)
return std::string(arg);
if constexpr (mcpp::platform::is_windows) {
// cmd.exe / CreateProcess argv convention: wrap in double quotes,
// escape embedded `"` as `\"`.
std::string out = "\"";
for (char c : arg) {
if (c == '"') out += "\\\"";
else out.push_back(c);
}
out += "\"";
return out;
} else {
// POSIX sh: wrap in single quotes (nothing is special inside single
// quotes except `'` itself), escaping an embedded `'` as `'\''`
// (close quote, literal quote, reopen quote).
std::string out = "'";
for (char c : arg) {
if (c == '\'') out += "'\\''";
else out.push_back(c);
}
out += "'";
return out;
}
}
std::string render_link_intent_flags(
const mcpp::manifest::LinkIntent& intent,
LinkIntentFlavor flavor) {
std::string out;
auto token = [](std::string value) {
return shell_quote_arg(escape_ninja_chars(value));
};
auto path_token = [&](std::string_view prefix,
const std::filesystem::path& path) {
return token(std::string(prefix) + path.string());
};
for (auto const& dir : intent.linkLibraryDirs) {
out += ' ';
out += path_token(flavor == LinkIntentFlavor::PeMsvc
? "/LIBPATH:" : "-L", dir);
}
if (flavor == LinkIntentFlavor::Elf) {
for (auto const& dir : intent.transitiveNeededDirs) {
out += ' ';
out += path_token("-Wl,-rpath-link,", dir);
}
}
if (flavor == LinkIntentFlavor::Elf
|| flavor == LinkIntentFlavor::MachO) {
for (auto const& dir : intent.runtimeSearchDirs) {
out += ' ';
out += path_token("-Wl,-rpath,", dir);
}
}
for (auto const& library : intent.libraries) {
if (library.empty()) continue;
out += ' ';
const std::filesystem::path asPath(library);
const bool explicitToken = library.starts_with('-')
|| library.starts_with('/') || asPath.has_parent_path()
|| asPath.has_extension();
if (explicitToken) {
out += token(library);
} else if (flavor == LinkIntentFlavor::PeMsvc) {
out += token(library + ".lib");
} else {
out += token("-l" + library);
}
}
if (flavor == LinkIntentFlavor::MachO) {
for (auto const& framework : intent.frameworks) {
if (framework.empty()) continue;
out += " -framework ";
out += token(framework);
}
}
return out;
}
CompileFlags compute_flags(const BuildPlan& plan) {
CompileFlags f;
// Central query points for per-toolchain decisions — prefer these over
// ad-hoc is_clang()/is_gcc() calls:
// caps — what the toolchain can do (scan-deps, stdlib id, …)
// d — how a flag is SPELT (GNU "-I" vs MSVC "/I")
// traits — BMI mechanics + module-flag spellings
auto caps = mcpp::toolchain::capabilities_for(plan.toolchain);
const auto& d = mcpp::toolchain::dialect_for(plan.toolchain);
// macOS minimum supported OS version for produced binaries.
// Precedence: MACOSX_DEPLOYMENT_TARGET env (explicit per-invocation
// override, the convention cargo/rustc/cc honor) > the manifest's
// [build] macos_deployment_target (project default, SwiftPM-style) >
// empty (toolchain/SDK default).
std::string macosDeploymentTarget = mcpp::platform::macos::deployment_target(
plan.manifest.buildConfig.macosDeploymentTarget);
f.cxxBinary = plan.toolchain.binaryPath;
f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain);
const bool isMsvcDialect = (d.id == "msvc");
// PIC is a GNU concept and a property of the TARGET FORMAT: PE code is
// position independent by design (base relocations), and clang rejects the
// flag outright — `unsupported option '-fPIC' for target
// 'x86_64-pc-windows-msvc'`.
//
// ⚠️ The condition used to be `!isMsvcDialect`, i.e. the DIALECT. Windows'
// default toolchain is clang, which speaks the GNU dialect while targeting
// the MSVC ABI, so `-fPIC` was emitted and every MSVC-ABI shared build died
// in clang-scan-deps before compiling anything. It was unreachable while
// `kind = "shared"` was refused on that ABI; allowing it is what surfaced
// this. Same shape as the shared-library guard itself: asking which
// COMPILER when the question is which TARGET.
const bool peTarget = [&] {
if (auto t = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple))
return t->is_pe();
return bool(mcpp::platform::is_windows);
}();
bool need_pic = false;
for (auto& lu : plan.linkUnits) {
if (lu.kind == LinkUnit::SharedLibrary) {
need_pic = true;
break;
}
}
std::string pic_flag = (need_pic && !isMsvcDialect && !peTarget) ? " -fPIC" : "";
// Include dirs — this is the TYPED PATH channel (bare paths from the
// manifest; the dialect prefix is applied here at emission), not the
// FLAG-STRING channel that `normalize_include_flags` serves (cflags/
// cxxflags, where the -I/-iquote/... prefix is already embedded in the
// string by the scanner). `normalize_include_flags`'s prefix table only
// knows GNU spellings, so routing dialect-prefixed tokens through it
// silently no-ops under MSVC (`/Iinclude` matches nothing and is never
// rewritten against plan.projectRoot — but ninja runs with cwd = output
// dir, so a relative include dir stops resolving). Absolutize the path
// directly instead (dialect-agnostic), then prepend the prefix, then
// ninja-$-escape and shell-quote per token (#234) so an include dir
// whose name contains a space can't silently split into two shell words
// once ninja hands the resolved command line to the shell.
// The one place this file turns a manifest include entry into a path.
// make_preferred: a multi-segment TOML entry like `generated/inc` keeps
// its `/` on MSVC, and the bare `projectRoot / inc` join would be MIXED —
// reaching both the ninja command line and the CDB's arguments (via
// f.cxx → split_flags). Same rule as every other manifest-path ingestion
// point (#390); no-op on POSIX. ONE lambda because the same join is needed
// four times in this function — {include_dirs, include_dirs_after} × {the
// C/C++ token list, the NASM one} — and re-deriving it per site is how the
// two channels drifted apart in the first place.
auto abs_native = [&](const std::filesystem::path& inc) {
auto p = inc.has_root_path() ? inc : (plan.projectRoot / inc);
p.make_preferred();
return p;
};
std::vector<std::string> includeTokens;
for (auto& inc : plan.manifest.buildConfig.includeDirs) {
includeTokens.push_back(include_token(d, abs_native(inc)));
}
// #249: `[build] include_dirs_after` — searched AFTER the toolchain's
// system dirs via -idirafter (gcc+clang), so entries can't shadow
// standard headers. cl.exe has no -idirafter; under the msvc dialect
// they degrade to regular /I appended at the END of the include list
// (documented degradation; clang-MSVC uses the gnu dialect).
const bool msvcInclude = d.includePrefix == std::string_view("/I");
for (auto& inc : plan.manifest.buildConfig.includeDirsAfter) {
includeTokens.push_back(
include_token(d, abs_native(inc), msvcInclude ? "/I" : "-idirafter"));
}
std::string include_flags;
for (auto& t : includeTokens) {
include_flags += ' ';
include_flags += t; // already prefixed, escaped and quoted
}
// Sysroot / payload paths — resolved ONCE by the toolchain link model
// (mcpp.toolchain.linkmodel, the single source of truth shared with
// stdmod / build_program / the cfg fixup; see
// .agents/docs/2026-07-07-hermetic-toolchain-link-model-design.md).
// Payload-first, --sysroot fallback; for Clang with a cfg file we bypass
// the (install-time-generated, non-reproducible) cfg with
// --no-default-config and provide everything explicitly.
const auto dm = mcpp::toolchain::resolve_clang_driver(plan.toolchain);
const auto lm = mcpp::toolchain::resolve_link_model(plan.toolchain);
const mcpp::toolchain::PathEscape ninjaEsc =
[](const std::filesystem::path& p) { return escape_path(p); };
std::string compile_toolchain_flags;
std::string link_toolchain_flags;
std::string link_toolchain_flags_c; // same, minus C++ runtime selection
const bool isClangWithCfg = dm.hasCfg;
// LLVM root of a clang-with-cfg toolchain — used by the macOS link
// path below to locate libc++.a/libc++abi.a for staticStdlib.
std::filesystem::path llvmRootForStdlib;
// Compile side: the shared producer (mcpp.toolchain.hostflags), which the
// std module build and the build.mcpp host compile also use. It emits
// clang-cfg bypass → macOS deployment target → C library headers, the
// order this function has always used.
//
// The macOS deployment target is on the command line rather than left to
// the environment so (a) the ninja commands don't depend on env
// propagation and (b) the value participates in the BMI fingerprint via
// canonical flags — mixing targets in one sandbox otherwise reuses a
// std.pcm built for a different arm64-apple-macosxNN triple and dies with
// a config mismatch (observed on macos CI). The link side is added to
// f.ld below (the macOS link path doesn't consume link_toolchain_flags).
//
// binutilsPrefix / runtimeLibDirs stay off here: this function computes
// -B separately into f.bFlag, and routes runtime dirs through
// depRuntimeLibraryDirs.
{
mcpp::toolchain::HostFlagOptions hopt;
hopt.cfgBypass = mcpp::toolchain::HostFlagOptions::CfgBypass::Always;
hopt.macosDeploymentTarget = macosDeploymentTarget;
compile_toolchain_flags = mcpp::toolchain::render_tokens(
mcpp::toolchain::host_compile_tokens(plan.toolchain, hopt, ninjaEsc));
}
if (isClangWithCfg) {
llvmRootForStdlib = dm.llvmRoot;
// Linker flags that cfg normally provides. The payload C-runtime
// flags (-B/-L/loader) are appended via payload_ld below.
link_toolchain_flags = " --no-default-config";
if (lm.mode == mcpp::toolchain::CLibMode::Sysroot)
link_toolchain_flags += lm.link_flags(ninjaEsc);
link_toolchain_flags_c = link_toolchain_flags
+ std::string(mcpp::toolchain::ClangDriverModel::kLinkDriverFlagsC);
link_toolchain_flags +=
mcpp::toolchain::ClangDriverModel::kLinkDriverFlags;
f.sysroot = link_toolchain_flags;
} else if (lm.mode != mcpp::toolchain::CLibMode::None) {
// GCC (or Clang without cfg): --sysroot from probe, or the payload
// headers + C runtime (-B for crt discovery, -L for -lc/-lm).
link_toolchain_flags = lm.link_flags(ninjaEsc);
link_toolchain_flags_c = link_toolchain_flags; // nothing C++-only here
f.sysroot = link_toolchain_flags;
}
// Binutils -B flag — a GCC/libstdc++ payload concern (musl and MinGW-w64
// cross both bundle their own as/ld; Clang and MSVC never take an external
// binutils). MinGW must not get the Linux binutils -B — its PE/SEH output
// is only assemblable by its own x86_64-w64-mingw32-as.
bool isMuslTc = mcpp::toolchain::is_musl_target(plan.toolchain);
bool isMingwTc = mcpp::toolchain::is_mingw_target(plan.toolchain);
const auto linkIntentFlavor = [&] {
if (isMingwTc) return LinkIntentFlavor::PeGnu;
if (isMsvcDialect) return LinkIntentFlavor::PeMsvc;
auto triple = plan.toolchain.targetTriple;
std::ranges::transform(triple, triple.begin(),
[](unsigned char c) { return std::tolower(c); });
if (triple.find("darwin") != std::string::npos
|| triple.find("apple") != std::string::npos)
return LinkIntentFlavor::MachO;
if (triple.find("windows") != std::string::npos
|| triple.find("mingw") != std::string::npos)
return LinkIntentFlavor::PeGnu;
if (triple.empty()) {
if constexpr (mcpp::platform::is_windows)
return LinkIntentFlavor::PeGnu;
if constexpr (mcpp::platform::needs_explicit_libcxx)
return LinkIntentFlavor::MachO;
}
return LinkIntentFlavor::Elf;
}();
const std::string link_intent_ld =
render_link_intent_flags(plan.linkIntent, linkIntentFlavor);
// The SubOS farm tail — the only origin in `plan.runtimeSearch` with no
// other producer, and the one that must be LAST in the artifact's
// DT_RPATH (see `runtime_search_closure`).
//
// IT DOES NOT GO INTO `f.ld`. That was the defect: `f.ld` is rendered as
// `$ldflags`, which every link rule places BEFORE `$unit_ldflags`, and
// `$unit_ldflags` is where `$ORIGIN` lives. "Appended last" inside `f.ld`
// is still ahead of the artifact's own directory, so a project with a
// shared-library dependency resolved `libX11.so.6` out of the mutable farm
// view instead of the `bin/` directory it had just been linked against.
// It now travels as `link_line::UnitTail::runtimeFallback`, which is
// after `$ORIGIN` by construction.
//
// RUNPATH ONLY, never `-L`. Link-time resolution already works: mcpp
// passes `--sysroot=<subos>`, which makes `<subos>/lib` the linker's
// default library directory. Emitting `-L` as well would be redundant on
// a link line that has a hard 128KiB ceiling real workspaces already spend
// 43% of. This is the same rule `runtimeSearchDirs` states for package
// dirs, applied to the origin that needed it most.
std::string farm_ld;
if (linkIntentFlavor == LinkIntentFlavor::Elf) {
for (auto const& dir : plan.runtimeSearch) {
if (dir.origin != mcpp::platform::search::Origin::SubosFarm) continue;
farm_ld += ' ';
farm_ld += shell_quote_arg(escape_ninja_chars(
"-Wl,-rpath," + dir.path.string()));
}
}
// Assigned HERE, not at the end: several target branches below return
// early, and every one of them is PE (where `farm_ld` is empty anyway).
// Filling the slot at its point of definition makes that a fact rather
// than something the reader has to re-derive from the return paths.
f.ldRuntimeFallback = farm_ld;
std::filesystem::path binutilsBin;
if (!isMuslTc && !isMingwTc && caps.stdlib_id == "libstdc++") {
auto ar = mcpp::toolchain::archive_tool(plan.toolchain);
if (!ar.empty())
binutilsBin = ar.parent_path();
}
std::string b_flag;
if (!binutilsBin.empty()) {
b_flag = " -B" + escape_path(binutilsBin);
f.bFlag = b_flag;
}
// AR binary
f.arBinary = mcpp::toolchain::archive_tool(plan.toolchain);
// Opt level + debug come from the resolved build profile
// ([profile.<name>] → buildConfig). musl keeps -Og as an ICE workaround
// unless the profile pins -O0.
auto& prof = plan.manifest.buildConfig;
std::string opt_flag = isMuslTc && prof.optLevel != "0"
? " -Og"
: (isMsvcDialect && prof.optLevel == "0")
? " /Od" // MSVC's no-opt spelling (there is no /O0)
: std::format(" {}{}", d.optPrefix, prof.optLevel);
if (prof.debug) opt_flag += std::format(" {}", d.debugFlags);
if (prof.lto && !isMsvcDialect) opt_flag += " -flto";
// MSVC baseline: /nologo /EHsc /utf-8 (dialect alwaysFlags) + the CRT
// model — /MD by default, /MT when either knob asks for the static CRT
// (portable-by-default is impossible on MSVC-ABI; /MT at least removes
// the vcruntime DLL dep).
std::string msvc_base;
if (isMsvcDialect) {
msvc_base = std::format(" {}", d.alwaysFlags);
// ONE derivation, shared with the std module build — see
// `msvc_wants_static_crt` in mcpp.toolchain.dialect and #422.
msvc_base += std::format(" {}", mcpp::toolchain::msvc_crt_flag(
d, mcpp::toolchain::msvc_wants_static_crt(
plan.manifest.buildConfig.linkage,
plan.manifest.buildConfig.cxxRuntime)));
}
// User link flags
std::string user_ldflags;
for (auto const& flag : plan.manifest.buildConfig.ldflags) {
user_ldflags += ' ';
user_ldflags += normalize_ldflag(plan.projectRoot, flag);
}
// C standard
std::string c_std =
plan.manifest.buildConfig.cStandard.empty() ? "c11" : plan.manifest.buildConfig.cStandard;
// Assemble
// Module-flag spellings come from BmiTraits: GCC needs -fmodules on every
// TU (BMIs implicit); Clang/MSVC reference the staged std BMI and a BMI
// search dir explicitly (spelled -fmodule-file=/-fprebuilt-module-path vs
// /reference//ifcSearchDir).
auto traits = mcpp::toolchain::bmi_traits(plan.toolchain);
std::string module_flag{traits.compileModulesFlag};
// A BMI flag and its path are ONE shell word, so the quotes have to wrap
// both — a build under `/Users/me/my work dir/…` otherwise hands the
// shell `-fmodule-file=std=/Users/me/my`, `work`, `dir/…` and the compile
// dies on "no such file or directory: 'work'" with nothing naming the
// flag that split. The BmiTraits prefixes carry a leading space for this
// string channel, and MSVC's is itself two words (`/reference std=`), so
// split at the LAST space: everything before it stays outside the quotes.
auto bmi_flag = [](std::string_view prefix, const std::filesystem::path& p) {
auto sp = prefix.find_last_of(' ');
std::string_view lead = sp == std::string_view::npos
? std::string_view{} : prefix.substr(0, sp + 1);
std::string_view body = sp == std::string_view::npos
? prefix : prefix.substr(sp + 1);
return std::string(lead)
+ shell_quote_arg(escape_ninja_chars(std::string(body) + p.string()));
};
std::string std_module_flag;
if (!traits.stdBmiUsePrefix.empty() && !plan.stdBmiPath.empty()) {
std_module_flag = bmi_flag(traits.stdBmiUsePrefix,
staged_std_bmi_path(plan));
}
std::string std_compat_module_flag;
if (!traits.stdCompatBmiUsePrefix.empty() && !plan.stdCompatBmiPath.empty()) {
auto compatDst = mcpp::toolchain::staged_std_compat_bmi_path(
plan.toolchain, plan.outputDir);
std_compat_module_flag = bmi_flag(traits.stdCompatBmiUsePrefix, compatDst);
}
std::string prebuilt_module_flag;
if (traits.needsPrebuiltModulePath) {
// Absolute path: a bare `pcm.cache` / `gcm.cache` works at ninja
// time because ninja runs commands with cwd = outputDir, but the
// same flag ends up verbatim in `compile_commands.json` whose
// `directory` field is the project root. clangd does `cd directory`
// before resolving the flag, so a bare relative path points at
// `<projectRoot>/pcm.cache` (which doesn't exist) and `import`
// resolution fails with `module 'X' not found`. The other
// `-fmodule-file=` flags in this block are already escape_path'd
// (absolute) for the same reason — this one was a leftover.
prebuilt_module_flag = bmi_flag(traits.bmiSearchPrefix,
plan.outputDir / traits.bmiDir);
}
std::string cxx_std_flag =
plan.cppStandardFlag.empty()
? std::format("{}c++23", d.stdPrefix) : plan.cppStandardFlag;
// plan.dialectFlags rides right behind -std= (issue #210): module-graph-
// global dialect flags reach every TU (deps included) via this global
// cxxflags string, exactly like the standard flag itself.
f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, plan.dialectFlags,
msvc_base, module_flag, std_module_flag,
std_compat_module_flag, prebuilt_module_flag,
opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags);
// MSVC compiles C with cl.exe too; /std: for C uses cN spellings — skip
// the C standard flag there (cl defaults are fine for the C entry TUs).
f.cc = isMsvcDialect
? std::format("{}{}{}{}{}", msvc_base, opt_flag, compile_toolchain_flags,
b_flag, include_flags)
: std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag,
compile_toolchain_flags, b_flag, include_flags);
// GAS assembly (.S/.s via the C driver): the asm-safe subset — no -std
// (C-only) and no -O (meaningless), but PIC stays (.S sources gate on
// __PIC__), -g is fine, and the toolchain-location flags must come along
// (hermetic link model: never fall back to a host `as`). MSVC dialect has
// no GAS path — prepare hard-errors before these flags are consumed.
f.as = std::format("{}{}{}{}{}",
prof.debug ? " -g" : "", pic_flag,
compile_toolchain_flags, b_flag, include_flags);
// NASM (.asm): fixed GNU-ish spelling of its own — include dirs are
// re-spelt with -I regardless of dialect (nasm ≥2.14 inserts a missing
// path separator itself); DWARF debug info exists on ELF only.
if (!plan.nasmPath.empty()) {
// Same abs_native join as the C/C++ channel above — one decision, one
// implementation. Two knock-on effects, both wanted: the entry is now
// spelt with native separators (#390), and the "already rooted?" test
// becomes has_root_path() instead of is_absolute(), so a root-relative
// `/x` entry is left alone here exactly as it is for the C/C++ include
// list. The two predicates only differ on Windows, and only for that
// spelling — where NASM disagreeing with the compiler about the SAME
// `include_dirs` key was the bug, not the feature.
std::string nasm_includes;
for (auto& inc : plan.manifest.buildConfig.includeDirs) {
nasm_includes += " -I" + escape_path(abs_native(inc));
}
// #249: nasm has no system header dirs to defer to — after-dirs
// degrade to plain -I appended at the end.
for (auto& inc : plan.manifest.buildConfig.includeDirsAfter) {
nasm_includes += " -I" + escape_path(abs_native(inc));
}
std::string nasm_debug;
if (prof.debug && plan.nasmFormat.starts_with("elf"))
nasm_debug = " -g -F dwarf";
f.nasm = nasm_debug + nasm_includes;
}
// Link flags
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
f.linkage = plan.manifest.buildConfig.linkage;
// Whether the ARTIFACT can be fully static is a property of the target,
// not of this machine. Reading the host constant directly here dropped
// `-static` from every Windows→Linux cross build, silently turning the
// musl targets into something they are not. The host constant is still
// the right answer for a host-target build, so it is threaded in as the
// fallback rather than discarded.
const bool full_static_ok = mcpp::toolchain::target_supports_full_static(
plan.toolchain.targetTriple, mcpp::platform::supports_full_static);
std::string full_static = (full_static_ok && f.linkage == "static") ? " -static" : "";
// ---- C++ runtime distribution contract (issue #336) -------------------
//
// THE single derivation of "does this artifact carry its own C++ runtime",
// for every role and every platform. `-static-libstdc++`, MinGW's
// `-static` and macOS's `-load_hidden` archives all come out of here now;
// nothing below re-decides it. The flags land in the PER-UNIT channel
// (`unit_ldflags`) rather than the global one because two roles in the
// same build may hold different contracts.
{
namespace dist = mcpp::build::dist;
auto const& bc = plan.manifest.buildConfig;
// The output FORMAT is resolved first because the role defaults
// depend on it: what a shared library should promise is a judgement
// about a hazard, and the hazard is format-specific (see
// `dist::default_contract`).
//
// Target-keyed, not host-keyed: a Linux-hosted MinGW cross build
// produces a PE and must take the PE answer.
const dist::Format format = [&] {
if (isMingwTc) return dist::Format::Pe;
// The TARGET's own word, when it has one. `isMingwTc` was the only
// cross case this knew about, so every other question about the
// output format was answered by asking the HOST — which is right
// whenever they agree and unaskable in a test that does not run on
// the platform it is about. A triple that names its OS is a fact;
// the host is a stand-in for one.
//
// Only ADDS answers: a triple that says neither falls through to
// exactly the previous derivation, so no existing build changes.
const auto& t = plan.toolchain.targetTriple;
if (t.find("windows") != std::string::npos
|| t.find("mingw") != std::string::npos)
return dist::Format::Pe;
if (t.find("apple") != std::string::npos
|| t.find("darwin") != std::string::npos)
return dist::Format::MachO;
if constexpr (mcpp::platform::needs_explicit_libcxx)
return dist::Format::MachO;
else if constexpr (mcpp::platform::is_windows)
return dist::Format::Pe;
else
return dist::Format::Elf;
}();
// `static_stdlib` is a faithful alias of the two ends of the contract:
// its documented meaning has always been exactly self-contained vs the
// dynamic system runtime. An explicit `cxx_runtime` wins.
//
// The role defaults come from `dist::default_contract` rather than
// being spelled again here. They were spelled again here, and that
// second derivation is why `default_contract` sat with no caller while
// this file quietly disagreed with it about shared libraries.
const dist::Contract base =
dist::parse_contract(bc.cxxRuntime).value_or(
bc.staticStdlib
? dist::default_contract(dist::Role::Distributable, format)
: dist::Contract::HostCoupled);
const dist::Contract testsContract =
dist::parse_contract(bc.cxxRuntimeTests).value_or(base);
// A project-wide statement (`cxx_runtime = "…"` or `static_stdlib =
// false`) applies to shared libraries too — a human said what the
// whole project promises. Only when nobody said anything does the
// role's own default apply, which is the case that changes on ELF.
const bool projectWideExplicit = !bc.cxxRuntime.empty() || !bc.staticStdlib;
const dist::Contract sharedContract =
dist::parse_contract(bc.cxxRuntimeShared).value_or(
projectWideExplicit
? base
: dist::default_contract(dist::Role::SharedLibrary, format));
// Archive lookup. LLVM lays these out either directly under lib/ (the
// macOS packages) or under lib/<llvm-triple>/ (the Linux ones), so try
// both rather than hard-coding one layout. Sorted so the choice cannot
// depend on directory iteration order.
auto find_archive = [&](std::string_view name) -> std::filesystem::path {
if (llvmRootForStdlib.empty()) return {};
std::error_code ec;
auto libDir = llvmRootForStdlib / "lib";
auto direct = libDir / name;
if (std::filesystem::exists(direct, ec)) return direct;
std::vector<std::filesystem::path> hits;
for (auto& e : std::filesystem::directory_iterator(libDir, ec)) {
std::error_code de;
if (!e.is_directory(de)) continue;
auto p = e.path() / name;
if (std::filesystem::exists(p, de)) hits.push_back(p);
}
std::ranges::sort(hits);
return hits.empty() ? std::filesystem::path{} : hits.front();
};
// Does this archive define a given symbol? Answered by scanning the
// ranlib symbol index, which a BSD/Mach-O archive keeps in its FIRST
// member — so a bounded read of the head is enough and no toolchain
// subprocess is involved. Used for exactly one thing: refusing to
// generate the macOS ordering shim against a libc++ that does not
// export the symbol it binds. That failure mode is not hypothetical —
// the first CI round of #336 turned every macOS link into `undefined
// symbol` — and a check here cannot fail the build the way a bad
// reference in the generated TU can.
auto archive_defines = [](const std::filesystem::path& p,
std::string_view sym) -> bool {
if (p.empty()) return false;
std::ifstream is(p, std::ios::binary);
if (!is) return false;
constexpr std::streamsize kHead = 8 << 20;
std::string head(static_cast<std::size_t>(kHead), '\0');
is.read(head.data(), kHead);
head.resize(static_cast<std::size_t>(is.gcount()));
return head.find(sym) != std::string::npos;
};
dist::MechanismInput mi;
mi.stdlibId = caps.stdlib_id;
mi.hostIsWindows = mcpp::platform::is_windows;
mi.fullStaticLibc = (f.linkage == "static");
// The CRT model this WHOLE project is being compiled with. Per-role
// contracts cannot move it: cl bakes _MSVC_MT / _MSVC_MD into the std
// module, one std module is built per project, and a TU importing the
// other one fails inside the ucrt headers (#422). The mechanism table
// needs to know so it can say that out loud rather than silently
// ignoring a role override.
mi.msvcStaticCrt = mcpp::toolchain::msvc_wants_static_crt(
bc.linkage, bc.cxxRuntime);
mi.mingw = isMingwTc;
mi.macosFloor = !macosDeploymentTarget.empty();
mi.format = format;
const bool wantsArchives =
(base == dist::Contract::SelfContained
|| testsContract == dist::Contract::SelfContained
|| sharedContract == dist::Contract::SelfContained)
&& caps.stdlib_id == "libc++";
if (wantsArchives) {
auto libcxxA = find_archive("libc++.a");
auto libcxxAbiA = find_archive("libc++abi.a");
mi.libcxxArchive = libcxxA.empty() ? std::string{} : escape_path(libcxxA);
mi.libcxxAbiArchive = libcxxAbiA.empty() ? std::string{} : escape_path(libcxxAbiA);
// ELF only: without it the "self-contained" binary still pulls
// libunwind.so.1. Mach-O's libc++abi.a carries its own unwinder.
if (mi.format == dist::Format::Elf) {
auto unwindA = find_archive("libunwind.a");
if (!unwindA.empty()) mi.libunwindArchive = escape_path(unwindA);
} else {
// Searched without the leading underscore so the same needle
// matches the ELF (`_ZN...`) and Mach-O (`__ZN...`) spellings.
mi.streamInitSymbolPresent =
archive_defines(libcxxA, "ZNSt3__18ios_base4InitC1Ev");
}
}
// "Explicit" = a human wrote it down. `static_stdlib = false` counts:
// nobody sets a flag to its default to get non-default behavior.
const bool explicitBase = projectWideExplicit;
const bool explicitTests = explicitBase || !bc.cxxRuntimeTests.empty();
const bool explicitShared = explicitBase || !bc.cxxRuntimeShared.empty();
// Report a role's degradation only if this build HAS that role.
//
// The contract is still RESOLVED for every role — `ldStdlibByRole` is
// indexed on demand and must be total. What is gated is the WARNING:
// telling a project with no shared library what its shared libraries
// promise is not information, and with four roles an unconditional
// report turns one honest warning into a wall of them.
auto role_is_built = [&](dist::Role r) {
return std::ranges::any_of(plan.linkUnits, [&](auto const& lu) {
return role_of(lu.kind) == r;
});
};
bool wantsToolchainRuntime = false;
for (auto [role, requested, wasAsked] : {
std::tuple{dist::Role::Distributable, base, explicitBase},
std::tuple{dist::Role::Test, testsContract, explicitTests},
std::tuple{dist::Role::Intermediate, base, explicitBase},
std::tuple{dist::Role::SharedLibrary, sharedContract, explicitShared}}) {
mi.role = role;
mi.requested = requested;
mi.explicitRequest = wasAsked;
auto r = dist::resolve(mi);
auto i = static_cast<std::size_t>(role);
f.ldStdlibByRole[i] = r.unitFlags;
f.ldStdlibCByRole[i] = r.unitFlagsC;
f.contractByRole[i] = r.effective;
if (r.streamInitShim) f.needsStreamInitShim = true;
// Only a role this build actually HAS may pull DLLs into the
// output tree. The contract is resolved for every role because
// `ldStdlibByRole` must be total; staging files is a side effect
// on disk, and a project with no test binaries should not get a
// CRT copied beside nothing.
if (r.deployToolchainRuntime && role_is_built(role))
wantsToolchainRuntime = true;
if (!r.diagnostic.empty() && role_is_built(role))
f.diagnostics.push_back(std::format(
"{} target: {}", dist::to_string(role), r.diagnostic));
}
if (wantsToolchainRuntime) {
// `linkRuntimeDirs` is the toolset's own redistributable CRT
// directory and nothing else on this toolchain — `enrich_toolchain
// _from_cl` puts exactly `vc_redist_dir()` there. Guarded on the
// compiler anyway: the field means "the toolchain's private
// runtime" for every provider, and on gcc it holds libstdc++'s
// directory, which has no business being copied into a PE tree.
if (plan.toolchain.compiler == mcpp::toolchain::CompilerId::MSVC) {
std::vector<std::filesystem::path> sources;
std::error_code ec;
for (auto const& dir : plan.toolchain.linkRuntimeDirs) {
for (auto const& e :
std::filesystem::directory_iterator(dir, ec)) {
if (!e.is_regular_file(ec)) continue;
auto ext = e.path().extension().string();
std::ranges::transform(ext, ext.begin(),
[](unsigned char c) { return std::tolower(c); });
if (ext != ".dll") continue;
sources.push_back(e.path());
}