-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathhostflags.cppm
More file actions
672 lines (624 loc) · 35.5 KB
/
Copy pathhostflags.cppm
File metadata and controls
672 lines (624 loc) · 35.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
// mcpp.toolchain.hostflags — the single producer of host-compile flags.
//
// One assembly, three consumers:
// flags.cppm → rendered with ninja `$` escaping into build.ninja
// stdmod.cppm → rendered with shell quoting into a std-module command
// build_program.cppm → used as argv tokens directly (build.mcpp execs, no shell)
//
// Before this module those three hand-wrote the same thing. The resolvers
// (linkmodel, clang driver model) were already shared; the ASSEMBLY was not,
// because the seam only produced strings and the argv consumer could not use
// it. Every bug in mcpp#331/PR#332's batch was an instance of that split —
// flags.cppm knew about quoting / the macOS deployment target / the MSVC
// dialect and the other two did not — and a 0.0.9x fix had already corrected
// the same file once for the same reason (musl→static re-derived).
//
// See .agents/docs/2026-08-02-host-compile-single-producer-design.md.
//
// Deliberately its own module rather than a helper inside build_program.cppm:
// that file's anonymous namespace has demonstrated (PR#332, clang 22.1.8 +
// C++20 modules + -O2) that adding a function to it can miscompile a
// NEIGHBOURING function — an unused `split_ws` was enough to corrupt a local
// vector in `contract_env`. Mechanism unknown, reproduction solid; the cheap
// response is to not grow that namespace. Design §6.2.
export module mcpp.toolchain.hostflags;
import std;
import mcpp.platform;
import mcpp.toolchain.model;
import mcpp.toolchain.linkmodel;
import mcpp.toolchain.registry;
import mcpp.toolchain.triple;
export namespace mcpp::toolchain {
// The knobs below exist because the three consumers genuinely differ TODAY.
// Each one is a documented divergence, not a switch to preserve an accident:
// consolidating without them would silently change behaviour, and dropping
// the reasons would leave the next reader unable to tell which is which.
struct HostFlagOptions {
// Whether to bypass clang's bundled `<driver>.cfg`.
//
// Always — the main build and the std module. The cfg is an
// install-time-generated, non-reproducible artifact, so
// everything it would provide is spelled out explicitly.
// LinuxOnly — the build.mcpp host helper. On macOS/Windows it keeps
// TRUSTING the cfg, because the macOS link additionally
// needs the libc++abi/unwind handling that the main build's
// needs_explicit_libcxx path owns; duplicating that for a
// host compile produced undefined __cxa_* /
// __gxx_personality_v0 (build_program.cppm, pre-existing).
// Never — always trust the cfg. No caller selects it; it completes
// the enum, and it is what makes the cfg-trusting branch of
// `host_link_tokens` reachable from a test on a Linux
// runner. `LinuxOnly` folds into `Always` there, so without
// this value that branch could only be exercised on the two
// hosts it was written for -- which is how it came to be
// missing the runtime-directory tokens in the first place.
enum class CfgBypass { Always, LinuxOnly, Never };
CfgBypass cfgBypass = CfgBypass::Always;
// binutils `-B` so the driver finds as/ld. A GCC/libstdc++ payload
// concern only: musl and MinGW-w64 bundle their own, and Clang/MSVC never
// take an external binutils. MinGW must NOT get the Linux binutils — its
// PE/SEH output is only assemblable by x86_64-w64-mingw32-as.
bool binutilsPrefix = false;
// `-L` (plus `-Wl,-rpath` where the format has one) for the toolchain's
// own runtime dirs, so the produced program can load private libs in
// tree. The main build routes these through depRuntimeLibraryDirs
// instead, so it leaves this off.
bool runtimeLibDirs = false;
// Emit `-stdlib=libc++` alongside the cfg bypass.
//
// ClangDriverModel deliberately leaves this to callers: flags.cppm's
// string feeds C compiles too, and a C command must not carry it. The std
// module build has no such constraint — it compiles exactly one C++ TU —
// and states the stdlib selection explicitly.
bool clangStdlibSelect = false;
// Resolved value from platform::macos::deployment_target(); empty = omit.
// Must agree across the std BMI and everything that imports it — clang
// rejects a module built for a different deployment target outright.
//
// A macOS VERSION, so it is emitted only for a macOS target. An iOS
// target carries its own version space (`ios_deployment_target`) and
// clang REFUSES the two together -- `-mmacosx-version-min` with an
// `arm64-apple-ios…` triple is an error, not a no-op -- which is why
// `appleSdkRoot` below is the discriminator rather than a second version
// field: exactly one of the two Apple platforms is ever in play.
std::string macosDeploymentTarget;
// THE LOCATED APPLE SDK, for a target whose SDK is not the host's.
//
// Read from `Toolchain::appleSdkRoot`, which prepare resolves once. It is
// non-empty only for the iOS rows, and its presence is what says "this is
// an Apple cross": the deployment-target flag above then belongs to the
// other platform and is withheld.
std::filesystem::path appleSdkRoot;
// DOES THE TARGET'S C LIBRARY COME FROM A DIRECTORY THAT EXISTED
// BEFORE DEPENDENCY RESOLUTION? — `plan.targetSide.cAbi.prebuilt()`, READ
// rather than derived.
//
// This function used to ask `!tc.crossTargetFlag.empty()`: is there a
// `--target=` on the command line. Its own comment said what it meant to
// ask — "AND NOT WHEN THE TARGET SIDE COMES FROM THE GRAPH" — and those
// are different questions. A project that names its host's own target
// while depending on nothing answers yes to the first and no to the
// second.
//
// THE LINK SIDE OF THIS DEFECT WAS FIXED IN 2026.8.26.1 (#511) AND THIS
// SIDE WAS NOT. Measured on 2026.8.26.2, same machine, same compiler, same
// target, differing only in whether it was spelled out:
//
// $ mcpp build ldflags: identical
// $ mcpp build --target x86_64-unknown-linux-gnu cxxflags: SIX tokens gone
//
// --no-default-config -nostdinc++
// -isystem <payload>/include/c++/v1
// -isystem <payload>/include/<triple>/c++/v1
// -isystem <glibc>/include
// -isystem <linux-headers>/include
//
// ⇒ headers from one library, objects linked from another. On a machine
// with system headers it compiles against /usr/include and links the
// payload — the silent ABI mix; on one without, it fails naming the
// payload.
//
// e2e 295 states the invariant ("naming the host's own target changes
// nothing") and compared only `^ldflags`, so the identity held one line
// above the line where it did not. It now compares both.
//
// Default true = "prebuilt", which is the zero-dependency case and what
// every caller that has no graph (the std module build, the build.mcpp
// host helper) means.
bool cAbiPrebuilt = true;
// DOES THE C++ RUNTIME COME FROM THE GRAPH? -- `plan.targetSide.cxx.fromGraph()`,
// READ rather than derived from `cAbiPrebuilt`.
//
// The payload's libc++ header set was withheld exactly when the C LIBRARY
// was the graph's. The two questions coincide for openkal (both layers
// from packages) and for a native build (both from the payload), and come
// apart on a hosted target whose C library is a located SDK while a
// package supplies libc++: the iOS rows with `llvm.libcxx` (mcpp#630).
// There the old predicate emitted the payload's `-isystem …/c++/v1` on
// top of the package's headers, two libc++ on one command line.
bool cxxFromGraph = false;
// THE SDK'S C++ HEADERS INSTEAD OF THE PAYLOAD'S -- `Toolchain::appleSdkCxxHeaders`,
// read rather than derived from `appleSdkRoot`: prepare decides it from
// whether the graph imports `std`, which this function cannot see.
bool appleSdkCxxHeaders = false;
};
// THE TWO C FLOATING MACROS AN APPLE SDK LEAVES TO <float.h>, as argv words:
// `-DINFINITY=HUGE_VALF` and `-DNAN=__builtin_nanf("0x7fc00000")` for a clang
// compiling for an Apple target, and nothing otherwise.
//
// The macOS 27.0 SDK's <math.h> defines INFINITY and NAN itself only when
// `__has_feature(modules)` is false. With modules on it includes <float.h>
// with `__need_infinity_nan` set and expects the compiler's header to supply
// them, and clang 22's does not in a strict (`-std=c++23`) compile. A module
// interface unit has modules on, so libc++'s std module stopped building
// (measured on the `xcode-27` image, macOS 27.0 26A5406e, llvm 22.1.8:
// `<complex>:1012: use of undeclared identifier 'INFINITY'`; the 26.5 SDK and
// `-std=gnu++23` both build).
//
// The values are the SDK's own GNU-mode spellings, token for token, so where
// <math.h> does define them the redefinition is identical and silent, and
// clang's <__float_infinity_nan.h> undefines before it defines. They are
// stated for every Apple compile rather than only for module units because
// the condition is the SDK's, and one rule is easier to hold than a
// per-unit one. Measured: plain C and C++ units including <math.h>, <cmath>,
// <float.h> and <cfloat> build with `-Wall -Werror` on the 27.0 SDK.
//
// Words, not rendered text: the NAN value holds quotes and parentheses, and
// each reader (ninja text, the std module's shell command, a build program's
// argv) quotes a word its own way.
std::vector<std::string> apple_float_macro_words(const Toolchain& tc);
// Host-compile flags as argv tokens, in the order the string channels have
// always emitted them (clang cfg → deployment target → C library), so
// rendering reproduces today's command lines byte for byte.
std::vector<std::string> host_compile_tokens(const Toolchain& tc,
const HostFlagOptions& opt,
const PathEscape& esc);
// Link-side tokens for a driver invocation that compiles AND links a host
// program in one step — which is what build.mcpp is. The main build keeps its
// own link assembly (it links target artifacts under a different linkage
// policy); this exists so the one-shot host case has a producer at all
// instead of hand-writing one.
std::vector<std::string> host_link_tokens(const Toolchain& tc,
const HostFlagOptions& opt,
const PathEscape& esc);
// A "use this BMI" flag as argv tokens.
//
// BmiTraits stores these for the ninja string channel, where the shape does
// not matter: `-fmodule-file=std=<p>` is one word but `/reference std=<p>` is
// two, and a string consumer never has to know. An argv consumer does — one
// element containing a space is a single argument with a space in it, which
// cl.exe rejects. Split at the prefix's last space, the same rule the ninja
// side's quoting uses.
std::vector<std::string> bmi_reference_tokens(std::string_view usePrefix,
const std::filesystem::path& bmi);
// The first `<name>=<path>` token in `argv` that no switch introduces, if any.
//
// A DEFECT THAT IS ONLY VISIBLE IN THE ASSEMBLED ARGV. `bmi_reference_tokens`
// returns MSVC's reference as a PAIR -- `/reference`, then `<name>=<path>` --
// because cl.exe takes the two as separate arguments. The pair's halves are
// individually well-formed, so every check that reads one token at a time
// passes while the pair is broken. Measured: a per-token de-duplicator dropped
// the second `/reference` (already present from the bundled `mcpp` module) and
// left its partner standing alone, which cl read as a source file name:
//
// c1xx: fatal error C1083: Cannot open source file:
// 'huxerui.rules.sources=...\huxerui.rules.sources.ifc'
//
// A message that names the module and the BMI and does not name the flag, so
// it reads as a missing file rather than as a missing switch.
//
// The rule: a token that carries `=` and does not itself begin with `-` or `/`
// is an argument TO something, and the token before it must be a switch. This
// is defence in depth and not the fix -- the fix is that nothing filters the
// pair any more -- but the failure it converts is expensive to diagnose from
// cl's own words, and the check costs one pass over an argv that is already
// being built.
std::optional<std::string> orphaned_reference(
const std::vector<std::string>& argv);
} // namespace mcpp::toolchain
namespace mcpp::toolchain {
std::vector<std::string> apple_float_macro_words(const Toolchain& tc) {
if (tc.compiler != CompilerId::Clang) return {};
auto tt = triple::parse(tc.targetTriple);
if (!tt || !tt->is_apple()) return {};
return {"-DINFINITY=HUGE_VALF", "-DNAN=__builtin_nanf(\"0x7fc00000\")"};
}
std::vector<std::string> host_compile_tokens(const Toolchain& tc,
const HostFlagOptions& opt,
const PathEscape& esc) {
std::vector<std::string> out;
// A TOOLCHAIN THAT SHIPS ITS OWN SYSROOT IS TOLD NOTHING.
//
// What this function emits is a target's system reconstructed onto the
// command line: libc++'s headers, glibc's, the Linux UAPI headers, the
// cfg bypass, the C-runtime prefix. Every one of those is an answer mcpp
// supplies because the payload's clang does not have one. An Emscripten or
// Android SDK does: `em++` bakes `--sysroot=<payload>/.../cache/sysroot`
// into every invocation and the NDK's clang derives its bionic sysroot
// from its own install prefix.
//
// Measured before this gate, on the std module precompile for
// `wasm32-emscripten`:
//
// em++ ... -isystem'<xim-x-glibc>/include' -isystem'<linux-headers>/include'
// --precompile <emsdk sysroot>/share/libc++/v1/std.cppm
// <xim-x-glibc>/include/gnu/stubs.h:7: fatal error:
// 'gnu/stubs-32.h' file not found
//
// This host's glibc headers, handed to a wasm compile. The error names a
// missing 32-bit stub, so it reads as a broken glibc payload rather than
// as a C library that has no business being there.
//
// The cfg bypass is withheld too, and deliberately: it exists to stop
// clang reading a per-install `clang++.cfg`, while `em++` is a wrapper
// whose entire job is to supply configuration. Suppressing it would be
// suppressing the toolchain.
//
// "NOTHING" WAS ONE TOKEN TOO STRONG, AND THIS FUNCTION ALREADY SAID SO
// FURTHER DOWN. The paragraph beginning "THE TRIPLE, SAID OUT LOUD" states
// the opposite rule for the same reason -- an ordinary clang emits for the
// machine it is running on unless told otherwise -- and this early return
// stood in front of it, so the stronger claim won by position.
//
// Both are right about their own object. The SYSTEM is the payload's and
// must not be reconstructed; WHICH TARGET is still mcpp's to say, because
// one NDK serves both Android arches and nothing on the command line
// otherwise distinguishes them. Measured on `aarch64-linux-android`, with
// the std module already correct:
//
// error: AST file 'std.pcm' was compiled for the target
// 'aarch64-unknown-linux-android21' but the current translation unit
// is being compiled for target 'x86_64-unknown-linux-gnu'
//
// Two machines in one build, reported by the module loader rather than by
// either compile -- and then eight cascading "use of undeclared identifier
// 'std'" errors, which is what a reader sees first.
if (auto tt = triple::parse(tc.targetTriple); tt && tt->has_own_sysroot()) {
if (!tc.crossTargetFlag.empty()) out.push_back(tc.crossTargetFlag);
return out;
}
// MSVC carries none of this on the command line: cl.exe and link.exe find
// headers and import libraries through INCLUDE / LIB, which detection
// synthesizes into tc.envOverrides. Emitting the GNU shapes below would
// produce a string of unknown options and then LNK1181.
if (tc.compiler == CompilerId::MSVC) return out;
const auto dm = resolve_clang_driver(tc);
const auto lm = resolve_link_model(tc);
// THE TRIPLE, SAID OUT LOUD, WHEN NOTHING ELSE SAYS IT.
//
// Every hosted cross this build tool could do was served by a payload whose
// driver had exactly one target — `x86_64-w64-mingw32-g++` needs no
// `--target` because it has no choice. So nothing emitted one outside the
// freestanding path, and the assumption "the driver knows" was true.
//
// It stops being true the moment the TARGET SIDE comes from the dependency
// graph instead of from a payload. Then the compiler is an ordinary clang,
// which emits every format it was built with, and which will emit for THIS
// machine unless told otherwise.
//
// Measured 2026-08-23. A build for `aarch64-macos` with an explicit
// `[target.aarch64-macos] toolchain = "llvm@…"` resolved the whole graph,
// took the C library's aarch64 headers, and compiled with no `--target` —
// host code generation, target declarations. It was caught by an assertion
// the C library port wrote for precisely this situation:
//
// the C library and the compiler disagree about LDBL_DIG ('33 == 18')
//
// 33 is aarch64's binary128 and 18 is x87: two machines in one command.
//
// The decision itself is not made here — see Toolchain::crossTargetFlag,
// which is set where both the request and the compiler are known. This
// reads it.
if (!tc.crossTargetFlag.empty()) out.push_back(tc.crossTargetFlag);
// AND WHAT A `throw` AND A `thread_local` COMPILE INTO, WHICH IS A
// PROPERTY OF THE GRAPH AND NOT OF ANY ONE PACKAGE — see
// `graph_runtime_compile_flags` for what and why.
//
// IT WAS DECLARED PER-PACKAGE, WHICH IS EXACTLY AS FAR AS IT REACHED.
// `openkal-llvm-runtime` set `-fdwarf-exceptions` in its own `[build]`, so
// its objects agreed with each other and nothing else did. Measured
// 2026-08-23 — every object compiled, and the link said:
//
// ld.lld: error: undefined symbol: __gxx_personality_seh0
//
// referenced from the CONSUMER's `main.o`, which had a `try` block and no
// reason to know any of this. A user cannot be asked to write a flag whose
// necessity is a fact about their dependencies.
for (auto& f : graph_runtime_compile_flags(tc)) out.push_back(f);
const bool bypassCfg =
dm.hasCfg && (opt.cfgBypass == HostFlagOptions::CfgBypass::Always
|| (opt.cfgBypass == HostFlagOptions::CfgBypass::LinuxOnly
&& mcpp::platform::is_linux));
// Trusting the cfg means contributing no include paths, stdlib selection
// or runtime choices — it already carries them. It does NOT mean
// contributing nothing: the deployment target still has to be stated (see
// below), which is why this suppresses the two blocks rather than
// returning early.
const bool trustCfg = !bypassCfg && dm.hasCfg;
// AND NOT WHEN THE TARGET SIDE COMES FROM THE GRAPH — the compile-side
// counterpart of the replacement `flags.cppm` makes on the link line.
//
// These tokens are the payload's: `-isystem <payload>/include/c++/v1` and
// the C library beside it. For an openkal target the C++ runtime and the C
// library are packages, and the payload's copies are built for the machine
// doing the building.
//
// `-nostdinc++` DOES NOT REMOVE THEM, which is what makes this its own
// fix rather than a flag. That option suppresses the DRIVER's own C++
// search; a path put there explicitly with `-isystem` stays. Measured
// 2026-08-23, cross-compiling openkal-windows — a package that uses no C++
// standard library at all — with `-nostdinc++` on the command line:
//
// winnt.h:16 → …/xim-x-llvm/…/include/c++/v1/ctype.h
// → __config:13 '__config_site' file not found
//
// mingw's own header asked for `<ctype.h>`, and the payload's libc++ was
// still ahead of the sysroot that had just been pointed at the right place.
//
// READ, NOT DERIVED — see HostFlagOptions::cAbiPrebuilt for the
// measurement that replaced `!tc.crossTargetFlag.empty()` here. This site
// and `flags.cppm`'s link side now ask one question of one value, so they
// cannot disagree.
const bool graphSuppliesTarget = !opt.cAbiPrebuilt;
// THE C++ HEADERS ARE THE C++ LAYER'S QUESTION. `dm.compile_tokens` carries
// libc++'s directories and nothing else, so it is emitted only when the
// payload's libc++ headers are the ones in use: not when a package
// supplies the C++ layer (`cxxFromGraph`), and not when prepare chose
// the SDK's headers for an Apple cross target whose runtime is the SDK's
// libc++ (`appleSdkCxxHeaders`). Measured on Xcode 16.4 with llvm 22.1.8:
// the payload's libc++ 22 headers over the SDK's libc++ 19 dylib fail at
// link on `__hash_memory`, which an inline function in the newer headers
// names and the older dylib does not export (mcpp#630).
const bool cxxFromPayload = !opt.cxxFromGraph && !opt.appleSdkCxxHeaders;
if (bypassCfg && !graphSuppliesTarget && cxxFromPayload) {
for (auto& t : dm.compile_tokens(esc, opt.clangStdlibSelect))
out.push_back(t);
} else if (bypassCfg) {
// THE BYPASS IS NOT PART OF THE PAYLOAD'S HEADER SET, AND IT WAS
// BEING SUPPRESSED WITH IT.
//
// The payload's `-isystem` rows describe a C library this target does
// not use, so the branch above is right to withhold them. The cfg
// bypass is a different statement: `post_install.cppm` calls that file
// "a per-machine, per-install-path artifact", and reading it makes the
// command line depend on what happened to be installed when the
// payload landed.
//
// Measured on 2026.8.26.2: `mcpp build --target <the host's own>`
// dropped `--no-default-config`, so clang read `bin/clang++.cfg` and
// the build silently inherited that machine's install. It is also what
// made a hand-written `<triple>-clang++.cfg` a working workaround for
// mcpp#514 — a workaround that only exists because this token went
// missing.
//
// Emitted here rather than moved into `compile_tokens`: that vector's
// rendering is part of the std module's cache identity, and reordering
// it would invalidate every user's std BMI for no behavioural gain.
// Nothing that used to be emitted moves; this path emitted nothing.
out.push_back("--no-default-config");
}
// THE C LIBRARY'S OWN HOST LOCATIONS, WHEN A GRAPH PACKAGE SUPPLIES THE
// TARGET'S C LIBRARY. The link side has read this exact value since
// #511 (`plan.targetSide.cAbi.prebuilt()`, by way of `graphSuppliesTarget`
// above) and dropped `-nostdlib` accordingly; this was the missing
// compile-side half (#662).
//
// `-nostdlibinc` rather than `-nostdinc`: the latter also drops the
// COMPILER's OWN bundled headers (stddef.h, stdarg.h, the builtin
// intrinsics), which are the compiler's layer and not the C library's —
// a graph-supplied musl still expects them ahead of its own copies on
// the search path. A package that needs the stronger form states
// `-nostdinc` itself; openkal-musl already does, in its own unit flags.
//
// Measured (x86_64-windows-gnu, openkal-musl over openkal-windows,
// clang 22.1.8, `-xc -v -fsyntax-only`): without this token the driver's
// header search list still ends in `/usr/x86_64-w64-mingw32/include` —
// the HOST's mingw, which happened to satisfy every text `#include` the
// graph's own headers did not, until one of its declarations disagreed
// with musl's (`typedef redefinition`, `conflicting types for 'chmod'`).
// With it, the list ends at the compiler's own resource directory.
//
// GCC has no equivalent single flag — the shape would be `-nostdinc`
// plus `-isystem <gcc -print-file-name=include>` and `<…/include-fixed>`,
// re-adding exactly the two directories GCC's own C-library search
// already contributes beside the sysroot. Not implemented: `dm.hasCfg`
// is false for GCC (resolve_clang_driver only looks for a `<driver>.cfg`
// beside a Clang binary), so `bypassCfg` already withholds this whole
// block from that family — GCC stays exactly as it was before #662,
// unisolated, on every target row this codebase's own table pairs it
// with today (test_hostflags.cpp,
// GccEmitsNeitherIsolationTokenRegardlessOfGraphOrigin).
if (bypassCfg && graphSuppliesTarget) out.push_back("-nostdlibinc");
// THE CONDITION USED TO BE `!graphSuppliesTarget && !cxxFromPayload`,
// WHICH IS RIGHT ABOUT `cxxFromPayload` AND WRONG TO ASK ABOUT THE C
// LIBRARY AT ALL — this token is the C++ LAYER's question, exactly as
// the comment on `cxxFromPayload` above already says. Asking about the
// C library too meant the one case where BOTH layers come from the graph
// (openkal: `graphSuppliesTarget` true, `cxxFromGraph` true) answered
// `!graphSuppliesTarget` false and never got here — so clang kept
// searching beside itself for the payload's libc++, found the HOST's
// libstdc++ instead (`/usr/lib/gcc/x86_64-w64-mingw32/…/include/c++`),
// and every unit that `#include`s a header transitively reaching it saw
// two C++ standard libraries at once (#662, the C++ twin of the C defect
// above — unreached by the issue's own repro, which only `import std`s).
//
// `!cxxFromPayload` is exactly "the payload is not the one supplying
// these headers", which is true for both `cxxFromGraph` (a package does)
// and `appleSdkCxxHeaders` (the SDK does) — the two cases this branch
// already told apart below by whether `opt.cxxFromGraph` is set.
if (bypassCfg && !cxxFromPayload) {
// The driver's own C++ search contributes nothing: beside the compiler
// it finds the payload's libc++, and clang's Darwin driver prefers that
// copy to the SDK's whenever it exists. What replaces it is either the
// graph package's directories, which reach every unit through the
// target-side broadcast, or the SDK's `c++/v1`, named here.
out.push_back("-nostdinc++");
if (opt.clangStdlibSelect) out.push_back("-stdlib=libc++");
if (!opt.cxxFromGraph)
out.push_back("-isystem"
+ esc(opt.appleSdkRoot / "usr" / "include" / "c++" / "v1"));
}
// Unconditional on macOS, cfg or no cfg. clang refuses to load a module
// built for a different deployment target, and this result feeds every
// compile that touches one — the bundled mcpp module's precompile, its
// object step, and the build.mcpp compile. Skipping it on the trust-cfg
// path is exactly the mismatch e2e 181 catches: the std BMI is built for
// 14.0 while the TU importing it is not.
//
// AND ONLY FOR A macOS TARGET. This asked whether the HOST is macOS, which
// was the same question while macOS was the only Apple target mcpp could
// build for. The iOS rows are built ON a macOS host and FOR another
// platform, and clang refuses the combination outright:
//
// error: invalid argument '-mmacosx-version-min=14.0' not allowed with
// 'arm64-apple-ios18.0'
//
// so the flag would not merely be useless there, it would stop the build.
// The iOS deployment target travels in the effective triple instead --
// `arm64-apple-ios18.0` -- which is one place rather than two for the same
// value.
if (mcpp::platform::is_macos && !opt.macosDeploymentTarget.empty()
&& opt.appleSdkRoot.empty())
out.push_back("-mmacosx-version-min=" + opt.macosDeploymentTarget);
// THE APPLE CROSS TARGET'S OWN SDK, on the compile side.
//
// A native macOS build needs nothing here: it reads the payload's
// `clang++.cfg`, which `post_install.cppm` filled with the located macOS
// SDK. An Apple cross suppresses that cfg because it names the wrong
// platform, so this is the only thing that tells the driver where the
// iPhoneOS headers are. `-isysroot` is JoinedOrSeparate in clang, so the
// joined form is one token like every other path here.
if (!opt.appleSdkRoot.empty())
out.push_back("-isysroot" + esc(opt.appleSdkRoot));
if (!trustCfg && !graphSuppliesTarget
&& (bypassCfg || lm.mode != CLibMode::None))
for (auto& t : lm.compile_tokens(esc)) out.push_back(t);
return out;
}
std::optional<std::string> orphaned_reference(
const std::vector<std::string>& argv) {
auto is_switch = [](std::string_view t) {
return !t.empty() && (t.front() == '-' || t.front() == '/');
};
for (std::size_t i = 0; i < argv.size(); ++i) {
std::string_view t = argv[i];
if (is_switch(t) || t.find('=') == std::string_view::npos) continue;
// A path can contain `=`, and an input file is a legitimate bare
// token. What distinguishes a reference is that its `=` precedes any
// directory separator: `<name>=<path>` names a module first.
auto eq = t.find('=');
auto sep = t.find_first_of("/\\");
if (sep != std::string_view::npos && sep < eq) continue;
if (i == 0 || !is_switch(argv[i - 1]))
return std::string(t);
}
return std::nullopt;
}
std::vector<std::string> bmi_reference_tokens(std::string_view usePrefix,
const std::filesystem::path& bmi) {
std::string_view p = usePrefix;
while (!p.empty() && p.front() == ' ') p.remove_prefix(1);
if (p.empty()) return {};
auto sp = p.find_last_of(' ');
if (sp == std::string_view::npos)
return { std::string(p) + bmi.string() };
return { std::string(p.substr(0, sp)),
std::string(p.substr(sp + 1)) + bmi.string() };
}
// The toolchain's own runtime directories, on both exits of the function
// below. `-L` is link-time and wanted everywhere; rpath is an ELF and Mach-O
// concept. A PE target reaches here too, where the rpath flag is inert and
// self-containment comes from the static link instead (#299).
void append_runtime_lib_dirs(const Toolchain& tc, const HostFlagOptions& opt,
const PathEscape& esc, std::vector<std::string>& out) {
if (!opt.runtimeLibDirs) return;
for (auto& d : tc.linkRuntimeDirs) {
out.push_back("-L" + esc(d));
if constexpr (mcpp::platform::supports_rpath)
out.push_back("-Wl,-rpath," + esc(d));
}
}
std::vector<std::string> host_link_tokens(const Toolchain& tc,
const HostFlagOptions& opt,
const PathEscape& esc) {
std::vector<std::string> out;
if (tc.compiler == CompilerId::MSVC) return out;
const auto dm = resolve_clang_driver(tc);
const auto lm = resolve_link_model(tc);
const bool bypassCfg =
dm.hasCfg && (opt.cfgBypass == HostFlagOptions::CfgBypass::Always
|| (opt.cfgBypass == HostFlagOptions::CfgBypass::LinuxOnly
&& mcpp::platform::is_linux));
if (bypassCfg) {
for (auto& t : dm.link_tokens(esc)) out.push_back(t);
} else if (dm.hasCfg) {
// Trusting the cfg — with ONE exception, and it is not a preference.
//
// The cfg picks runtimes; it does not pick a linker, so on macOS the
// default is Xcode's /usr/bin/ld. That binary is itself a C++ Mach-O
// linked against libc++, and it runs inside the same DYLD_* the
// payload toolchain sets up, so dyld resolves ITS libc++ to the
// payload's. When that copy lacks a symbol Apple's ld needs, ld
// aborts before it links anything:
//
// dyld: Symbol not found: __ZdaPv (operator delete[])
// Referenced from: .../XcodeDefault.xctoolchain/usr/bin/ld
// Expected in: .../xim-x-llvm/22.1.8/lib/libc++.1.0.dylib
//
// The MAIN build already refuses to use Xcode's ld for exactly this,
// and says so at flags.cppm's macOS branch: "Xcode 15.4's ld aborting
// at launch on macos-14 CI when its libc++ resolution was diverted".
// The host helper links in the same environment, so it cannot be
// allowed to differ — a toolchain that builds the project but not its
// build.mcpp is not a working toolchain (mcpp#437).
//
// lld ships with the very toolchain doing the compile, so it cannot
// be diverted to a libc++ it was not built against.
if constexpr (mcpp::platform::is_macos) out.push_back("-fuse-ld=lld");
// AND DELIBERATELY NOT THE TOOLCHAIN'S RUNTIME DIRECTORIES. THIS WAS
// TRIED, AND WHAT IT COSTS IS RECORDED HERE RATHER THAN REDISCOVERED.
//
// The motivation is real. Trusting the cfg decides WHICH runtimes are
// linked and never decides WHERE they are found, so on macOS `-lc++`
// resolves through the SDK to /usr/lib/libc++.tbd -- the system copy,
// whose version floats with the host OS -- while the headers come from
// the payload. On macOS 14 those two disagree, measured on a build
// program that does nothing but `import std`:
//
// ld64.lld: error: undefined symbol:
// std::__1::__is_posix_terminal(__sFILE*)
// >>> referenced by std::__1::__print::__is_terminal(__sFILE*)
//
// `std::print` is not header-only; that support symbol arrived in a
// libc++ macOS 14 does not ship, and macOS 15's copy has it, which is
// why every macOS runner this project uses was green.
//
// ADDING `-L<payload>/lib` HERE FIXES THAT AND BUYS A WORSE PROBLEM.
// It makes `-lc++` resolve to the toolchain's own dylib, which is the
// ToolchainCoupled contract that `dist::mechanism_for` REFUSES on
// Mach-O for a measured reason: LLVM's macOS libc++abi and libunwind
// dylibs upward-link /usr/lib/libc++, so the system libc++ loads
// alongside the toolchain's and an object freed across the two aborts
// in libmalloc (#202). The first step of that path is what CI reported
// when this was tried -- the link stopped on `__cxa_end_catch`,
// `std::runtime_error::~runtime_error()` and the rest of the ABI
// surface the system libc++ re-exports and the payload's does not.
//
// So the C++ runtime on this host is the system one, and the
// deployment floor is made real by the STATIC libc++ the distribution
// contract selects -- a mechanism that belongs to an artifact and not
// to a helper mcpp compiles, runs here, and throws away. The
// consequence, stated because it is a real limit: on macOS 14 a build
// program cannot use `std::print` or `std::println`. `std::format` is
// header-only and has none of this.
return out;
}
for (auto& t : lm.link_tokens(esc)) out.push_back(t);
if (opt.binutilsPrefix) {
if (auto ar = archive_tool(tc); !ar.empty())
out.push_back("-B" + esc(ar.parent_path()));
}
append_runtime_lib_dirs(tc, opt, esc, out);
return out;
}
} // namespace mcpp::toolchain