-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathflags.cppm
More file actions
2168 lines (2073 loc) · 118 KB
/
Copy pathflags.cppm
File metadata and controls
2168 lines (2073 loc) · 118 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.build.refusal;
import mcpp.diag;
import mcpp.freestanding.target;
import mcpp.freestanding.linkline;
import mcpp.manifest.types;
import mcpp.manifest.flag_words;
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)
// THE LINK IS DRIVEN BY THIS BINARY INSTEAD OF BY THE COMPILER, AND
// WHEN IT IS SET THE WHOLE FLAG VOCABULARY CHANGES WITH IT.
//
// Empty everywhere except a freestanding target whose row carries an
// `lldEmulation` — today `x86_64-none-elf`, where clang delegates the link
// to the host's `g++`. `ld`/`ldC` then hold LINKER flags rather than driver
// flags: `-Map=` and not `-Wl,-Map=`, `-m elf_x86_64` and not `--target=`.
// A caller that mixes the two gets a diagnostic from the linker naming a
// flag the reader never wrote.
std::filesystem::path ldDriver;
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, Wasm };
// 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);
// ── Which link line a (host, target) pair takes (#647 E3) ─────────────────
//
// THE HOST DOES NOT DECIDE THIS ALONE. Three of the link branches describe a
// link on this machine's own platform family: `link.exe` for the MSVC dialect,
// `-fuse-ld=lld` without a payload model for a PE, and the Apple SDK line
// (`-isysroot`, the deployment floor) for a Mach-O. They used to be selected by
// `if constexpr` on the HOST, which was the same question while each host built
// only for its own family. An Android row built on a macOS host is the first
// target that separated them, and the Apple line it received had no
// `--target`, so `-fuse-ld=lld` selected `ld64.lld` for an ELF object:
//
// ld64.lld: error: unknown argument '-soname'
//
// The generic branch is the one that consumes `link_toolchain_flags`, where
// the target is named. Every target a host branch does not describe takes it.
//
// ONE EXCEPTION IS KEPT, AND IT IS STATED BY ITS INPUT. On a Windows host a
// non-PE target whose driver is NOT told its target by a flag (a canadian GCC
// cross, which names its target by its own prefix) keeps the PE-host line it
// has always had: nothing on that line is wrong for such a driver, and the
// cross build that uses it is verified in CI. A target named by `--target`
// (an SDK such as the NDK, or a retargetable clang) is exactly the case the
// host line cannot serve.
enum class LinkHost { Linux, MacOS, Windows };
enum class LinkShape { MsvcLinkExe, PeLld, AppleSdk, Generic };
LinkShape link_shape(LinkHost host, mcpp::build::dist::Format targetFormat,
bool msvcDialect, bool targetNamedByFlag);
// The host this binary was built for, in `link_shape`'s vocabulary.
constexpr LinkHost current_link_host() {
return mcpp::platform::is_windows ? LinkHost::Windows
: mcpp::platform::needs_explicit_libcxx ? LinkHost::MacOS
: LinkHost::Linux;
}
// 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 under the MSVCRT
// argument rules (an embedded `"` becomes `\"`, and the backslashes before it
// or before the closing quote are doubled). The inverse is
// `mcpp::manifest::host_command_words`: reading the result back yields `arg`.
std::string shell_quote_arg(std::string_view arg);
// One word of a compile-flag list as it is written on a ninja `command =`
// line: quoted for the host's command-line reader, then `$` doubled so that
// ninja hands the reader a literal `$`. A plain word comes back unchanged.
std::string ninja_command_word(std::string_view word);
// The ninja text of a compile-flag list: every element read into words
// (`mcpp::manifest::flag_words`), every word written by `ninja_command_word`,
// each preceded by a space. The compile databases list the same words.
std::string ninja_flag_list(const std::vector<std::string>& elements);
// 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);
// Does a search-path entry begin with a token the dynamic loader expands
// (#634, item 11 of the triage record)? ELF's `$ORIGIN` and every other
// `$`-token (`${ORIGIN}`, `$LIB`), and Mach-O's `@executable_path`,
// `@loader_path` and `@rpath`. Such an entry is relative to an object the
// loader has loaded, not to the package that wrote it, so the ldflag
// normalisers leave it as written. One predicate for both of them: the second
// copy exempted `$` alone, and an `@executable_path` rpath arrived in the binary
// as `<package dir>/@executable_path/..`.
bool is_loader_relative_search_path(std::string_view entry);
} // 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;
}
bool is_loader_relative_search_path(std::string_view entry) {
if (entry.starts_with('$')) return true;
for (std::string_view token : {std::string_view("@executable_path"),
std::string_view("@loader_path"),
std::string_view("@rpath")}) {
if (entry.starts_with(token)
&& (entry.size() == token.size() || entry[token.size()] == '/'))
return true;
}
return false;
}
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() || is_loader_relative_search_path(raw)) 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) {
// MSVCRT argument rules: inside double quotes a run of backslashes is
// literal unless a `"` follows it, in which case every backslash of
// the run is doubled and the quote is escaped. The closing quote is
// such a `"`, so a trailing run is doubled as well; `C:\dir\` would
// otherwise escape the quote that ends the word.
std::string out = "\"";
std::size_t backslashes = 0;
for (char c : arg) {
if (c == '\\') { ++backslashes; continue; }
if (c == '"') {
out.append(backslashes * 2 + 1, '\\');
} else {
out.append(backslashes, '\\');
}
out.push_back(c);
backslashes = 0;
}
out.append(backslashes * 2, '\\');
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 ninja_command_word(std::string_view word) {
// An empty word is still a word. shell_quote_arg leaves it empty, which is
// right for its callers that append optional pieces, and wrong here.
if (word.empty()) return mcpp::platform::is_windows ? "\"\"" : "''";
std::string out;
for (char c : shell_quote_arg(word)) {
if (c == '$') out.push_back('$');
out.push_back(c);
}
return out;
}
std::string ninja_flag_list(const std::vector<std::string>& elements) {
std::string out;
for (auto const& word : mcpp::manifest::flag_words(elements)) {
out += ' ';
out += ninja_command_word(word);
}
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;
}
LinkShape link_shape(LinkHost host, mcpp::build::dist::Format targetFormat,
bool msvcDialect, bool targetNamedByFlag) {
using mcpp::build::dist::Format;
switch (host) {
case LinkHost::Windows:
if (msvcDialect) return LinkShape::MsvcLinkExe;
if (targetFormat == Format::Pe) return LinkShape::PeLld;
return targetNamedByFlag ? LinkShape::Generic : LinkShape::PeLld;
case LinkHost::MacOS:
return targetFormat == Format::MachO ? LinkShape::AppleSdk
: LinkShape::Generic;
case LinkHost::Linux:
return LinkShape::Generic;
}
return LinkShape::Generic;
}
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);
}();
// READ, not re-derived. `make_plan` decides this once and the cache key
// hashes the same bit; a second scan here is how the compiler and the
// cache came to disagree about which objects they were talking about.
std::string pic_flag =
(plan.needsPic && !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
// THE TRIPLE ON THE LINK LINE TOO, AND FOR A DIFFERENT REASON THAN ON
// THE COMPILE LINE.
//
// Compiling without it produces objects for the wrong machine. LINKING
// without it produces the wrong LINKER: `-fuse-ld=lld` names a family, and
// the clang driver picks the flavour from the target — `ld.lld` for ELF,
// `ld64.lld` for Mach-O, `lld-link` for PE. With no target it picks the
// host's.
//
// Measured 2026-08-23, cross-linking for macOS from Linux, after the
// objects were already correct Mach-O:
//
// ld.lld: error: obj/main.o: unknown file type
//
// — the ELF linker, handed Mach-O objects, describing them accurately and
// saying nothing about why it was the one running.
const std::string crossTarget = plan.toolchain.crossTargetFlag.empty()
? std::string{}
: " " + plan.toolchain.crossTargetFlag;
// Does the TARGET bring its own sysroot -- an Emscripten or Android SDK,
// where the C library, the C++ runtime and the loader are all inside the
// payload? Read once here; the link branch below is its only consumer.
const bool ownSysrootTarget = [&] {
auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple);
return tt && tt->has_own_sysroot();
}();
const bool isClangWithCfg = dm.hasCfg;
// THE TARGET SIDE COMES FROM THE DEPENDENCY GRAPH, READ RATHER THAN
// DERIVED.
//
// This used to be `targetCxxRuntime && !crossTargetFlag.empty()`, and the
// twenty lines that stood here argued why neither condition could be
// dropped. The argument was sound about the two conditions and wrong about
// the question: both are proxies measured before the dependency graph
// exists, and a proxy cannot see a case it was not written for.
//
// The case it could not see was a C program. `targetCxxRuntime` says a
// package supplies a C++ RUNTIME, and a C program has none while its
// system still comes from the graph. So the gate in prepare admitted the
// build, this predicate rejected it, the payload's own libc++ stayed on the
// link line, and a macOS cross ended in:
//
// ld64.lld: error: …/lib/x86_64-unknown-linux-gnu/libc++.so:
// unhandled file type
//
// `mcpp.targetside` answers the question directly, after resolution, for
// every layer separately. Reading it here means this site and the gate
// cannot disagree, because there is nothing left to disagree about.
// (The `system_from_graph` reading that stood here is gone: both of its
// former users ask about the C library, and one of them was getting a
// different answer than it needed. Leaving the name in scope would have
// left the wrong question one keystroke away.)
// 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.
// Is this build for a target with no OS under it? Asked once, here, and
// used by both the compile block immediately below and the link block at
// the end of this function.
const bool isFreestandingTarget = [&] {
auto ft = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple);
return ft && ft->is_freestanding();
}();
// A TARGET WHOSE TOOLCHAIN SHIPS ITS OWN SYSROOT IS HANDLED IN THE SHARED
// PRODUCER, not here. `host_compile_tokens` is read by this site, by the
// std module's command assembly (stdmod.cppm) and by the build.mcpp host
// compile, and the first attempt at this fix put the answer at THIS site
// only -- so `mcpp build --target wasm32-emscripten` stopped injecting the
// host's headers into ordinary compiles and went on injecting them into the
// std module precompile, which is where it had been failing. One decision,
// one site, three readers.
if (!isFreestandingTarget) {
mcpp::toolchain::HostFlagOptions hopt;
hopt.cfgBypass = mcpp::toolchain::HostFlagOptions::CfgBypass::Always;
hopt.macosDeploymentTarget = macosDeploymentTarget;
// READ, NOT RE-DERIVED. `prepare` located this once, where the target
// and the machine's Xcode are both known, and refused there if it was
// absent -- so a second `sdk_path()` call here could only disagree.
hopt.appleSdkRoot = plan.toolchain.appleSdkRoot;
// THE SAME EXPRESSION THE LINK SIDE ASKS, twenty lines further down
// (`plan.targetSide.cAbi.prebuilt()`). Reading one value at both sites
// is what makes it impossible for them to disagree — which they did,
// from #511 until now, because only the link side was corrected.
hopt.cAbiPrebuilt = plan.targetSide.cAbi.prebuilt();
// AND THE C++ LAYER'S OWN ANSWER, which the C library's used to stand
// in for. The two differ on a hosted target whose C library is a
// located SDK while a package supplies libc++ (mcpp#630, §5).
hopt.cxxFromGraph = plan.targetSide.cxx.fromGraph();
hopt.appleSdkCxxHeaders = plan.toolchain.appleSdkCxxHeaders;
compile_toolchain_flags = mcpp::toolchain::render_tokens(
mcpp::toolchain::host_compile_tokens(plan.toolchain, hopt, ninjaEsc));
for (auto const& w : mcpp::toolchain::apple_float_macro_words(plan.toolchain))
compile_toolchain_flags += " " + ninja_command_word(w);
} else {
// Skipped entirely, not filtered. What this block emits is the
// HOST's world reconstructed by hand — libc++'s headers, glibc's
// headers, the Linux UAPI headers — because the cfg that normally
// supplies them is bypassed. Every one of those is for the host, and
// on a freestanding target they do not merely go unused: picolibc's
// own <stdio.h> includes <stddef.h>, which then resolves to libc++'s
// copy, which opens a `__config_site` generated for the host and
// absent here. Measured — and the error names __config_site, so it
// reads as a broken payload rather than as the wrong include path.
//
// The cfg bypass itself is still required (the cfg carries a hardcoded
// x86-64 dynamic linker); it is added to the freestanding prefix.
compile_toolchain_flags = " --no-default-config";
}
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 = crossTarget + " --no-default-config";
// THE CONDITION IS WHETHER THE GRAPH SUPPLIES THE C LIBRARY, AND
// FOR A LONG TIME IT ASKED WHETHER `--target` HAD BEEN TYPED.
//
// `crossTarget` is the string `--target=<llvm triple>`. It is non-empty
// for ANY named target, including a project that names the host's own
// and depends on nothing — and such a project's system comes from the
// payload, not from a graph. Measured 2026-08-26, same machine, same
// compiler, same target, differing only in whether it was spelled out:
//
// $ mcpp build → ELF 64-bit LSB pie exec
// $ mcpp build --target x86_64-linux-gnu → hermetic link check failed
//
// The explicit spelling lost `-stdlib=libc++`, `--rtlib=compiler-rt`,
// `--unwindlib=libunwind` and every reference to the installed
// `xim:glibc` — `-B`, `-L` and the loader — so clang fell back to its
// defaults and the startup objects resolved out of /lib.
//
// `cAbi.prebuilt()` IS THE QUESTION THE COMMENT BELOW ALREADY ASKED,
// and it is the same predicate 2026.8.25.1 moved three other decisions
// onto. e2e 295 states the invariant as an identity: naming the host's
// own target changes nothing.
if (!plan.targetSide.cAbi.prebuilt()) {
// THE TARGET SIDE COMES FROM THE GRAPH, SO THE HOST'S MODEL
// CONTRIBUTES NOTHING — THE SAME REPLACEMENT `stdModuleFlags`
// ALREADY MAKES ON THE COMPILE SIDE.
//
// `lm.link_flags()` describes the C library THIS MACHINE has and
// `kLinkDriverFlags` selects the C++ runtime THE PAYLOAD ships.
// Both are right for a native link and both are wrong here: the C
// library, the C++ runtime and the platform are packages, and the
// package that knows a format states its own link line (openkal-musl
// carries `-nostdlib` plus that format's entry symbol).
//
// Measured 2026-08-23, after the correct linker was finally
// being chosen:
//
// ld64.lld: error: unknown argument '--as-needed'
// ld64.lld: error: unknown argument
// '--dynamic-linker=…/xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2'
//
// — this host's glibc loader, handed to a Mach-O linker. Each
// message is accurate and none of them names the cause.
//
// `-fuse-ld=lld` stays because it names a FAMILY and the driver
// picks the flavour from the target; that is the one part of the
// selection that is still ours to make.
link_toolchain_flags += " -fuse-ld=lld";
link_toolchain_flags_c = link_toolchain_flags;
} else {
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 = crossTarget + lm.link_flags(ninjaEsc);
link_toolchain_flags_c = link_toolchain_flags; // nothing C++-only here
f.sysroot = link_toolchain_flags;
} else if (!crossTarget.empty() && ownSysrootTarget) {
// AN SDK THAT BRINGS ITS OWN SYSROOT STILL HAS TO BE TOLD WHICH TARGET.
//
// Both branches above are skipped for such a target, and that is
// correct for everything they carry: the link model contributes
// nothing, because the C library, the C++ runtime, the crt objects and
// the loader all live inside the SDK and the driver finds them itself.
// What it cannot do is guess WHICH of them to find -- one NDK serves
// both Android arches -- so falling through with an empty string linked
// the target's objects with the host's startup files:
//
// hermetic link check failed
// /lib/x86_64-linux-gnu/Scrt1.o (outside the sandbox)
// /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o
// /lib64/ld-linux-x86-64.so.2
//
// Six host objects on an aarch64 link, every one of them resolved by a
// driver that believed it was building for this machine. The compile
// side already said the target; only the link side did not.
//
// `crossTarget` ALONE, and that is the whole content of this branch.
// Adding the C-runtime flags the branch above adds would reintroduce
// the host's model, which is the thing the SDK replaces.
link_toolchain_flags = crossTarget;
link_toolchain_flags_c = crossTarget;
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);
// The object format the TARGET produces, derived once: the runtime contract
// table and the link-line shape both read it (#647 E3). Target-keyed, with
// the host's format only as the fallback for a triple that names none; a
// MinGW toolchain is a PE whatever its triple spelling says.
const mcpp::build::dist::Format targetObjectFormat =
isMingwTc ? mcpp::build::dist::Format::Pe
: mcpp::build::dist::format_for(plan.toolchain.targetTriple,
mcpp::platform::needs_explicit_libcxx
? mcpp::build::dist::Format::MachO
: mcpp::platform::is_windows
? mcpp::build::dist::Format::Pe
: mcpp::build::dist::Format::Elf);
const auto linkIntentFlavor = [&] {
if (isMingwTc) return LinkIntentFlavor::PeGnu;
if (isMsvcDialect) return LinkIntentFlavor::PeMsvc;
// THE OBJECT FORMAT IS ASKED OF THE PARSED TRIPLE, AND THE SUBSTRING
// TEST BELOW IS NOW ONLY THE ESCAPE HATCH.
//
// `plan.toolchain.targetTriple` is mcpp's CANONICAL spelling, and
// `aarch64-macos` contains neither "apple" nor "darwin" -- so an
// explicit `--target aarch64-macos`, a verified row, fell through to
// `Elf`. Only a NATIVE macOS build was right, and by a different
// branch: an empty triple reaching the `needs_explicit_libcxx` rescue
// below. That is why nothing caught it -- the two paths through this
// function disagreed and only one of them was exercised.
//
// The substring test is kept for a triple `parse` REJECTS, which is
// the `[target.<triple>]` escape hatch: an author may name a spelling
// outside the canonical vocabulary, and it is then an LLVM-shaped
// string where "apple" and "windows" do appear.
if (auto t = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple)) {
switch (t->object_format()) {
case mcpp::toolchain::triple::ObjectFormat::MachO:
return LinkIntentFlavor::MachO;
case mcpp::toolchain::triple::ObjectFormat::Pe:
return LinkIntentFlavor::PeGnu;
case mcpp::toolchain::triple::ObjectFormat::Wasm:
// #622 A5 closes the open half of #597: what `link_lib`
// and a search path mean for an Emscripten link is ELF's
// own spelling. `-L` for a search directory and `-l<name>`
// for a library are both flags emcc's driver accepts
// unchanged (`-lidbfs.js` is one of its own JS system
// libraries), and `frameworks` and `link_library_dirs`
// carry no Emscripten-specific rendering either. So the
// flavor exists to give the switch below an honest arm —
// not because the rendering differs from `Elf`'s.
return LinkIntentFlavor::Wasm;
case mcpp::toolchain::triple::ObjectFormat::Elf:
return LinkIntentFlavor::Elf;
}
}
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) {