-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathdistribution.cppm
More file actions
620 lines (590 loc) · 31.9 KB
/
Copy pathdistribution.cppm
File metadata and controls
620 lines (590 loc) · 31.9 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
// 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;
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 };
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;
}
// ---------------------------------------------------------------- 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;
// 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;
// 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;
// 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;
};
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.
//
// Only a SHARED LIBRARY 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" }`. An executable's
// static libstdc++ is already local (ld exports only what a loaded object
// references, and mcpp passes no `-rdynamic`); 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.
//
// 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,
std::initializer_list<std::string_view> archives) {
if (role != Role::SharedLibrary) 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;
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;
}
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.
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;
}
// --------------------------------------------------------------- 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, {"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;
m.unitFlags += detail::hide_static_cxx_runtime(
in.role, {"libc++.a", "libc++abi.a"});
if (!in.libunwindArchive.empty()) {
m.unitFlags += " " + in.libunwindArchive;
m.unitFlags += detail::hide_static_cxx_runtime(
in.role, {"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