-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathtriple.cppm
More file actions
1062 lines (1003 loc) · 58.4 KB
/
Copy pathtriple.cppm
File metadata and controls
1062 lines (1003 loc) · 58.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// mcpp.toolchain.triple — the single source of truth for target identity.
//
// mcpp owns its target-triple language: canonical form is `arch-os[-env]`
// (three segments, no vendor — Zig-style). `x86_64-linux-musl` was already
// canonical before this module existed; this extends the same convention to
// every target. GNU/LLVM spellings (`x86_64-w64-mingw32`,
// `x86_64-unknown-linux-gnu`, `arm64-apple-darwin24`) are permanent input
// aliases, normalized here.
//
// Everything that previously parsed triples ad hoc (cfgpred::context_for,
// abi_profile, model.cppm's is_*_target, registry's musl signals) consumes
// this module now. Vocabulary: os ∈ {linux, macos, windows} (never "darwin"),
// arch is the GNU spelling ({x86_64, aarch64, riscv64, …} — never "arm64"),
// env ∈ {gnu, musl, msvc} (empty on macos). `static` is NOT part of a triple:
// it is a target's default linkage property, flipped via [build].
//
// The known-target table below is the closed vocabulary `--target` validates
// against (with an escape hatch for explicit [target.X] manifest sections)
// and the source the README platform table is drawn from. Adding a target =
// adding a row here (+ payload mapping in registry.cppm if a new payload
// shape is involved).
//
// See .agents/docs/2026-07-15-toolchain-target-naming-unification-design.md.
export module mcpp.toolchain.triple;
import std;
import mcpp.platform;
export namespace mcpp::toolchain::triple {
// WHAT A TARGET PRODUCES, AS ONE ANSWER RATHER THAN A DERIVATION AT EACH SITE.
//
// The binary format used not to be anything: it was re-derived from `os`
// wherever it was needed -- `is_pe()` asked `os == "windows"`, artifact naming
// asked again, the packer asked a third time -- and that is affordable only
// while the answer has two values. A THIRD produces an addition at every such
// site, and a site that was missed does not fail: it silently answers "ELF",
// because ELF is what every `else` branch in the tree assumes.
//
// That is why this exists before wasm needs it rather than after. `wasm32` is
// the first target in mcpp's vocabulary whose object format is neither of the
// two the tree was written around, and #597 is a target-model change for
// exactly this reason -- not because a table row is hard.
//
// IT IS NOT THE SAME QUESTION AS `is_freestanding()`, and merging them would be
// the mistake this replaces. "Which container do objects come in" and "is there
// an operating system to link against" are different axes: a bare-metal
// RISC-V image is ELF with no OS, and a wasm module has an OS-like layer
// (Emscripten's POSIX emulation) and is not ELF.
enum class ObjectFormat { Elf, MachO, Pe, Wasm };
std::string_view to_string(ObjectFormat f) {
switch (f) {
case ObjectFormat::Elf: return "ELF";
case ObjectFormat::MachO: return "Mach-O";
case ObjectFormat::Pe: return "PE";
case ObjectFormat::Wasm: return "wasm";
}
return "ELF";
}
struct Triple {
std::string arch; // "x86_64" | "aarch64" | "riscv64" | ... (GNU spelling)
std::string os; // "linux" | "macos" | "windows"
std::string env; // "gnu" | "musl" | "msvc" | "" (always empty on macos)
// WHETHER THE ENV SEGMENT WAS WRITTEN, AS OPPOSED TO SUPPLIED BY THIS
// PARSER — AND THE TRIPLE HAS TO CARRY BOTH BECAUSE IT SERVES TWO ROLES.
//
// A triple is an IDENTITY — the output directory's name, part of a cache
// key, the subject of a `cfg()` predicate — and identities must be total
// and canonical. It is also a REQUEST, and a request has to be able to say
// nothing. `parse` makes the identity total by filling `x86_64-linux` in as
// `x86_64-linux-gnu`, and until this flag existed that filling ALSO
// destroyed the request: the two states were indistinguishable downstream.
//
// Measured: a project whose graph supplies musl, built with
// `--target x86_64-linux`, reported
//
// Target x86_64-linux-gnu → x86_64-unknown-linux-gnu
// c-abi musl (openkal-musl@0.3.3, graph)
//
// — a name that contradicts the fact printed two lines under it. The user
// had declined to name a C library; the parser named one for them.
//
// The narrow shape is deliberate. Removing the fill would make `env` empty
// for a hosted target at 22 read sites, ten of which are in this file, and
// every one would need a new answer for a state that never existed before.
// A flag beside the value leaves the identity exactly as it was and gives
// the request somewhere to live.
bool envExplicit = false;
bool empty() const { return arch.empty() && os.empty(); }
// Canonical rendering: "arch-os[-env]"; "" for an empty (= host) triple.
std::string str() const {
if (empty()) return {};
std::string s = arch + "-" + os;
if (!env.empty()) { s += "-"; s += env; }
return s;
}
// THE SPELLING A COMPILER TAKES, WHICH IS NOT THE SPELLING mcpp USES.
//
// `str()` is mcpp's vocabulary: short, unambiguous, and the thing a user
// types. LLVM's is a four-field form with a vendor, and on Apple platforms
// the architecture has a different name and the OS carries a version.
//
// THIS EXISTS BECAUSE CROSS-COMPILING USED TO MEAN SOMETHING NARROWER.
// Every hosted cross mcpp could do was served by a payload whose DRIVER was
// already specialised — `x86_64-w64-mingw32-g++` needs no `--target`,
// because it has only one. So nothing ever needed this function, and
// nothing emitted `--target=` outside the freestanding path.
//
// openkal changes the shape of the question. The target side — headers,
// C library, C++ runtime, the OS's own openkal implementation — is a set of
// PACKAGES in the dependency graph, built from source by whichever compiler
// is running. What remains for the compiler is code generation, and clang
// emits every format it was built with from one binary. There is no payload
// to specialise, so the triple has to be said out loud.
//
// Measured 2026-08-23, before this existed: a build for `aarch64-macos`
// resolved the whole graph, took musl's aarch64 headers, and compiled with
// NO `--target` at all — so the host's x86_64 code generation met aarch64
// declarations. What caught it was the port's own assertion, which exists
// for exactly this:
//
// okm_float_assert.c: the C library and the compiler disagree about
// LDBL_DIG ('33 == 18')
//
// 33 is aarch64's binary128; 18 is x87. Two machines in one command line.
std::string llvm_triple(std::string_view macosVersion = {}) const {
if (empty()) return {};
if (os == "macos") {
// Apple spells the 64-bit ARM architecture `arm64`, and the OS
// component carries the deployment target: `arm64-apple-macos14`.
// Without a version clang picks its own default, which is a
// decision belonging to the project rather than to the compiler.
const std::string a = (arch == "aarch64") ? "arm64" : arch;
std::string t = a + "-apple-macos";
t += macosVersion.empty() ? std::string("14.0")
: std::string(macosVersion);
return t;
}
if (os == "windows") {
if (is_msvc_env()) return arch + "-pc-windows-msvc";
return arch + "-w64-windows-gnu";
}
// APPLE'S OTHER OS. Same `arm64` spelling and the same vendor segment;
// what differs is the SDK and the deployment-target flag.
//
// NO VERSION IS BAKED IN, unlike the macOS branch above, and that is a
// decision rather than an omission. `-miphoneos-version-min` belongs to
// the layer that also owns the SDK path and the `.app` bundle -- a
// distribution plugin -- and a default written here would be a second
// place that answers it. clang picks its own when nothing says.
if (os == "ios") {
const std::string a = (arch == "aarch64") ? "arm64" : arch;
return a + "-apple-ios";
}
// ANDROID IS LINUX, AND THE ENV SEGMENT IS WHERE IT SAYS SO. clang also
// accepts an API level fused onto the OS segment
// (`aarch64-linux-android24`), which selects which bionic symbols are
// visible; it is omitted here for the reason the iOS version is --
// the minimum platform version is the project's statement, and clang
// has a default.
if (os == "linux") return arch + "-unknown-linux-" + (env.empty() ? "gnu" : env);
// Emscripten's own effective triple. The vendor segment is `unknown`
// and the OS segment is the platform layer rather than a kernel, which
// is why `object_format()` reads the ARCH for this row.
if (os == "emscripten") return arch + "-unknown-emscripten";
if (os == "none") return str(); // freestanding: already LLVM's form
return str();
}
bool is_musl() const { return env == "musl"; }
bool is_msvc_env() const { return env == "msvc"; }
bool is_windows_gnu() const { return os == "windows" && env == "gnu"; }
// THE SINGLE DERIVATION. Every question about the container objects come in
// is answered here and nowhere else -- see `ObjectFormat` for why a third
// value makes that a requirement rather than a preference.
//
// The arch test precedes the ELF fallback because a wasm target's OS
// segment names a platform layer (`emscripten`), not a format, and the
// fallback would otherwise claim ELF for it -- the silent wrong answer this
// whole axis exists to remove.
ObjectFormat object_format() const {
if (os == "windows") return ObjectFormat::Pe;
if (os == "macos" || os == "ios") return ObjectFormat::MachO;
if (arch.starts_with("wasm")) return ObjectFormat::Wasm;
return ObjectFormat::Elf;
}
// Kept as its own name because it is what 30-odd sites already ask, and now
// reads the single answer rather than re-deriving one.
bool is_pe() const { return object_format() == ObjectFormat::Pe; }
bool is_mach_o() const { return object_format() == ObjectFormat::MachO; }
bool is_wasm() const { return object_format() == ObjectFormat::Wasm; }
// APPLE, AS ONE QUESTION. `os == "macos"` was the whole of it while macOS
// was the only Apple row; iOS shares the object format, the linker, the
// `arm64` spelling and `codesign`, and differs in the SDK and the
// deployment-target flag. A site that means "Apple" and asks "macOS" gets
// iOS wrong in the direction that still links.
bool is_apple() const { return os == "macos" || os == "ios"; }
// Android is Linux with a different C library and a different loader path.
// `os` stays `linux` for that reason -- it is the kernel, and every
// Linux-shaped decision in the tree is right about it -- and the env
// segment carries what differs.
bool is_android() const { return env == "android"; }
// Bare metal: there is no OS to link against. THE predicate every
// freestanding decision keys off, spelled once here so no consumer
// re-derives it from `os == "none"` and drifts.
bool is_freestanding() const { return os == "none"; }
// WHETHER THIS ROW'S TOOLCHAIN PIN IS A CAPABILITY RATHER THAN A
// CONVENTION — the distinction that decides whether an author may override
// it.
//
// A hosted row's pin answers "which payload supplies this target's C
// library", and an author who supplies one may name any compiler. These
// rows answer a different question, and the answer does not depend on who
// supplies what:
//
// freestanding no per-host cross payload exists at all; clang and lld
// are cross-compilers by construction and gcc is not.
// PE + musl no gcc payload emits a PE with a musl C library. The
// mingw payload emits PE with the MinGW CRT, which is the
// separate `-gnu` row; there is no third gcc.
//
// SPELLED HERE RATHER THAN AT EACH DECISION, because the first version
// said `is_freestanding()` at two of them and `x86_64-windows-musl` — a row
// added later — was a convention at both. Measured: declaring gcc for it
// resolved the host's Linux musl payload and reported a missing C++
// frontend.
bool pin_is_capability() const { return is_freestanding() || (is_pe() && is_musl()); }
// cfg() `family` dimension: unix | windows.
//
// iOS and Android are unix for the reason macOS and Linux are: the
// predicate answers about the API surface a source can assume, and both are
// POSIX. Emscripten is unix on the same test rather than on a claim about
// wasm -- it supplies a POSIX emulation, and a source guarded by
// `cfg(unix)` compiles against it. A target with no OS still answers
// nothing, unchanged: `cfg(unix)` on bare metal would be false in a way no
// source could act on.
std::string family() const {
if (os == "windows") return "windows";
if (os == "linux" || os == "macos" || os == "ios"
|| os == "emscripten") return "unix";
return {};
}
// NASM `-f` output format for this target. NASM is x86-family only:
// nullopt off x86, and the caller must hard-error (suggesting cfg-gated
// sources) rather than pick a format.
std::optional<std::string> nasm_format() const {
bool x64 = arch == "x86_64";
bool x32 = arch == "x86" || arch == "i386" || arch == "i486"
|| arch == "i586" || arch == "i686";
if (!x64 && !x32) return std::nullopt;
if (os == "windows") return x64 ? "win64" : "win32";
if (os == "macos") return x64 ? "macho64" : "macho32";
if (os == "linux") return x64 ? "elf64" : "elf32";
return std::nullopt;
}
// IDENTITY IS THE THREE SEGMENTS, AND `envExplicit` IS DELIBERATELY NOT
// AMONG THEM — WHICH IS WHY THIS IS NOT `= default`.
//
// The flag records where the env segment came from, not what the target is.
// A defaulted comparison would make `x86_64-linux-gnu` written by a user
// unequal to the same triple derived by `host_triple`, and the first thing
// that breaks is the `host` tag in `mcpp toolchain list`, which compares
// exactly those two.
bool operator==(const Triple& o) const {
return arch == o.arch && os == o.os && env == o.env;
}
};
// Lenient parse of any recognizable triple spelling into canonical fields.
// Handles mcpp-canonical ("x86_64-linux-musl"), GNU ("x86_64-w64-mingw32",
// "x86_64-pc-linux-gnu"), LLVM/Rust 4-segment ("x86_64-unknown-linux-musl",
// "x86_64-pc-windows-msvc") and Apple ("arm64-apple-darwin24.1.0",
// "arm64-apple-macosx15.0") forms. Returns nullopt when no OS is
// recognizable — the input is not a triple at all.
std::optional<Triple> parse(std::string_view s);
// ── Known-target registry (closed vocabulary; data, not code) ────────────────
//
// tier semantics (Rust-style):
// verified — CI builds AND executes the artifact end-to-end (qemu/wine count)
// preview — it builds and links; no execution has been recorded for the row
// planned — registered intent; payload or CI row not wired yet
struct TargetInfo {
std::string_view canonical; // "x86_64-linux-musl"
std::string_view tier; // "verified" | "preview" | "planned"
std::string_view note; // display annotation: "static" / "PE" / ""
// Convention toolchain pin for `--target <canonical>` with no explicit
// [target.X] toolchain override. Empty = no convention (host default).
std::string_view pin;
// The TARGET's C library, resolved at compile time exactly the way `pin`
// resolves the compiler. Empty = none applies.
//
// This axis exists because bare metal was the one target class without
// it, and the gap leaked into every package. A hosted target gets its libc
// automatically — `x86_64-linux-musl` carries musl inside its gcc payload,
// and glibc arrives through PayloadPaths — so nobody writes `xim:glibc` in
// a manifest. Freestanding pins a generic clang, which brings no target
// libc at all, so before this every bare-metal package had to declare
// `[xlings] deps = ["xim:picolibc-riscv@1.8.12"]` itself. That is not a
// dependency of the package; it is a property of the target, and stating
// it per-package bound a board-support package and a standard-library
// subset alike to one libc, one ISA and one version.
std::string_view sysroot;
bool defaultStatic; // target's default linkage is static
};
// (note deliberately excludes "static" — the display layer derives that tag
// from defaultStatic, so listing it here would duplicate it.)
inline constexpr TargetInfo kKnownTargets[] = {
// canonical tier note pin sysroot defaultStatic
{ "x86_64-linux-gnu", "verified", "", "", "", false },
{ "x86_64-linux-musl", "verified", "", "gcc@16.1.0", "", true },
{ "aarch64-linux-musl", "verified", "", "gcc@16.1.0", "", true },
{ "x86_64-windows-gnu", "verified", "PE", "gcc@16.1.0", "", true },
// musl ON WINDOWS. IT EXISTS, AND UNTIL THIS ROW mcpp HAD NO NAME FOR IT.
//
// LLVM's triple vocabulary offers `gnu` and `msvc` for Windows and both
// name an ABI, so a reader concludes there is no third possibility and
// calls a musl-based Windows build `-gnu`. The artefact disagrees. Measured
// on one built over `openkal-musl`:
//
// imports ntdll, KERNEL32, SHELL32 — no msvcrt, no ucrtbase
// `_Z…` symbols 4507 `?…` symbols 0
//
// No MinGW C runtime is linked. That is musl on Windows, and calling it
// `gnu` put the one thing the C library is not into its identity, its
// output directory, its `cfg(env = …)` and its packed ABI tag.
//
// THE FIX IS A NAME, NOT A MECHANISM, AND THE REASON THE MISTAKE HELD SO
// LONG IS WORTH RECORDING. "LLVM cannot spell x86_64-windows-musl" is true
// and is about the string handed to CLANG. mcpp's canonical form is a
// different string — the build report prints both, either side of an arrow:
//
// Target x86_64-windows-gnu → x86_64-w64-windows-gnu
// ^ mcpp's identity ^ what clang is given
//
// Letting the compiler's vocabulary bound mcpp's own merged two axes into
// one. `llvm_triple()` already sends every non-MSVC Windows target to
// `…-w64-windows-gnu`, and that spelling is correct there and stays: it
// selects the Itanium C++ ABI, which is the ABI this C library was compiled
// for. mcpp's name answers a different question — which C library — and now
// it can.
//
// mcpp target → clang c-abi
// x86_64-linux-musl → x86_64-unknown-linux-musl musl
// x86_64-windows-gnu → x86_64-w64-windows-gnu MinGW CRT
// x86_64-windows-musl → x86_64-w64-windows-gnu musl
//
// The last two rows differ in the first column and agree in the second,
// which is the whole point.
//
// THE PIN IS `llvm`, AND IT IS NOT A PREFERENCE. The column names the
// payload that supplies this target's C library everywhere else in this
// table; here nothing supplies it, and what the column has to prevent is
// the OPPOSITE — a global default of gcc being carried onto a target no gcc
// can emit. Measured with `mcpp toolchain default gcc@16.1.0`:
//
// error: toolchain payload 'xim:musl-gcc@16.1.0' has no known C++
// frontend in …/xim-x-musl-gcc/16.1.0/bin
//
// — a message about a missing frontend, for a target whose real problem is
// that only clang emits it at all. The bare-metal rows carry `llvm` for the
// same reason and say so in their own note.
//
// The C library still comes from the dependency graph. `host_can_serve`
// says no for this row on a non-Windows host — correctly, for the prebuilt
// system — and that refusal is diagnosed early and RELEASED once the graph
// is known (see the long note at prepare.cppm's `unservedTargetDiagnosis`),
// so a project whose C library comes from a dependency is not turned away.
//
// TIER IS `preview`, NOT `verified`. `verified` in this table means an
// artefact was built AND RUN. Running a PE on a Linux host needs wine, and
// openkal's CI has that step — so this is measurable, and the tier moves
// when it has been measured rather than when it seems likely.
{ "x86_64-windows-musl", "preview", "PE", "llvm@22.1.8","", true },
{ "x86_64-windows-msvc", "verified", "PE", "", "", false },
{ "aarch64-macos", "verified", "", "", "", false },
{ "riscv64-linux-musl", "planned", "", "", "", true },
{ "aarch64-linux-gnu", "planned", "", "", "", false },
{ "x86_64-macos", "planned", "", "", "", false },
// Bare metal. `defaultStatic` is not a preference here — there is no
// loader, so there is no other option. The pin is llvm on every host
// because clang/lld are cross-compilers by construction: unlike the hosted
// rows above, these need no per-host cross payload at all.
// ISA profile (-march/-mabi/-mcmodel) lives in mcpp.freestanding.target,
// which is the single place that decision is made.
// The sysroot column is what keeps a bare-metal PACKAGE from having to
// name a libc: the C library is the target's, like the compiler.
{ "riscv64-none-elf", "verified", "bare","llvm@22.1.8","xim:picolibc-riscv@1.8.12", true },
{ "riscv32-none-elf", "verified", "bare","llvm@22.1.8","xim:picolibc-riscv@1.8.12", true },
// AN EMPTY SYSROOT COLUMN, AND IT IS A STATEMENT RATHER THAN AN OMISSION.
//
// The two rows above name a C library because a project targeting them
// ordinarily wants one. This row does not, because there is no aarch64
// build of picolibc in the index — and, more to the point, because the
// first consumer of this row does not want one. `openarch` is a layer of
// machine mechanism: contexts, traps, page-table entries. It references no
// C library symbol, and a row that resolved one would make every project
// on this target carry a payload it never calls.
//
// An empty column here means exactly what `[target.<triple>].sysroot = ""`
// means in a manifest — the zero-libc tier: no headers on the compile line,
// no library directory on the link, and `#include <stdio.h>` does not
// resolve. A project that wants a C library on this target says so in its
// own manifest, which is also how it would choose a different one.
//
// The tier is `preview` and not `verified`: `verified` in this table
// means an image has been built AND RUN for the row, and running one needs
// an emulator. `xim:qemu-arm` provides `qemu-system-aarch64`; until a probe
// has actually booted under it, claiming `verified` would be claiming the
// measurement rather than reporting it.
{ "aarch64-none-elf", "preview", "bare","llvm@22.1.8","", true },
// THIS ROW EXISTS SO THAT A THIRD MACHINE CAN DISAGREE WITH THE FIRST
// TWO, WHICH IS THE ONLY THING THAT TELLS AN ABSTRACTION FROM A HABIT.
//
// riscv64 and aarch64 are both load/store RISC machines with a weak memory
// model and a fixed instruction width, so an interface that fits both may
// fit because it is right or because they are alike. x86_64 is neither: it
// has variable-length instructions, a total-store-order memory model under
// which three of openarch's four barriers need no instruction at all, and
// an interrupt mechanism that is a table of gates rather than a base
// register. What survives all three is an abstraction.
//
// The tier is `preview` for the same reason aarch64's is, and the reason
// is stricter than it sounds: `verified` here means an image was built AND
// RUN. `xim:qemu-x86` does not exist yet — the index carries no
// `qemu-system-x86_64` — so nothing on this row has booted. Claiming
// `verified` would be claiming a measurement that has not been made.
//
// The sysroot column is empty, the zero-libc tier, for the reason given
// above `aarch64-none-elf`: the first consumer is `openarch`, which
// references no C library symbol.
{ "x86_64-none-elf", "preview", "bare","llvm@22.1.8","", true },
// ── Cortex-M ────────────────────────────────────────────────────────────
//
// SEVEN ROWS AND NOT ONE, BECAUSE "Cortex-M" IS NOT AN INSTRUCTION SET.
//
// Every other bare-metal family here is one row per architecture. M-profile
// is not: an object built for `thumbv7em` uses instructions a Cortex-M0
// does not have, and one built for `thumbv6m` runs on both but leaves the
// larger part unused. The two spellings are not a preference a board
// expresses — they produce incompatible objects — so they are rows.
//
// The rule this table states about itself governs: it exists so that
// `--target <triple>` ALONE is enough to produce a correct object file. A
// single `arm-none-eabi` row plus an `-mcpu` the project remembers would
// move a correctness decision out of the table and into every manifest.
//
// THE `eabi`/`eabihf` SUFFIX IS THE FLOAT ABI, AND CLANG ALREADY READS
// IT. Measured on llvm 22.1.8 (`-###`, `-cc1` line): `thumbv7em-none-eabi`
// gives `-mfloat-abi soft` and `-none-eabihf` gives `hard`, with no flag
// from us. So the ABI needs no entry in the ISA table's `extra` column —
// only the FPU does, and only on the soft rows. See `kThumbSoftExtra`.
//
// `sysroot` IS EMPTY ON EVERY ROW, AND THAT IS THE POINT RATHER THAN A
// GAP. The three older bare-metal families name an `xim:` payload here; a C
// library for these targets arrives from the DEPENDENCY GRAPH instead
// (`mcpp:c-abi=picolibc`, docs/14). A prebuilt payload would have to ship
// one multilib per row — seven here — and the `libdir` column would have to
// match its layout byte for byte, which is the defect #481 fixed. A source
// package is compiled with the consuming target's own flags, so the ABI
// agreement holds by construction and there is no multilib at all.
//
// THE TIER COLUMN RECORDS WHAT WAS RUN, NOT WHAT WAS REASONED. Measured
// 2026-09-04 under `xim:qemu-arm@9.2.4-1`: each `verified` row below built
// an image that BOOTED on the named machine and printed over semihosting —
// thumbv6m on `microbit`, thumbv7m on `mps2-an385`, thumbv7em-eabihf on
// `mps2-an386`, thumbv8m.main-eabi on `mps2-an505`. The three `preview`
// rows build and link; no emulator run has been recorded for them.
{ "thumbv6m-none-eabi", "verified", "bare","llvm@22.1.8","", true },
{ "thumbv7m-none-eabi", "verified", "bare","llvm@22.1.8","", true },
{ "thumbv7em-none-eabi", "preview", "bare","llvm@22.1.8","", true },
{ "thumbv7em-none-eabihf", "verified", "bare","llvm@22.1.8","", true },
{ "thumbv8m.base-none-eabi","preview", "bare","llvm@22.1.8","", true },
{ "thumbv8m.main-none-eabi","verified", "bare","llvm@22.1.8","", true },
{ "thumbv8m.main-none-eabihf","preview","bare","llvm@22.1.8","", true },
// ── ARMv7-A (Cortex-A, 32-bit) ──────────────────────────────────────────
//
// NOT A SECOND SPELLING OF THE M ROWS. A-profile has a memory management
// unit and a page-table walker; M-profile has an MPU and no page-table
// entry at all. It is the first 32-bit machine in this table on which an
// address space can be described, which is precisely the question the
// openarch layer has never been able to ask of a 32-bit target.
//
// `verified` records what was RUN. Measured 2026-09-04 under
// `xim:qemu-arm@9.2.4-1`: both rows booted on `-M virt -cpu cortex-a15` and
// printed over semihosting. The soft row carries `-mfpu=none` for the
// reason `kThumbSoftExtra` gives, measured again on this architecture
// rather than carried over from M-profile.
//
// `sysroot` is empty, the zero-libc tier, exactly as for the M rows: a C
// library for these targets arrives from the dependency graph.
{ "armv7a-none-eabi", "verified", "bare","llvm@22.1.8","", true },
{ "armv7a-none-eabihf", "verified", "bare","llvm@22.1.8","", true },
// ── The three platforms a package cannot add ────────────────────────────
//
// A package can add a language, a tool, an action, a payload and a
// generated module. IT CANNOT ADD A TRIPLE: identity is these three
// strings and this table is compiled into the binary, so every layer above
// -- the `.apk` step, the `.app` step, the `.html`+`.wasm` step, the
// runner, the signing -- waits on a row here and on nothing else in the
// engine. Registering the rows is what turns each of those into a plugin
// that can be written rather than a plugin that has nowhere to attach.
//
// ALL FOUR ARE `planned`, WHICH IS A REFUSAL AND NOT A GAP. The tier gate
// refuses a planned row with `tier-planned` naming the row, so
// `mcpp build --target aarch64-linux-android` says the vocabulary has this
// target and nothing is wired yet -- rather than `unknown target`, which
// was false, or a build that resolves and produces nothing, which would be
// worse than either. What each row still needs is recorded in
// .agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md
// section 3, and it is a payload in every case, never engine work.
//
// THE PREREQUISITE NOBODY LISTS IS ANSWERED FOR TWO OF THE THREE. mcpp is
// module-first, so a row whose toolchain cannot compile a module interface
// unit would be worse than its absence. Measured 2026-09-11: `import std`
// works on both the NDK's clang 18 and Emscripten's, and neither needs a
// fork or a compiler upgrade -- what both need is the generated module
// surface their vendor chose not to install (133 files, 620 KB, taken from
// the libc++ revision matching `_LIBCPP_VERSION`, which for Emscripten is
// NOT the version its clang reports). Apple's half is not measurable on a
// Linux host and is the one genuinely open question of the three.
// ANDROID IS THE SMALLEST OF THE THREE, and the ranking is the opposite of
// the demand ranking. `aarch64` is already an arch, ELF is already the
// object format, and Linux is already the OS: what was missing is an `env`
// value and a sysroot that points at an NDK. No `pin`, because no cross
// payload exists yet -- `xim:android-ndk` is the row's whole remaining
// cost, and until it lands `[target.<triple>].sysroot` is the escape hatch
// for a machine that has an NDK already.
{ "aarch64-linux-android", "planned", "", "", "", false },
// The emulator's row. Not a convenience: x86_64 is what an Android
// emulator image runs, so a row for the device without one for the
// emulator describes a target nothing in CI can execute.
{ "x86_64-linux-android", "planned", "", "", "", false },
// iOS IS NEXT. `aarch64-macos` is `verified`, so Mach-O, `arm64`, the
// linker and the Apple half of the toolchain model all exist; what is
// missing is an `os` value and the iPhoneOS SDK.
//
// THE SDK IS A LICENCE QUESTION AND NOT A PACKAGING ONE, which is why this
// row carries no `sysroot`. The NDK is Apache-2.0 and Emscripten is MIT,
// both redistributable; the iPhoneOS SDK is neither. The recipe should
// reach for the lowest of three tiers its licence allows -- redistribute,
// fetch from upstream without a mirror, or locate what the machine already
// has -- and say which tier it took, because a consumer reading "locator"
// needs to know that is a licence conclusion rather than an unfinished
// recipe. `msvc@system` is the shape of the third tier and mcpp already
// has it.
//
// The simulator is deliberately not a row. It has its own SDK and produces
// its own object, so folding it in would make two targets share an
// identity -- the mistake `x86_64-windows-musl` was added to undo.
{ "aarch64-ios", "planned", "", "", "", false },
// WEB IS THE OUTLIER, AND IT IS THE ONLY ONE OF THE THREE THAT CHANGES THE
// MODEL RATHER THAN EXTENDING A TABLE. A new arch (`wasm32`), a new os
// (`emscripten`), and -- the sharp part -- a new OBJECT FORMAT, which
// before `ObjectFormat` existed was not a field at all but a derivation
// repeated at every site that needed it. That is why this is
// https://github.com/mcpp-community/mcpp/issues/597 and not a table row.
//
// IT IS NOW ONLY THAT, AND THE STANDARD-LIBRARY HALF IS SIMPLER THAN THIS
// COMMENT FIRST SAID. Measured 2026-09-11 against Emscripten 6.0.9:
// `em++` compiles and links `import std` with NO additional flags and no
// generated surface at all, because the toolchain SHIPS one -- 134 files
// -- and `node app.js` printed the expected output.
//
// The version numbers here were two releases stale, in exactly the
// direction the design record warns about: they said llvm 20.1.7 and
// `_LIBCPP_VERSION 200100` against clang 22.0.0git. Emscripten 6.0.9
// reports `220108` (llvm 22.1.8) and clang 24.0.0git. The rule those
// numbers were supporting is unaffected and is the reason to keep them
// accurate: the surface must match the LIBRARY, never the compiler, and a
// recipe's job is to pin the `_LIBCPP_VERSION` it measured and refuse a
// change. A stale number in a comment becomes a stale number in a
// diagnostic, and then in somebody's install command.
//
// `defaultStatic` is true because wasm has no dynamic loader in the sense
// the other rows mean: an Emscripten link produces one module plus its
// JavaScript, and there is no shared object for a search path to find.
{ "wasm32-emscripten", "planned", "wasm","", "", true },
};
inline std::span<const TargetInfo> known_targets() { return kKnownTargets; }
inline const TargetInfo* find_known_target(const Triple& t) {
auto s = t.str();
for (auto& k : kKnownTargets)
if (k.canonical == s) return &k;
return nullptr;
}
inline bool is_known_target(const Triple& t) { return find_known_target(t) != nullptr; }
// ── Completing a request that declined to name a C library ──────────────────
//
// `parse` FILLS THE ENV SEGMENT LEXICALLY, AND THE TIER GATE USED TO ASK
// ABOUT THE FILLED VALUE RATHER THAN ABOUT THE REQUEST.
//
// The fill is an IDENTITY operation and has to stay exactly as it is: total,
// lexical, and independent of the host (see the note on `Triple::envExplicit`
// and the one beside the fill itself). `x86_64-linux` is the identity
// `x86_64-linux-gnu` on every machine, and a unit test says so.
//
// What it is NOT is an answer to "does mcpp support this". Measured on
// 2026.8.26.1:
//
// $ mcpp build --target aarch64-linux
// error: target 'aarch64-linux-gnu' is registered but not yet supported
// $ mcpp build --target aarch64-linux-musl
// Finished dev [unoptimized + debuginfo] in 0.99s
//
// The question asked was "aarch64, Linux". The question answered was
// "aarch64-linux-GNU", and the error even quotes a triple the user never typed.
// The same fill sends `riscv64-linux` to `riscv64-linux-gnu`, a row that does
// not exist at all, so a registered target family is reported as UNKNOWN.
//
// This function is the request's own completion, applied only where a request
// is read and only when the segment was not written. It consults the vocabulary
// — compile-time data, therefore the same on every host, so target identity
// still does not depend on where the build ran.
//
// RULE ONE MAKES THIS RETIRE ITSELF. When `aarch64-linux-gnu` graduates from
// `planned`, rule one matches first and the completion goes back to the lexical
// answer with nobody editing this function.
struct RequestResolution {
Triple triple; // the identity to use from here on
// The lexical fill was replaced by a row from the vocabulary. For the
// report: the user wrote one thing and mcpp resolved it to another.
bool completedFromVocabulary = false;
std::vector<std::string_view> siblings; // every row sharing (arch, os)
std::vector<std::string_view> supported; // of those, the ones not `planned`
// Several rows are supported and the lexical fill names none of them, so
// there is no basis to pick. No (arch, os) group has this shape today; the
// rule is written down so the first one does not get an invented answer.
bool ambiguous = false;
};
inline RequestResolution resolve_request(const Triple& parsed) {
RequestResolution r;
r.triple = parsed;
// A written segment is a request, not a gap: honour it, including when it
// names a `planned` row (the tier gate is what refuses that, and its
// subject is then genuinely what the user typed).
if (parsed.envExplicit || parsed.arch.empty() || parsed.os.empty())
return r;
const std::string prefix = parsed.arch + "-" + parsed.os;
for (auto& k : kKnownTargets) {
// Exact (macOS rows carry no env) or `arch-os-<env>`. The separator
// check is what keeps a prefix from spanning two different OS names.
const bool exact = k.canonical == prefix;
const bool sub = k.canonical.size() > prefix.size()
&& k.canonical.starts_with(prefix)
&& k.canonical[prefix.size()] == '-';
if (!exact && !sub) continue;
r.siblings.push_back(k.canonical);
if (k.tier != "planned") r.supported.push_back(k.canonical);
}
const std::string lexical = parsed.str();
for (auto s : r.supported)
if (s == lexical) return r; // rule 1: the fill is supported
if (r.supported.size() == 1) { // rule 2: the only supported row
auto only = r.supported.front();
r.triple.env = only.size() > prefix.size()
? std::string(only.substr(prefix.size() + 1))
: std::string{};
// STILL NOT EXPLICIT. `envExplicit` records what the PROJECT asked
// for and feeds the C-library-request check; mcpp choosing a row is not
// the project naming a C library. Setting it here would make
// `check_request` compare mcpp's own answer against itself, and would
// print `aarch64-linux-musl` where the user wrote `aarch64-linux`.
r.completedFromVocabulary = true;
return r;
}
// rule 4 before rule 3: several supported rows and the fill names none.
if (r.supported.size() > 1) r.ambiguous = true;
// rule 3: nothing supported (empty group, or every row `planned`). Keep the
// lexical identity and let the caller diagnose from `siblings`, which is
// what lets the message name a row that actually exists.
return r;
}
// The effective target C library for one build.
//
// SINGLE READ POINT, and it is one because it was two. `prepare_build` derived
// "which sysroot does this target use" in two places — once to compute the
// include/library paths and once to materialize the xim package — and adding a
// project-level override to only one of them would have produced a build that
// installs one C library and compiles against another. This codebase has paid
// for that shape repeatedly (#233/#240/#242/#344).
//
// `override_` is the project's `[target.<triple>].sysroot`, and its optionality
// is load-bearing:
//
// nullptr -> the project said nothing; the target table's column applies
// "xim:..." -> the project named a different C library
// "" -> the project asked for NO C library (the zero-libc tier)
//
// Returning "" for the last case is deliberate: it is what a hosted target row
// already carries, and every consumer of this function already treats empty as
// "add no target sysroot paths". The tier therefore needs no new branch
// anywhere downstream — it reuses the answer the engine already knew how to
// handle.
// A POINTER AND NOT AN `std::optional<std::string>`. The tri-state is the
// same — null means "the project said nothing" — and a pointer parameter
// instantiates nothing in this module's interface. See the note on
// `TargetEntry::sysroot` for what the optional cost when it reached one.
inline std::string effective_sysroot(const Triple& t,
const std::string* override_)
{
if (override_) return *override_;
if (auto* k = find_known_target(t)) return std::string(k->sysroot);
return {};
}
// Closest known-target canonical name for a mistyped `--target` (checked
// against canonical names AND common alias spellings). nullopt when nothing
// is plausibly close.
std::optional<std::string> did_you_mean(std::string_view input);
// Host coordinates as a canonical Triple (linux hosts report env=gnu — the
// user-facing host default, independent of how mcpp itself was linked).
inline Triple host_triple() {
Triple t;
t.arch = std::string(mcpp::platform::host_arch);
t.os = std::string(mcpp::platform::name);
// Derived from the machine rather than written by anyone, so it states no
// request: a host build must not be refused for "contradicting" a C library
// its own triple never asked for.
if (t.os == "linux") t.env = "gnu";
else if (t.os == "windows") t.env = "msvc";
return t;
}
// ── Version pins (single site; §4.6 of the design doc) ───────────────────────
// Every default/convention toolchain version literal lives here. Help and
// error strings format these — never inline a pinned version elsewhere.
// Changing a pin: update this block, then sync docs/20-toolchains.md and the
// README platform table (drawn from kKnownTargets above).
namespace pins {
// First-run auto-install defaults (prepare.cppm), per host platform/arch.
//
// macOS and Windows shared ONE pin until 2026.8.2.1. They must not:
// Apple ships no GCC, so upstream LLVM with bundled libc++ is the only
// self-contained choice there — but on Windows clang targets the MSVC
// ABI (host triple env=msvc) and therefore uses the MSVC STL, which only
// arrives with Visual Studio's "Desktop development with C++" workload.
// A bare Windows box got a default it could never build with, and no
// diagnostic. The Windows pin is now chosen by detection, not by
// sharing macOS's answer.
inline constexpr std::string_view kFirstRunMac = "llvm@20.1.7";
// Windows WITH a usable MSVC (STL + SDK, see msvc::has_usable_msvc()):
// unchanged behavior. The MSVC ABI is what lets a project link vcpkg /
// third-party .lib artifacts, so it stays the answer when it can work.
inline constexpr std::string_view kFirstRunWinMsvc = "llvm@20.1.7";
// Windows WITHOUT one: winlibs GCC targeting PE/GNU. Fully self-contained
// (static libstdc++/libgcc, its own UCRT), zero Visual Studio dependency,
// `import std` works. Must stay equal to the x86_64-windows-gnu row's
// `pin` in kKnownTargets above — test_windows_defaults.cpp enforces it.
inline constexpr std::string_view kFirstRunWinGnu = "gcc@16.1.0";
inline constexpr std::string_view kFirstRunWinGnuTarget = "x86_64-windows-gnu";
inline constexpr std::string_view kFirstRunLinuxX86_64 = "gcc@16.1.0";
inline constexpr std::string_view kFirstRunLinuxOther = "gcc@15.1.0-musl";
// Suggested install spellings used by help / MCPP_NO_AUTO_INSTALL errors.
inline constexpr std::string_view kSuggestLlvm = "llvm 20.1.7";
inline constexpr std::string_view kSuggestGccMusl = "gcc 15.1.0-musl";
inline constexpr std::string_view kSuggestGccMingw = "gcc 16.1.0";
} // namespace pins
// ── Artifact naming conventions ──────────────────────────────────────────────
//
// How a built artifact is NAMED is a property of the TARGET, never of the
// machine doing the build. `mcpp::platform::{exe_suffix,lib_prefix,…}` answer a
// different question — "what does THIS machine call its own binaries" — and
// using them to name build outputs is wrong the moment host != target.
//
// It is a function of (os, env), not of os alone. The trap:
//
// x86_64-windows-gnu → libfoo.a (GNU/mingw convention)
// x86_64-windows-msvc → foo.lib (MSVC convention)
//
// A single `_WIN32` branch cannot express that, which is why building a static
// library with mingw ON a Windows host produces `foo.lib` today — a GNU archive
// wearing an MSVC name. That is a pre-existing defect, unrelated to cross
// compilation.
//
// See .agents/docs/2026-08-03-b3-target-aware-artifact-naming.md.
struct ArtifactNaming {
std::string_view exeSuffix; // "" | ".exe"
std::string_view libPrefix; // "lib" | ""
std::string_view staticLibExt; // ".a" | ".lib"
std::string_view sharedLibExt; // ".so" | ".dylib" | ".dll"
// PE consumers link against an import library, not the .dll itself. mcpp
// does not model import libraries yet, so this currently marks "shared
// libraries are not supported for this target" rather than describing a
// produced artifact. Shared libraries have never been verified end-to-end
// on PE or Mach-O — every shared-library e2e declares `# requires: elf`.
bool sharedNeedsImportLib;
};
// Naming for an explicit target triple. An EMPTY triple means "build for this
// machine", and only then is the host answer the correct one — so the caller
// passes it in rather than this module reaching for mcpp::platform, which keeps
// the decision testable from any host (and keeps this module dependency-free).
inline ArtifactNaming artifact_naming(const Triple& t, const ArtifactNaming& hostNaming) {
if (t.empty()) return hostNaming;
if (t.os == "windows") {
// PE. The static-library convention splits on env, not on os.
const bool msvc = t.is_msvc_env();
return ArtifactNaming{
.exeSuffix = ".exe",
.libPrefix = msvc ? "" : "lib",
.staticLibExt = msvc ? ".lib" : ".a",
.sharedLibExt = ".dll",
.sharedNeedsImportLib = true,
};
}
if (t.os == "macos") {
return ArtifactNaming{
.exeSuffix = "", .libPrefix = "lib",
.staticLibExt = ".a", .sharedLibExt = ".dylib",
.sharedNeedsImportLib = false,
};
}
if (t.os == "linux") {
return ArtifactNaming{
.exeSuffix = "", .libPrefix = "lib",
.staticLibExt = ".a", .sharedLibExt = ".so",
.sharedNeedsImportLib = false,
};
}
// Outside the triple language: fall back to the host answer rather than
// guessing. A wrong guess here silently misnames every artifact.
return hostNaming;
}
} // namespace mcpp::toolchain::triple
namespace mcpp::toolchain::triple {
namespace {
bool starts_with(std::string_view s, std::string_view p) {
return s.size() >= p.size() && s.substr(0, p.size()) == p;
}
std::string normalize_arch(std::string_view a) {
if (a == "arm64") return "aarch64"; // Apple/xlings spelling → GNU
if (a == "amd64") return "x86_64";
return std::string(a);
}
// Levenshtein distance (small inputs only).
std::size_t edit_distance(std::string_view a, std::string_view b) {
std::vector<std::size_t> prev(b.size() + 1), cur(b.size() + 1);
for (std::size_t j = 0; j <= b.size(); ++j) prev[j] = j;
for (std::size_t i = 1; i <= a.size(); ++i) {
cur[0] = i;
for (std::size_t j = 1; j <= b.size(); ++j) {
std::size_t sub = prev[j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1);
cur[j] = std::min({ prev[j] + 1, cur[j - 1] + 1, sub });
}
std::swap(prev, cur);
}
return prev[b.size()];
}
} // namespace
std::optional<Triple> parse(std::string_view s) {
if (s.empty()) return std::nullopt;
// Split on '-'.
std::vector<std::string_view> tok;
for (std::size_t b = 0; b <= s.size();) {
auto d = s.find('-', b);
if (d == std::string_view::npos) { tok.push_back(s.substr(b)); break; }
tok.push_back(s.substr(b, d - b));
b = d + 1;
}
if (tok.size() < 2 || tok[0].empty()) return std::nullopt;
Triple t;
t.arch = normalize_arch(tok[0]);
// `none` is BOTH a vendor segment and an OS segment, and which one it is
// depends on the rest of the triple, not on its position:
//
// riscv64-none-elf -> vendor absent, OS = none (bare metal)
// x86_64-none-linux-gnu -> vendor = none, OS = linux (hosted)
//
// So it cannot be decided inside the single left-to-right pass below — by
// the time `none` is seen, `linux` has not been read yet. Pre-scan for a
// real OS token first; `none` is the OS only when there is no other
// candidate. Getting this backwards is not a parse error, it is a SILENT
// one: the triple would parse as hosted and the build would produce a host
// binary while reporting success.
bool hasRealOs = false;
for (std::size_t i = 1; i < tok.size(); ++i) {
std::string_view k = tok[i];
if (k == "linux" || k == "windows" || k == "apple"
|| starts_with(k, "darwin") || starts_with(k, "macosx")
|| starts_with(k, "macos") || starts_with(k, "mingw")) {
hasRealOs = true;
break;
}
}
bool sawOs = false;
for (std::size_t i = 1; i < tok.size(); ++i) {
std::string_view k = tok[i];
if (k.empty()) return std::nullopt;
if (k == "none" && !hasRealOs) { t.os = "none"; sawOs = true; continue; }
// Vendor segments carry no information — skip. ("w64" is mingw-w64's
// vendor; "apple" implies macOS when no OS token follows.)
if (k == "unknown" || k == "pc" || k == "w64" || k == "none") continue;
if (k == "apple") { if (!sawOs) { t.os = "macos"; sawOs = true; } continue; }
if (k == "linux") { t.os = "linux"; sawOs = true; continue; }
if (k == "windows") { t.os = "windows"; sawOs = true; continue; }
if (starts_with(k, "darwin")
|| starts_with(k, "macosx")
|| starts_with(k, "macos")) { t.os = "macos"; sawOs = true; t.env.clear(); continue; }
// "mingw32" is the GNU os segment for ALL MinGW targets (64-bit
// included — historical residue); it means windows + gnu env.
if (starts_with(k, "mingw")) { t.os = "windows"; sawOs = true; t.env = "gnu"; t.envExplicit = true; continue; }
// APPLE'S SECOND OS. `starts_with` for the same reason the macOS
// branch above uses it: an effective triple carries the deployment
// target on this segment (`arm64-apple-ios17.0`). The simulator is a
// different row and is deliberately not spelled here -- it has a
// different SDK and a different object, so folding it into this one
// would make two targets share an identity.
if (starts_with(k, "iphoneos") || starts_with(k, "ios"))
{ t.os = "ios"; sawOs = true; t.env.clear(); continue; }
// EMSCRIPTEN IS AN OS SEGMENT, NOT AN ENV. It names the platform layer
// a wasm module is compiled against -- its POSIX emulation, its
// filesystem shim, its `main` loop -- which is the same kind of thing
// `linux` names and not the same kind of thing `musl` names.
if (starts_with(k, "emscripten")) { t.os = "emscripten"; sawOs = true; t.env.clear(); continue; }
// Bare-metal object-format / ABI segments. Only meaningful with
// os=none: `riscv64-none-elf`, `arm-none-eabi`, `arm-none-eabihf`.
// Gated on the OS so a hosted triple cannot pick them up by accident.
if (t.os == "none") {
if (k == "elf") { t.env = "elf"; t.envExplicit = true; continue; }
if (k == "eabihf") { t.env = "eabihf"; t.envExplicit = true; continue; }
if (k == "eabi") { t.env = "eabi"; t.envExplicit = true; continue; }
}
if (t.os != "macos") {
// ANDROID IS AN ENV SEGMENT ON A LINUX OS, and that placement is
// the whole of the modelling decision. The kernel IS Linux, so
// every Linux-shaped answer in the tree -- ELF, the `unix` family,
// `nasm -f elf64` -- is already right; what differs is the C
// library (bionic), the loader path and the SDK. An `os = "android"`
// would have made all three of those wrong by default and required
// a new answer at each site.
//
// `androideabi` is the 32-bit ARM spelling and resolves to the same
// env: the EABI half is the ARM calling convention, which `armv7a`
// already carries in the arch segment.
if (k == "android" || starts_with(k, "androideabi")) {
t.env = "android"; t.envExplicit = true; continue;
}
if (k == "musl" || starts_with(k, "musleabi")) { t.env = "musl"; t.envExplicit = true; continue; }
if (k == "gnu" || starts_with(k, "gnueabi")) { t.env = "gnu"; t.envExplicit = true; continue; }
// starts_with: clang effective triples can carry a version suffix
// on the env segment ("…-windows-msvc19.44.35211").
if (starts_with(k, "msvc")) { t.env = "msvc"; t.envExplicit = true; continue; }
}
// Unrecognized segment (wasi, …): not in mcpp's target language —
// treat as unparseable rather than guessing.
return std::nullopt;
}
if (!sawOs) return std::nullopt;
// macOS carries no env segment at all, so nothing was declined there. iOS
// and Emscripten are the same shape: the platform layer is the whole of the
// identity past the arch, and there is no C-library axis to decline.