-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathdistribution.cppm
More file actions
993 lines (951 loc) · 52.5 KB
/
Copy pathdistribution.cppm
File metadata and controls
993 lines (951 loc) · 52.5 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
// mcpp.build.distribution — the C++ runtime distribution contract.
//
// WHY THIS MODULE EXISTS
//
// "Does this artifact carry its own C++ runtime?" used to be derived
// independently in five places (issue #336): `ldStdlibDefault` and
// `ldStdlibTest` in flags.cppm, the `-static-libstdc++` string a few lines
// above them, the MinGW `-static` branch, and the `LinkUnit::TestBinary`
// two-way switch in ninja_backend.cppm. New semantics landed in some of them
// and not others, which is exactly how `[build] static_stdlib = false` came
// to be silently ignored for test binaries between 0.0.86 and today while the
// documentation kept promising it worked.
//
// The model is three layers, and only the third one knows about flags:
//
// Role — intrinsic to the link unit. Not user-specified.
// Contract — what the artifact promises about the machine that runs it.
// Defaulted per role, overridable via [build] cxx_runtime.
// Mechanism — (contract x stdlib x binary format) -> link flags.
//
// The mechanism table is a TOTAL function: every cell has an answer, and a
// cell that cannot be honored returns `degraded` plus a non-empty
// `diagnostic` that the caller MUST surface. A silent no-op is the one
// outcome this module is built to make impossible — before it, asking for a
// self-contained artifact on a Linux/libc++ toolchain produced no flag, no
// warning and a toolchain-coupled binary.
//
// Analysis: .agents/docs/2026-08-02-issue336-pr142-analysis.md
export module mcpp.build.distribution;
import std;
import mcpp.toolchain.triple;
export namespace mcpp::build::dist {
// ---------------------------------------------------------------- Layer 1
// What the link unit is FOR. Intrinsic — derived from LinkUnit::Kind, never
// written by a user. It exists so "tests run on the build machine, shipped
// artifacts do not" is a readable policy instead of an `if (kind ==
// TestBinary)` buried in the ninja emitter.
//
// `build.mcpp` host helpers are deliberately NOT a role here: their link
// policy (`staticHostHelper` in build_program.cppm) is about the libc axis —
// a musl helper needs `-static` because of PT_INTERP, a PE helper because
// DLLs resolve via PATH — not about the C++ runtime this module governs.
enum class Role {
Distributable, // Binary — leaves this machine as a program
Test, // TestBinary — runs here, right now, then is discarded
Intermediate, // StaticLibrary — carries no runtime, contract is vacuous
// SharedLibrary — loaded INTO a process that already has a C++ runtime.
//
// Split out of `Distributable` after a .so that carried its own static
// libstdc++ exported 2931 std symbols — 777 of them GLOBAL definitions
// that exist nowhere but libstdc++.a — and the executable linking it bound
// ITS std references to them, silently defeating the executable's own
// `-static-libstdc++`. "Leaves this machine" is true of both, but the
// HAZARD is not the same, and the hazard is what the contract is for.
//
// Appended rather than inserted: `CompileFlags::ldStdlibByRole` indexes by
// the enum value, so moving the existing three would silently re-map every
// stored contract.
SharedLibrary,
};
// One past the last role. The per-role arrays size themselves from this, so
// adding a role cannot leave a stale `3` behind.
inline constexpr std::size_t kRoleCount = 4;
// ---------------------------------------------------------------- Layer 2
// What the artifact promises about the machine that runs it. This is a
// DISTRIBUTION property, not a build one: it describes the runtime dependency
// set, and the flags that produce it differ per platform.
enum class Contract {
// No C++ runtime dependency outside the artifact. The default, and what
// makes the macOS deployment floor (14.0) real rather than aspirational.
SelfContained,
// Links the C++ runtime of the toolchain mcpp installed, found again at
// run time through the toolchain's rpath. The artifact travels only with
// that toolchain present.
ToolchainCoupled,
// Links whatever C++ runtime the driver resolves by default — the system
// one on most hosts. The form distro packaging (Debian/Homebrew) requires,
// and the form `static_stdlib = false` has always been documented to mean.
HostCoupled,
};
// NOTE ON SCOPE: the contract governs the C++ runtime (stdlib + its ABI and
// unwinder). The libc axis is separate and stays with `linkage`/`--static`
// (a musl `-static` link), and the deployment floor is a third axis
// (`macos_deployment_target`). Conflating them is what made a single
// `static_stdlib` bool expand into three different platform meanings.
//
// KNOWN LIMIT: `HostCoupled` promises only that mcpp adds nothing to embed a
// C++ runtime. It does not strip the toolchain rpath that the link already
// carries for other reasons, so on ELF a HostCoupled artifact may still find
// the toolchain's libraries first. Removing that rpath is a packaging axis of
// its own and is not part of this contract.
// The binary format decides which mechanisms even exist — Mach-O has no
// priority-ordered initializer section, PE has no rpath, ELF has both.
enum class Format { Elf, MachO, Pe, Wasm };
// WHICH FORMAT A TARGET PRODUCES, ASKED OF THE TARGET.
//
// `hostFallback` is what a triple outside the vocabulary falls back to, and it
// is a parameter rather than a compile-time constant so that this function can
// be examined without being the machine it is about.
//
// THIS USED TO BE A LAMBDA INSIDE A FIFTEEN-HUNDRED-LINE FUNCTION, AND THAT
// IS WHY IT HAD NO TEST. It tested the triple for the substrings `apple` and
// `darwin`, which are LLVM's words; mcpp's canonical form is `aarch64-macos`
// and contains neither, so the test fell through to a question about the HOST
// and produced opposite errors on opposite hosts:
//
// Linux host, macOS target → an ELF contract for a Mach-O
// macOS host, Linux target → a Mach-O contract for an ELF, which is
// `ld.lld: error: unable to find library
// -load_hidden` plus the host's own libc++.a on
// an ELF link line
//
// Both were found by running three hosts against three targets. Either would
// have been found by four lines of assertion, once this was a function.
//
// The substring tests remain as a fallback for a triple the vocabulary cannot
// parse — the `[target.X]` escape hatch — where a spelling is all there is.
Format format_for(std::string_view targetTriple, Format hostFallback) {
if (auto parsed = mcpp::toolchain::triple::parse(targetTriple)) {
if (parsed->is_pe()) return Format::Pe;
if (parsed->is_wasm()) return Format::Wasm;
// `is_mach_o()`, not `os == "macos"`: the latter answered the
// opposite-hosts defect above for macOS and would still get iOS
// wrong the same way, since iOS's `os` is `ios`.
if (parsed->is_mach_o()) return Format::MachO;
if (parsed->os == "linux"
|| parsed->os == "none") return Format::Elf;
// THE FOURTH MEMBER, and this comment used to say it was deferred
// "to whoever gives this module a mechanism for it". A wasm target
// parses here and fell out of every branch -- `is_pe()` and
// `is_mach_o()` are both false, and its `os` is `emscripten`, neither
// `linux` nor `none` -- reaching `hostFallback` and answering the
// MACHINE's format, which is the defect class this header measured
// for macOS.
//
// What it cost while it stood: every wasm build printed
//
// warning: cxx_runtime: distributable target: this toolchain ships
// no libc++.a/libc++abi.a; using toolchain-coupled (the artifact
// keeps a run-time dependency on the toolchain's libc++.so)
//
// -- a promise about a `libc++.so` that cannot exist for this target,
// on an artifact that has no run-time dependency of any kind.
}
if (targetTriple.find("windows") != std::string_view::npos
|| targetTriple.find("mingw") != std::string_view::npos)
return Format::Pe;
if (targetTriple.find("apple") != std::string_view::npos
|| targetTriple.find("darwin") != std::string_view::npos)
return Format::MachO;
return hostFallback;
}
std::string_view to_string(Contract c) {
switch (c) {
case Contract::SelfContained: return "self-contained";
case Contract::ToolchainCoupled: return "toolchain-coupled";
case Contract::HostCoupled: return "host-coupled";
}
return "self-contained";
}
std::string_view to_string(Role r) {
switch (r) {
case Role::Distributable: return "distributable";
case Role::Test: return "test";
case Role::Intermediate: return "intermediate";
case Role::SharedLibrary: return "shared-library";
}
return "distributable";
}
std::optional<Contract> parse_contract(std::string_view s) {
if (s == "self-contained") return Contract::SelfContained;
if (s == "toolchain-coupled") return Contract::ToolchainCoupled;
if (s == "host-coupled") return Contract::HostCoupled;
return std::nullopt;
}
// The (role x format) -> default contract policy, in one place.
//
// THIS FUNCTION IS THE SOURCE. It used to have no caller at all — `flags.cppm`
// derived the same policy a second time from the manifest — which is the exact
// shape of debt this module's opening comment was written to retire, relocated
// rather than removed. `compute_flags` now asks here.
//
// WHY FORMAT IS AN INPUT. A default is a judgement about a hazard, and the
// hazard a shared library poses is format-specific. Folding it into the
// mechanism table instead would have to spell the difference as a DEGRADATION,
// and a degradation means "mcpp promised something it could not deliver" — it
// prints a diagnostic. There is nothing broken about a self-contained .dylib;
// it is simply the right answer there. Say so in the default rather than
// apologising for it later.
//
// Test binaries default to SelfContained rather than the HostCoupled that
// their role alone would suggest, and that is deliberate: on macOS a test
// linked against the SYSTEM libc++ while compiled against the toolchain's
// libc++ HEADERS is a version split that detonated once already (undefined
// `__hash_memory` when libc++ 22 moved string hashing out of line, #202). The
// role model makes that trade visible instead of hard-coding it in the
// emitter; a project that wants the other side of it writes
// `cxx_runtime = { tests = "host-coupled" }` and now actually gets it.
Contract default_contract(Role r, Format f) {
switch (r) {
case Role::Distributable: return Contract::SelfContained;
case Role::Test: return Contract::SelfContained;
case Role::Intermediate: return Contract::SelfContained;
case Role::SharedLibrary:
// ELF has ONE global symbol namespace and the first definition
// loaded wins. A .so that statically embedded libstdc++ exports
// those symbols UNVERSIONED, and the linker then resolves
// the executable's own std references against that .so — because
// `-lfoo` precedes the driver's `-lstdc++`, so the archive member
// is never pulled. The executable's `-static-libstdc++` becomes a
// no-op and its C++ runtime is, in fact, whichever build of that
// .so happens to be loaded. Swap the .so for another build of the
// same SONAME and `std::runtime_error::what()` simply is not
// there. Coupling to the toolchain's libstdc++.so is the only
// spelling under which the executable keeps its own contract.
//
// The other two formats do not have that hazard, and their
// current behaviour is therefore correct and unchanged:
//
// Mach-O the self-contained mechanism already IS hiding —
// `-Wl,-load_hidden,<archive>` gives the archive's
// symbols hidden visibility precisely so dyld cannot
// unify them (PR #117). ToolchainCoupled is also a
// documented dead end there (#202): LLVM's macOS
// libc++abi/libunwind dylibs upward-link /usr/lib/libc++
// and a second libc++ loads alongside the toolchain's.
//
// PE no global symbol namespace at all — imports resolve
// per-DLL by name, so a DLL's private CRT cannot be
// picked up by anything else. `-static` is additionally
// the standalone-DLL convention there.
return f == Format::Elf ? Contract::ToolchainCoupled
: Contract::SelfContained;
}
return Contract::SelfContained;
}
// What a manifest states about the C++ runtime, read once for every role.
struct ContractStatement {
std::string_view cxxRuntime; // `cxx_runtime = "..."` or its `default`
std::string_view cxxRuntimeTests; // `cxx_runtime = { tests = "..." }`
std::string_view cxxRuntimeShared; // `cxx_runtime = { shared = "..." }`
bool staticStdlib = true;
};
// Which images of a build load a C++ shared library the build itself makes:
// its programs, and its test programs.
struct CxxSharedLoad {
bool program = false;
bool tests = false;
};
// The contract each role holds, and whether a human stated it.
//
// A stated contract is what the manifest said; an unstated one is a default,
// and a default is the only thing `role_contracts` is allowed to move.
struct RoleContracts {
Contract program = Contract::SelfContained;
Contract tests = Contract::SelfContained;
Contract intermediate = Contract::SelfContained;
Contract shared = Contract::SelfContained;
bool programStated = false;
bool testsStated = false;
bool sharedStated = false;
};
// THE ONE DERIVATION of every role's contract, for the flag assembly and for
// the refusal that reads the same answer before anything compiles.
RoleContracts role_contracts(const ContractStatement& s, Format f, CxxSharedLoad load);
// The role whose STATED contract splits the process's C++ runtime: on ELF, a
// self-contained program or test that loads a C++ shared library coupled to a
// shared runtime. Empty when no role does.
std::optional<Role> runtime_split(const RoleContracts& c, Format f, CxxSharedLoad load);
// The contract a manifest STATES for shared libraries, or nothing when it
// states none and the role's default applies.
//
// `cxx_runtime = { shared = "..." }` states it directly. A project-wide
// statement (`cxx_runtime = "..."`, or `static_stdlib = false`, which nobody
// writes to get the default) states it for shared libraries too. The flag
// assembly derives the shared-library contract from this, and so does the
// refusal of a C++ shared library in a graph whose runtime is a package
// (#641): that refusal is lifted by a statement, never by a default, so the
// two must agree about what was written.
std::optional<Contract> stated_shared_library_contract(std::string_view cxxRuntime,
std::string_view cxxRuntimeShared,
bool staticStdlib, Format f) {
if (auto shared = parse_contract(cxxRuntimeShared)) return shared;
if (cxxRuntime.empty() && staticStdlib) return std::nullopt;
return parse_contract(cxxRuntime).value_or(
staticStdlib ? default_contract(Role::Distributable, f) : Contract::HostCoupled);
}
RoleContracts role_contracts(const ContractStatement& s, Format f, CxxSharedLoad load) {
RoleContracts c;
c.programStated = !s.cxxRuntime.empty() || !s.staticStdlib;
c.program = parse_contract(s.cxxRuntime).value_or(
s.staticStdlib ? default_contract(Role::Distributable, f) : Contract::HostCoupled);
c.intermediate = c.program;
c.testsStated = c.programStated || !s.cxxRuntimeTests.empty();
c.tests = parse_contract(s.cxxRuntimeTests).value_or(c.program);
c.sharedStated = c.programStated || !s.cxxRuntimeShared.empty();
c.shared = stated_shared_library_contract(s.cxxRuntime, s.cxxRuntimeShared,
s.staticStdlib, f)
.value_or(default_contract(Role::SharedLibrary, f));
// ONE PROCESS, ONE C++ RUNTIME (#646 F3a).
//
// The ELF defaults are right one at a time and wrong together. A program
// is self-contained and a shared library couples to the toolchain's
// runtime, so a program that LOADS such a library holds a static runtime
// and a shared one. The executable exports the runtime symbols the library
// references, the library binds some of them to the program's copy and
// keeps the rest, and the two halves disagree about shared state.
// Measured on Linux x86_64 with llvm@22.1.8: the program aborted with
// `std::bad_cast` (exit 134) as soon as the library formatted a string;
// with gcc@16.1.0 it ran with 900 libstdc++ symbols interposed.
//
// So a role nobody stated takes the shared library's contract when its
// image loads a C++ shared library this build makes. The process already
// needs that runtime through the library's own NEEDED entry, so no
// deployment gains a requirement; what changes is that the program binds
// to the same copy. A stated contract is never changed here; one that
// splits the runtime is refused by the caller (`runtime_split`).
if (f == Format::Elf) {
if (load.program && !c.programStated) c.program = c.shared;
if (load.tests && !c.testsStated) c.tests = c.shared;
}
return c;
}
std::optional<Role> runtime_split(const RoleContracts& c, Format f, CxxSharedLoad load) {
if (f != Format::Elf) return std::nullopt;
// A shared library that embeds a hidden private copy keeps its runtime to
// itself (`hide_static_cxx_runtime`); that is the documented private-copy
// arrangement, not a split. A coupled library shares the process's.
if (c.shared == Contract::SelfContained) return std::nullopt;
if (load.program && c.programStated && c.program == Contract::SelfContained)
return Role::Distributable;
if (load.tests && c.testsStated && c.tests == Contract::SelfContained)
return Role::Test;
return std::nullopt;
}
// ---------------------------------------------------------------- Layer 3
struct MechanismInput {
Contract requested = Contract::SelfContained;
Role role = Role::Distributable;
// Did a human write this contract down, or is it just our default?
//
// The distinction decides whether a cell with no mechanism SPEAKS. A
// diagnostic is for a BROKEN PROMISE: mcpp said the artifact would be
// self-contained and it is not. Most roles DEFAULT to self-contained, so
// on a runtime where that default cannot be delivered, warning on every
// build would be noise nobody can act on. Cells where mcpp DOES promise
// something (a missing libc++.a under the default, say) report regardless.
bool explicitRequest = false;
// MSVC only: is this project compiled with the static CRT (`/MT`)?
//
// A whole-PROJECT fact, not a per-role one, and that is a property of the
// platform rather than a simplification: 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). So the
// table can honour a project-level request and must refuse a per-role
// one — out loud, since silently ignoring it is how a knob becomes
// decoration. Derived by `msvc_wants_static_crt`, which is also what
// emits the flag.
bool msvcStaticCrt = false;
// MSVC STL only: does mcpp pass a CRT model (`/MT` or `/MD`) to this
// compiler? True for cl.exe. FALSE FOR CLANG ON THE MSVC ABI: that driver
// speaks the GNU dialect, mcpp emits no runtime flag for it, and clang
// then links the static CRT (`-defaultlib:libcmt`, measured with the
// 22.1.8 driver). The table must report the model the compiler was given,
// not the model `cl.exe` would have been given (#649 E10).
bool msvcCrtModelEmitted = true;
// Toolchain capability id: "libstdc++", "libc++", or an MSVC STL spelling.
std::string_view stdlibId;
Format format = Format::Elf;
// MinGW targets are PE + libstdc++ and take the whole-link `-static`
// rather than the piecemeal `-static-libstdc++` (the latter still leaves
// libwinpthread-1.dll behind).
bool mingw = false;
// Host is Windows. Only reason it is here: the historical flag string
// adds `-static-libgcc` on a Windows HOST, and this table reproduces the
// existing bytes rather than quietly "improving" them.
bool hostIsWindows = false;
// `linkage = "static"` — the libc axis. On PE it shares the one `-static`
// spelling with the C++ runtime axis, so the table has to see it.
bool fullStaticLibc = false;
// Does this link line also name a C++ runtime that is NOT the toolchain's?
//
// Today that means a libc++ toolchain whose line carries libstdc++,
// which is what the SYCL and HIP rule packages produce: the device half is
// compiled by a second compiler configured against libstdc++, so the
// artifact links libc++ statically AND loads libstdc++.so at run time.
//
// It changes two things below, and both are corrections of an assumption
// that held only while no such line existed. See `hide_static_cxx_runtime`
// for the symbol half and the libc++ ELF branch for the unwinder half.
bool foreignCxxRuntime = false;
// Already-escaped archive paths for the explicit-archive mechanisms.
// Empty string = that archive is not available on this toolchain.
std::string libcxxArchive;
std::string libcxxAbiArchive;
std::string libunwindArchive;
// The archive FILE NAMES the linker opens for the libc++ pair, when they
// are not `libc++.a` and `libc++abi.a`. `--exclude-libs` matches the
// archive a member was taken from, and the Android NDK's `libc++.a` is a
// linker script, `INPUT(-lc++_static -lc++abi)`, so its members come from
// `libc++_static.a`. Measured on a self-contained shared library for
// `x86_64-linux-android`: 161 dynamic symbols with only the two default
// names, 4 with `libc++_static.a` named. Empty means the two defaults.
std::vector<std::string> libcxxLinkedArchiveNames;
// macOS only: the libc++ archive actually defines the ABI symbol the
// initializer-ordering shim binds to. Checked against the archive rather
// than assumed, so an unexpected spelling disables the shim instead of
// producing an undefined reference at link time.
bool streamInitSymbolPresent = false;
// macOS only: a deployment floor was resolved. The static-libc++
// mechanism exists to make that floor real, so without one there is
// nothing to make real.
bool macosFloor = false;
// AN APPLE CROSS TARGET -- the iOS rows, device and simulator.
//
// Same format as macOS and a different platform, which matters here
// because the payload's `libc++.a` is a MACH-O ARCHIVE BUILT FOR macOS.
// ld64 refuses an object built for one platform in a link for another, so
// the self-contained cell -- the one macOS uses to make its deployment
// floor real -- cannot apply, and `haveCxxArchives` says nothing about
// it: the archives exist, they are simply the wrong platform's.
//
// This is a distinct input and not a derivation from `macosFloor`,
// because `macosFloor` answers "did a macOS deployment target resolve"
// and an iOS build resolves one too (its default is a macOS version,
// which is exactly why it must not be consulted here).
bool appleCrossTarget = false;
// Bare metal — there is no C++ runtime to distribute WITH.
//
// Every cell of the table below answers "how does this artifact carry its
// C++ runtime", and on a freestanding target the honest answer is that
// there is not one: the toolchain's libc++.a is built for the HOST, and
// putting it on the line produces
//
// ld.lld: error: …/x86_64-unknown-linux-gnu/libc++.a(path.cpp.o)
// is incompatible with elf64lriscv
//
// (measured 2026-08-19). A target-side C++ runtime, if one is wanted, is
// an ordinary package — the same way the libc is.
bool freestanding = false;
// THE HOSTED FORM OF THE LINE ABOVE: a package in the graph supplies
// the C++ runtime, built for this target, and its objects are already on
// the link line.
//
// The table below has three answers and all of them name a runtime to LINK
// — the system's, the toolchain's, or a static form of one. Each is right
// when the runtime is something the artifact has to be JOINED to, and each
// is wrong here, where it is already inside. The archives it would find are
// the host's, which is the same defect the `freestanding` flag above
// exists for; the difference is only that this target has an OS.
//
// Measured 2026-08-23, cross-building for `aarch64-macos` over openkal
// right after the format decision was corrected to key on the target — the
// wrong format had been masking this:
//
// ld64.lld: error: library not found for -lc++
//
// ⇒ Not "pick openkal's here". openkal's IS the objects; there is no
// library to name, and the honest flag is the one that stops the driver
// from adding its own.
bool graphCxxRuntime = false;
};
struct Mechanism {
// Flags for this link unit, each with a leading space. Per-unit rather
// than global precisely so two roles in one build can differ.
std::string unitFlags;
// The subset that is NOT a statement about the C++ runtime, for a link
// unit with no C++ in it (mcpp#426). Accumulated HERE, beside the flags
// themselves, rather than filtered downstream: which of these is a C++
// decision is knowledge this table has and a string filter would have to
// re-derive. On macOS the difference is not cosmetic — the self-contained
// contract names `libc++.a`/`libc++abi.a` by path, so a pure-C library
// would otherwise have the C++ runtime linked INTO it.
std::string unitFlagsC;
Contract effective = Contract::SelfContained;
// effective != requested. `diagnostic` is then non-empty and the caller
// is required to surface it — see INV-1/INV-4 in the analysis doc.
bool degraded = false;
std::string diagnostic;
// Mach-O + static libc++: the archive's stream initializer is appended
// LAST in __init_offsets (Mach-O has no priority-ordered init section and
// libc++'s <iostream> carries no `ios_base::Init` guard of its own), so a
// global constructor that touches std::cout runs before the streams
// exist. Asks the backend for the ordering shim. See issue #336.
bool streamInitShim = false;
// PE + MSVC runtime + toolchain-coupled: the toolset's own redistributable
// CRT DLLs must be STAGED BESIDE the artifact.
//
// On ELF, `toolchain-coupled` needs no files copied — the artifact carries
// an rpath into the toolchain's lib directory and the loader follows it.
// PE has no rpath: a DLL is resolved from the directory of the executable
// (and then PATH), so on this format the mechanism IS the copy. Same
// contract, same meaning, different mechanism — which is exactly the split
// this module's three layers exist to express.
bool deployToolchainRuntime = false;
};
namespace detail {
inline bool is_libstdcxx(std::string_view id) { return id == "libstdc++"; }
inline bool is_libcxx(std::string_view id) { return id == "libc++"; }
// Keep a statically linked standard library OUT of a shared object's dynamic
// symbol table.
//
// A SHARED LIBRARY always needs this, and only when it actually embedded the
// runtime — which after `default_contract` happens on ELF exclusively through
// an explicit `cxx_runtime = { shared = "self-contained" }`. A .so exports
// every global it defines, which is how a pure-C compat package came to
// publish 777 GLOBAL libstdc++ definitions and become the executable's
// de-facto C++ runtime.
//
// AN EXECUTABLE NEEDS IT WHEN A FOREIGN C++ RUNTIME IS ON THE LINE, and the
// sentence that used to be here — "an executable's static libstdc++ is already
// local (ld exports only what a loaded object references, and mcpp passes no
// `-rdynamic`)" — was a correct premise with a wrong conclusion. The clause in
// the parentheses is the whole mechanism: when a loaded object DOES reference
// them, the linker puts them in `.dynsym`. Measured on a SYCL artifact
// (mcpp#596), which links libc++ statically and loads libstdc++.so: 89
// exported symbols, 68 of them also defined by libstdc++ or libgcc_s. The
// executable is searched first, so libstdc++'s own code called libc++abi's
// `std::exception::what`, libc++'s `std::runtime_error` constructors ran on
// objects libstdc++ would later destroy, and ten of libgcc_s's eighteen
// unwinder entry points were answered by the executable's libunwind while
// eight were not.
//
// It does NOT hide the weak/COMDAT template instantiations the library's own
// code emits, and must not: unifying those across the process is the intended
// C++ ABI behaviour, not a leak.
//
// The escape hatch has to stay usable, so it is guarded rather than refused.
//
// Archive BASENAMES — that is what `--exclude-libs` matches, and GNU ld and
// lld agree on it. Listed by name rather than `ALL` so a user's own static
// library linked into their .so keeps its exports.
std::string hide_static_cxx_runtime(Role role, bool foreignCxxRuntime,
std::initializer_list<std::string_view> archives) {
if (role != Role::SharedLibrary && !foreignCxxRuntime) return {};
std::string out;
for (auto archive : archives) {
out += " -Wl,--exclude-libs,";
out += archive;
}
return out;
}
} // namespace detail
// The one table. Total by construction: every return path sets `effective`,
// and every path where `effective != requested` also sets `diagnostic`.
Mechanism resolve(const MechanismInput& in) {
Mechanism m;
m.effective = in.requested;
// An archive is linked, not run. It embeds no runtime and imposes none —
// the contract belongs to whatever eventually links it.
if (in.role == Role::Intermediate)
return m;
// Bare metal: there is nothing to decide, because there is no C++ runtime
// on this side of the build. Returning SelfContained with an empty
// mechanism is not a degradation — the artifact genuinely carries
// everything it has — so this reports no diagnostic.
//
// Placed before the format switch rather than inside it: the format is
// ELF here, and every ELF cell below reaches for the toolchain's HOST
// archives. One of them silently produced a link line with
// x86-64 libc++.a on a riscv64 link.
if (in.freestanding || in.graphCxxRuntime) {
m.effective = Contract::SelfContained;
m.unitFlags = " -nostdlib++";
return m;
}
const bool haveCxxArchives =
!in.libcxxArchive.empty() && !in.libcxxAbiArchive.empty();
switch (in.format) {
// ------------------------------------------------------------- Mach-O
case Format::MachO: {
if (!detail::is_libcxx(in.stdlibId)) {
// Mach-O without libc++ is not a configuration mcpp produces.
m.effective = Contract::HostCoupled;
m.unitFlags = " -lc++";
if (in.requested != Contract::HostCoupled && in.explicitRequest) {
m.degraded = true;
m.diagnostic = std::format(
"cxx_runtime = \"{}\" is not available for stdlib '{}' on "
"Mach-O; using host-coupled", to_string(in.requested), in.stdlibId);
}
return m;
}
// iOS TAKES ITS C++ RUNTIME FROM THE SDK UNLESS THE GRAPH SUPPLIES ONE.
//
// Every iOS release ships libc++ in the OS, and the SDK's
// `libc++.tbd` is the stub that links against it -- so `-lc++` is
// the answer the PAYLOAD can give for these rows. The two payload
// alternatives are closed by construction rather than by policy: its
// static archives are built for macOS and ld64 refuses them in an
// iOS link, and its libc++.dylib is not present on a device at all.
// A graph package (`llvm.libcxx`, libc++ as source) is the other
// option, and it is decided before this switch: `graphCxxRuntime`
// returns `SelfContained` with `-nostdlib++` and this branch is not
// reached (mcpp#630). What remains here is the no-package case, whose
// headers `hostflags.cppm` takes from the SDK for the same reason.
//
// The contract vocabulary calls this `HostCoupled`, which reads
// oddly for a cross target; what it means in every cell is "the C++
// runtime comes from the system the ARTEFACT RUNS ON", and for these
// rows that system is iOS. The deployment floor is still real, and
// it is carried by the effective triple (`arm64-apple-ios18.0`)
// rather than by a static archive.
if (in.appleCrossTarget) {
m.effective = Contract::HostCoupled;
m.unitFlags = " -lc++";
if (in.requested != Contract::HostCoupled && in.explicitRequest) {
m.degraded = true;
m.diagnostic = std::format(
"cxx_runtime = \"{}\" is not available for an iOS target: "
"the toolchain's libc++ archives are built for macOS and "
"ld64 refuses them in an iOS link. Using the SDK's libc++, "
"which every iOS release ships; the deployment floor is "
"carried by the target triple", to_string(in.requested));
}
return m;
}
if (in.requested == Contract::ToolchainCoupled) {
// The toolchain's libc++.dylib is a dead end on this
// distribution: LLVM's macOS libc++abi/libunwind dylibs
// upward-link /usr/lib/libc++, so the SYSTEM libc++ loads
// alongside the toolchain's and objects freed across the two
// copies abort in libmalloc (#202 crash forensics).
m.effective = Contract::SelfContained;
m.degraded = true;
m.diagnostic =
"cxx_runtime = \"toolchain-coupled\" is not supported on macOS "
"(LLVM's libc++abi/libunwind dylibs upward-link /usr/lib/libc++, "
"which loads a second libc++ into the process); using self-contained";
}
if (m.effective == Contract::SelfContained) {
if (!haveCxxArchives || !in.macosFloor) {
m.effective = Contract::HostCoupled;
m.degraded = true;
m.diagnostic = haveCxxArchives
? "no macOS deployment floor resolved, so the static libc++ "
"that makes the floor real is pointless; using host-coupled"
: "this toolchain ships no libc++.a/libc++abi.a; "
"using host-coupled (the artifact then runs only on the "
"build machine's macOS version and above)";
m.unitFlags = " -lc++";
return m;
}
// -Wl,-load_hidden,<path> rather than a plain by-path link: it
// forces the ARCHIVE (never a sibling dylib) AND gives its
// symbols hidden visibility. Without the hidden part, dyld
// unifies them with the system libc++ from the shared cache and
// ostream<<int crosses into the other copy's locale machinery
// (PR #117 forensics).
m.unitFlags = " -nostdlib++"
" -Wl,-load_hidden," + in.libcxxArchive +
" -Wl,-load_hidden," + in.libcxxAbiArchive;
// The ordering shim binds a libc++ INTERNAL ABI symbol, so its
// presence is verified against the archive instead of assumed.
// Getting this wrong must not break the link — hence a check
// here rather than a weak reference in the shim: Mach-O's
// weak-undefined form is `weak_import` and applies to dylib
// symbols, so a plain weak declaration would NOT have saved a
// missing archive symbol (it did not: ld64.lld errored outright).
m.streamInitShim = in.streamInitSymbolPresent;
if (!m.streamInitShim) {
m.diagnostic =
"this libc++ does not export the stream initializer mcpp "
"orders first on macOS; a global object whose constructor "
"uses std::cout may crash at startup (mcpp#336). Use "
"cxx_runtime = \"host-coupled\" if you hit it";
}
return m;
}
// HostCoupled
m.unitFlags = " -lc++";
return m;
}
// ---------------------------------------------------------------- PE
case Format::Pe: {
if (!detail::is_libstdcxx(in.stdlibId)) {
// MSVC STL (cl.exe, or clang on the MSVC ABI). The CRT model is
// the mechanism here, and it is a whole-project switch: /MT is
// self-contained (no vcruntime DLL dependency), /MD is
// host-coupled. `msvcStaticCrt` is that switch, already derived
// by whoever emits the flag — so what this table reports and what
// cl was actually told cannot disagree.
//
// No unit flags: the model is a COMPILE flag on every TU, not
// something added to the link line.
//
// CLANG ON THE MSVC ABI IS GIVEN NO MODEL, so the table records
// the one its driver chooses. The rows below were written for
// cl.exe, and for this row they recorded `host-coupled` beside an
// artifact that imports no vcruntime DLL at all (#649 E10). The
// artifact is left as it is; the record, and an explicit request
// the row does not deliver, now say what it is.
if (!in.msvcCrtModelEmitted) {
m.effective = Contract::SelfContained;
if (in.requested != Contract::SelfContained && in.explicitRequest) {
m.degraded = true;
m.diagnostic = std::format(
"cxx_runtime = \"{}\" is not delivered for clang on the "
"MSVC ABI: mcpp passes this driver no CRT model, and clang "
"links the static CRT (libcmt) by default. Use msvc@system "
"for the dynamic CRT; using self-contained",
to_string(in.requested));
}
return m;
}
m.effective = in.msvcStaticCrt ? Contract::SelfContained
: Contract::HostCoupled;
if (in.requested == Contract::SelfContained && !in.msvcStaticCrt) {
// Asked for, not delivered. Only reachable from a per-ROLE
// override, because a project-level one would have set
// msvcStaticCrt — so name that, instead of the old "not
// implemented", which stopped being true and had already
// been contradicted by flags.cppm emitting /MT for
// `linkage = "static"`.
m.degraded = in.explicitRequest;
m.diagnostic = in.explicitRequest
? "on the MSVC runtime the CRT model is a whole-project "
"property — one std module is built per project and cl "
"bakes _MSVC_MT/_MSVC_MD into it, so a single role "
"cannot differ. Move it to [build] cxx_runtime = "
"\"self-contained\" (or linkage = \"static\") to apply "
"it everywhere; using host-coupled here"
: "";
} else if (in.requested == Contract::ToolchainCoupled) {
// THIS USED TO BE A FLAT REFUSAL, and the sentence it refused
// with was half true:
//
// "…has no meaning for the MSVC runtime (it ships with the
// OS/redistributable, not with the toolchain)"
//
// True of `ucrtbase.dll`, which IS an OS component since
// Win10. NOT true of `vcruntime140.dll` / `msvcp140.dll`,
// which are the toolset's own and sit inside every MSVC
// toolset ever shipped:
//
// VC\Redist\MSVC\<ver>\<arch>\Microsoft.VC<N>.CRT\*.dll
//
// That is the same relationship gcc has to libstdc++.so, so it
// takes the same contract — and refusing it left a hole in the
// matrix that had a real cost: the default `/MD` artifact
// depends on DLLs a machine with only a managed toolset does
// not have, and there was no spelling that made them travel.
//
// `/MT` is the one case that stays a degradation, and it is a
// genuine contradiction rather than a missing mechanism: a
// static CRT leaves NO DLL to couple to. Say which one won.
if (in.msvcStaticCrt) {
m.effective = Contract::SelfContained;
m.degraded = true;
m.diagnostic =
"cxx_runtime = \"toolchain-coupled\" cannot apply to a "
"project compiled with the static CRT (/MT): there is "
"no vcruntime140.dll/msvcp140.dll dependency left to "
"couple to. Drop linkage = \"static\" (or the "
"project-wide self-contained contract) if the toolset's "
"CRT should travel beside the artifact instead; using "
"self-contained";
} else {
m.effective = Contract::ToolchainCoupled;
m.deployToolchainRuntime = true;
}
}
return m;
}
// MinGW. `-static` is the standalone-exe convention here: the
// piecemeal -static-libstdc++ recipe still leaves libwinpthread-1.dll.
// It is also the spelling the libc axis uses, so `linkage = "static"`
// keeps it regardless of the C++ runtime contract.
const bool wantStatic =
m.effective == Contract::SelfContained || in.fullStaticLibc;
// `-static` and `-static-libgcc` are libc / compiler-runtime
// decisions, not C++ ones: a self-contained pure-C DLL wants both.
if (wantStatic) { m.unitFlags += " -static"; m.unitFlagsC += " -static"; }
if (m.effective == Contract::SelfContained) {
m.unitFlags += " -static-libstdc++";
if (in.hostIsWindows) {
m.unitFlags += " -static-libgcc";
m.unitFlagsC += " -static-libgcc";
}
}
return m;
}
// -------------------------------------------------------------- WASM
//
// THERE IS NOTHING TO BE COUPLED TO. An Emscripten link produces one
// module plus its JavaScript: no `DT_NEEDED`, no rpath, no loader, no
// shared object a search path could find. So the artifact is
// self-contained by construction rather than by flags, and the contract
// is satisfied with nothing added -- which is also why there is no
// degradation to report. A diagnostic here would be a broken promise
// about a mechanism the format does not have.
//
// `-nostdlib++` and the archive pair are deliberately NOT emitted. libc++
// reaches a wasm link through `em++`'s own link line (`-lc++-debug-noexcept
// -lc++abi-debug-noexcept`, measured), and naming archives from a sysroot
// this module did not resolve would be the second answer to a question the
// driver has already answered.
case Format::Wasm: {
m.effective = Contract::SelfContained;
return m;
}
// --------------------------------------------------------------- ELF
case Format::Elf:
default: {
if (detail::is_libstdcxx(in.stdlibId)) {
if (m.effective == Contract::SelfContained) {
m.unitFlags = " -static-libstdc++";
m.unitFlags += detail::hide_static_cxx_runtime(
in.role, in.foreignCxxRuntime, {"libstdc++.a"});
}
// ToolchainCoupled and HostCoupled are the same emission on ELF
// (no flag); they differ in the rpath the link already carries,
// which is the documented limit of this contract. For a shared
// library ToolchainCoupled is the DEFAULT (see `default_contract`)
// and "no flag" is the entire mechanism: the driver links
// libstdc++.so, and the toolchain's lib directory is already an
// `-L` and an rpath entry on this line.
return m;
}
if (detail::is_libcxx(in.stdlibId)) {
if (m.effective != Contract::SelfContained)
return m; // driver default: the toolchain's libc++.so via rpath
if (!haveCxxArchives) {
m.effective = Contract::ToolchainCoupled;
m.degraded = true;
m.diagnostic =
"this toolchain ships no libc++.a/libc++abi.a; using "
"toolchain-coupled (the artifact keeps a run-time dependency "
"on the toolchain's libc++.so)";
return m;
}
// Verified locally: -nostdlib++ plus the three archives leaves
// NEEDED = libc/libm/loader only. Without libunwind.a the binary
// still pulls libunwind.so.1, which is not self-contained — so it
// is part of the mechanism, not an optional extra.
m.unitFlags = " -nostdlib++ " + in.libcxxArchive
+ " " + in.libcxxAbiArchive;
if (in.libcxxLinkedArchiveNames.empty()) {
m.unitFlags += detail::hide_static_cxx_runtime(
in.role, in.foreignCxxRuntime, {"libc++.a", "libc++abi.a"});
} else {
for (auto const& name : in.libcxxLinkedArchiveNames)
m.unitFlags += detail::hide_static_cxx_runtime(
in.role, in.foreignCxxRuntime, {std::string_view(name)});
}
if (in.foreignCxxRuntime) {
// ONE UNWINDER PER PROCESS.
//
// libgcc_s is in this process either way: libstdc++.so needs
// it, so naming it here adds no loaded object -- measured, the
// NEEDED set gains the name and nothing else. What it removes
// is the second unwinder. Linking libunwind.a instead pulls in
// only the archive members something references, so ten of
// libgcc's eighteen entry points came from the executable and
// eight stayed in libgcc_s; libstdc++'s personality routine
// then read an LLVM libunwind context through libgcc's
// accessors, found no landing pad, and terminated past a
// handler that should have run (mcpp#596).
//
// `--unwindlib=libgcc` LAST WINS over the payload cfg file's
// `--unwindlib=libunwind`, which is why this is an addition
// rather than an edit of that file: the cfg is the default for
// every link, and only this line has the second runtime on it.
//
// NOT a degradation of the contract. The C++ runtime is still
// embedded; the unwinder was never the artifact's own here,
// because the process already had libstdc++'s.
m.unitFlags += " --unwindlib=libgcc";
} else if (!in.libunwindArchive.empty()) {
m.unitFlags += " " + in.libunwindArchive;
m.unitFlags += detail::hide_static_cxx_runtime(
in.role, in.foreignCxxRuntime, {"libunwind.a"});
} else {
m.degraded = true; // effective stays SelfContained: the C++
// runtime IS embedded; the unwinder is not
m.diagnostic =
"this toolchain ships no libunwind.a; the C++ runtime is "
"embedded but the artifact keeps a run-time dependency on "
"libunwind.so";
}
return m;
}
// Unknown stdlib on ELF: emit nothing rather than guess, but say so
// when something was actually asked for.
m.effective = Contract::HostCoupled;
if (in.requested != Contract::HostCoupled && in.explicitRequest) {
m.degraded = true;
m.diagnostic = std::format(
"cxx_runtime = \"{}\" has no mechanism for stdlib '{}'; "
"using host-coupled", to_string(in.requested), in.stdlibId);
}
return m;
}
}
}
// The Mach-O initializer-ordering shim, as a C translation unit.
//
// WHY C: it needs no standard library, no module flags and no C++ ABI of its
// own — it only has to run before everything else and poke one symbol.
//
// WHY THE NAME IS SPELLED WITH TWO UNDERSCORES: an `__asm__` label is used
// VERBATIM — clang does not add Mach-O's global `_` prefix to it. The C++
// symbol `_ZNSt3__18ios_base4InitC1Ev` therefore has to be written
// `__ZNSt3__18ios_base4InitC1Ev` here. Getting this wrong is not a silent
// no-op: ld64.lld reports `undefined symbol: ZNSt3__18ios_base4InitC1Ev` and
// every link fails, which is exactly what the first CI round did.
//
// WHY `weak_import` AND a presence check: Mach-O's weak-undefined form is
// `weak_import` (plain `weak` on a declaration does NOT make an undefined
// reference optional there). Even so, the real safety net is upstream — the
// backend only generates this TU when the archive actually defines the
// symbol, so an unexpected libc++ spelling disables the shim rather than
// breaking the link. The attribute is the second line of defence.
//
// The reference does not itself drag iostream.cpp.o out of the archive: if the
// program never touches a stream, there is nothing to order.
//
// WHY IT WORKS: libc++'s `ios_base::Init::Init()` is not empty — it is a
// guarded function-local static that calls `DoIOSInit::DoIOSInit()`, and THAT
// is the function whose relocations placement-new cin/cout/cerr. Calling it
// early constructs the streams; the archive's own `_GLOBAL__I_000100` then
// hits the same `__cxa_guard` and does nothing. `this` is never read by that
// constructor (verified by disassembly), but real storage is passed anyway.
//
// The object must be FIRST on the link line — see the backend, which prepends
// it to the link unit's inputs. Mach-O runs __init_offsets in link order.
std::string_view stream_init_shim_source() {
return
"/* Generated by mcpp. macOS + self-contained (static libc++) only.\n"
" * Mach-O has no priority-ordered initializer section, so the stream\n"
" * initializer pulled out of libc++.a lands LAST in __init_offsets and\n"
" * a global constructor that touches std::cout sees an unconstructed\n"
" * stream (null vptr -> SIGSEGV at process start). libc++'s <iostream>\n"
" * has no ios_base::Init guard of its own, unlike libstdc++/MSVC STL,\n"
" * so the header cannot fix it either. mcpp-community/mcpp#336.\n"
" *\n"
" * Weak: a toolchain without this exact ABI symbol links as before.\n"
" */\n"
"extern void mcpp_libcxx_ios_init(void *)\n"
" __attribute__((weak_import))\n"
" __asm__(\"__ZNSt3__18ios_base4InitC1Ev\");\n"
"\n"
"static char mcpp_libcxx_ios_init_storage[8];\n"
"\n"
"__attribute__((constructor))\n"
"static void mcpp_force_std_streams(void) {\n"
" if (mcpp_libcxx_ios_init)\n"
" mcpp_libcxx_ios_init(mcpp_libcxx_ios_init_storage);\n"
"}\n";
}
} // namespace mcpp::build::dist