-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathtest_distribution.cpp
More file actions
936 lines (858 loc) · 44.9 KB
/
Copy pathtest_distribution.cpp
File metadata and controls
936 lines (858 loc) · 44.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
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
// The C++ runtime distribution contract table (issue #336).
//
// These tests exist because the thing they cover used to be five independent
// derivations of one decision, and the bug was that a new rule landed in some
// of them and not the others. So the assertions are deliberately about the
// TABLE's totality and about each cell's identity — not about "the flags look
// roughly right".
#include <gtest/gtest.h>
import std;
import mcpp.build.distribution;
namespace dist = mcpp::build::dist;
namespace {
dist::MechanismInput macos_input() {
dist::MechanismInput in;
in.format = dist::Format::MachO;
in.stdlibId = "libc++";
in.macosFloor = true;
in.libcxxArchive = "/tc/lib/libc++.a";
in.libcxxAbiArchive = "/tc/lib/libc++abi.a";
in.streamInitSymbolPresent = true;
return in;
}
dist::MechanismInput linux_gcc_input() {
dist::MechanismInput in;
in.format = dist::Format::Elf;
in.stdlibId = "libstdc++";
return in;
}
} // namespace
// ---------------------------------------------------------------------------
// The regression this whole change exists for: `static_stdlib = false` (now
// `cxx_runtime = "host-coupled"`) was silently ignored for test binaries from
// 0.0.86 on, while the docs kept promising the opt-out. The table cannot
// reproduce that bug because the role is an INPUT to one function rather than
// a switch that picks between two independently-computed strings.
TEST(Distribution, HostCoupledReachesTestBinariesOnMacos) {
auto in = macos_input();
in.requested = dist::Contract::HostCoupled;
for (auto role : {dist::Role::Distributable, dist::Role::Test}) {
in.role = role;
auto m = dist::resolve(in);
EXPECT_EQ(m.effective, dist::Contract::HostCoupled) << dist::to_string(role);
EXPECT_EQ(m.unitFlags, " -lc++") << dist::to_string(role);
EXPECT_FALSE(m.degraded) << dist::to_string(role);
// No static libc++ means no Mach-O initializer-ordering problem.
EXPECT_FALSE(m.streamInitShim) << dist::to_string(role);
}
}
// The other half of #336: by default a test binary keeps the SAME
// self-contained runtime as a shipped one. Flipping this default would
// re-open #202 (system libc++ dylib against toolchain libc++ headers →
// undefined __hash_memory on libc++ 22), so it is asserted, not assumed.
TEST(Distribution, TestsDefaultToSelfContained) {
for (auto fmt : {dist::Format::Elf, dist::Format::MachO, dist::Format::Pe}) {
EXPECT_EQ(dist::default_contract(dist::Role::Test, fmt),
dist::Contract::SelfContained);
EXPECT_EQ(dist::default_contract(dist::Role::Distributable, fmt),
dist::Contract::SelfContained);
}
auto in = macos_input();
in.role = dist::Role::Test;
in.requested = dist::default_contract(dist::Role::Test, dist::Format::MachO);
auto m = dist::resolve(in);
EXPECT_NE(m.unitFlags.find("-load_hidden"), std::string::npos);
EXPECT_TRUE(m.streamInitShim);
}
// -load_hidden, not a plain by-path link: the visibility is the load-bearing
// half (PR #117 — default-visibility statics get unified with the system
// libc++ from dyld's shared cache and ostream<<int crosses copies).
TEST(Distribution, MacosSelfContainedUsesHiddenArchives) {
auto in = macos_input();
auto m = dist::resolve(in);
EXPECT_EQ(m.unitFlags,
" -nostdlib++ -Wl,-load_hidden,/tc/lib/libc++.a"
" -Wl,-load_hidden,/tc/lib/libc++abi.a");
EXPECT_FALSE(m.degraded);
EXPECT_TRUE(m.diagnostic.empty());
}
// An archive is linked, never run: it embeds no runtime and imposes none.
TEST(Distribution, IntermediateCarriesNoContractFlags) {
for (auto in : {macos_input(), linux_gcc_input()}) {
in.role = dist::Role::Intermediate;
in.requested = dist::Contract::SelfContained;
EXPECT_TRUE(dist::resolve(in).unitFlags.empty());
}
}
// ---------------------------------------------------------------------------
// INV-4: a contract that cannot be honored is REPORTED. The pre-#336 code
// degraded silently in exactly these spots — a toolchain with no libc++.a fell
// back to `-lc++` and said nothing, so an artifact documented as portable to
// macOS 14 was quietly pinned to the build machine.
TEST(Distribution, MissingArchivesDegradeLoudly) {
auto in = macos_input();
in.libcxxArchive.clear();
in.libcxxAbiArchive.clear();
auto m = dist::resolve(in);
EXPECT_EQ(m.effective, dist::Contract::HostCoupled);
EXPECT_TRUE(m.degraded);
EXPECT_FALSE(m.diagnostic.empty());
EXPECT_EQ(m.unitFlags, " -lc++");
}
TEST(Distribution, MissingMacosFloorDegradesLoudly) {
auto in = macos_input();
in.macosFloor = false;
auto m = dist::resolve(in);
EXPECT_EQ(m.effective, dist::Contract::HostCoupled);
EXPECT_TRUE(m.degraded);
EXPECT_FALSE(m.diagnostic.empty());
}
// macOS has no toolchain-coupled form: LLVM's libc++abi/libunwind dylibs
// upward-link /usr/lib/libc++, so asking for one loads a SECOND libc++ into
// the process (#202 forensics). Refused with an explanation rather than
// half-honored.
TEST(Distribution, MacosToolchainCoupledIsRefusedNotFaked) {
auto in = macos_input();
in.requested = dist::Contract::ToolchainCoupled;
auto m = dist::resolve(in);
EXPECT_EQ(m.effective, dist::Contract::SelfContained);
EXPECT_TRUE(m.degraded);
EXPECT_NE(m.diagnostic.find("toolchain-coupled"), std::string::npos);
}
// The silent no-op this model was built to make impossible: on a Linux/libc++
// toolchain, `static_stdlib = true` produced NO flag, NO warning and a
// toolchain-coupled binary — while the manifest, the docs and `--version` all
// said the artifact was self-contained.
TEST(Distribution, LinuxLibcxxSelfContainedIsRealOrLoud) {
dist::MechanismInput in;
in.format = dist::Format::Elf;
in.stdlibId = "libc++";
in.requested = dist::Contract::SelfContained;
// No archives on this toolchain → say so, and name what you got instead.
auto bare = dist::resolve(in);
EXPECT_EQ(bare.effective, dist::Contract::ToolchainCoupled);
EXPECT_TRUE(bare.degraded);
EXPECT_FALSE(bare.diagnostic.empty());
// Archives present → a real self-contained link. libunwind.a is part of
// the mechanism, not a bonus: without it the binary still pulls
// libunwind.so.1 and is not self-contained (verified against a real
// llvm@22.1.8 payload).
in.libcxxArchive = "/tc/libc++.a";
in.libcxxAbiArchive = "/tc/libc++abi.a";
in.libunwindArchive = "/tc/libunwind.a";
auto full = dist::resolve(in);
EXPECT_EQ(full.effective, dist::Contract::SelfContained);
EXPECT_FALSE(full.degraded);
EXPECT_EQ(full.unitFlags,
" -nostdlib++ /tc/libc++.a /tc/libc++abi.a /tc/libunwind.a");
// Mach-O only — ELF sorts .init_array by priority, so the ordering bug
// does not exist here (confirmed by running the repro on Linux).
EXPECT_FALSE(full.streamInitShim);
in.libunwindArchive.clear();
auto noUnwind = dist::resolve(in);
EXPECT_TRUE(noUnwind.degraded);
EXPECT_NE(noUnwind.diagnostic.find("libunwind"), std::string::npos);
}
// ---------------------------------------------------------------------------
// Byte-level parity with the pre-#336 emission on the platforms that were
// already correct. These strings are the whole reason the change is safe to
// ship: the Linux/Windows link is the same link, only assembled in one place.
TEST(Distribution, LinuxGccParity) {
auto in = linux_gcc_input();
EXPECT_EQ(dist::resolve(in).unitFlags, " -static-libstdc++");
in.requested = dist::Contract::HostCoupled;
EXPECT_EQ(dist::resolve(in).unitFlags, "");
EXPECT_FALSE(dist::resolve(in).degraded);
// ELF has no distinct emission for toolchain-coupled; the difference is
// the rpath the link already carries, which this contract does not own.
in.requested = dist::Contract::ToolchainCoupled;
EXPECT_EQ(dist::resolve(in).unitFlags, "");
EXPECT_FALSE(dist::resolve(in).degraded);
}
TEST(Distribution, MingwParity) {
dist::MechanismInput in;
in.format = dist::Format::Pe;
in.stdlibId = "libstdc++";
in.mingw = true;
// Whole-link -static is what "self-contained" means on MinGW: the
// piecemeal -static-libstdc++ recipe still leaves libwinpthread-1.dll.
EXPECT_EQ(dist::resolve(in).unitFlags, " -static -static-libstdc++");
in.hostIsWindows = true;
EXPECT_EQ(dist::resolve(in).unitFlags,
" -static -static-libstdc++ -static-libgcc");
// The libc axis is independent: `linkage = "static"` keeps -static even
// when the C++ runtime is host-coupled.
in.hostIsWindows = false;
in.requested = dist::Contract::HostCoupled;
in.fullStaticLibc = true;
EXPECT_EQ(dist::resolve(in).unitFlags, " -static");
}
// MSVC's self-contained form IS the /MT runtime, and mcpp emits it — the
// switch is `msvcStaticCrt`, derived once by `msvc_wants_static_crt` from the
// two manifest keys that mean the same physical thing on this ABI.
//
// What the table must get right is that the switch is whole-PROJECT: cl bakes
// _MSVC_MT/_MSVC_MD into the one std module a project builds, so a per-role
// request that disagrees cannot be honoured and must say so.
TEST(Distribution, MsvcCrtModelIsWholeProjectAndReportedAsSuch) {
dist::MechanismInput in;
in.format = dist::Format::Pe;
in.stdlibId = "msvc";
in.explicitRequest = true;
// Project compiled /MT: self-contained is DELIVERED, not degraded.
in.requested = dist::Contract::SelfContained;
in.msvcStaticCrt = true;
auto served = dist::resolve(in);
EXPECT_EQ(served.effective, dist::Contract::SelfContained);
EXPECT_FALSE(served.degraded);
EXPECT_TRUE(served.diagnostic.empty());
// The CRT model is a COMPILE flag on every TU, never a link-line addition.
EXPECT_TRUE(served.unitFlags.empty());
// Project compiled /MD, one role asking for self-contained: refused, and
// the message has to name the whole-project constraint rather than claim
// the feature is missing.
in.msvcStaticCrt = false;
auto refused = dist::resolve(in);
EXPECT_EQ(refused.effective, dist::Contract::HostCoupled);
EXPECT_TRUE(refused.degraded);
EXPECT_NE(refused.diagnostic.find("whole-project"), std::string::npos)
<< refused.diagnostic;
EXPECT_NE(refused.diagnostic.find("cxx_runtime"), std::string::npos)
<< refused.diagnostic;
EXPECT_TRUE(refused.unitFlags.empty());
// `linkage = "static"` is the same switch seen from the libc axis.
in.fullStaticLibc = true;
in.msvcStaticCrt = true; // as msvc_wants_static_crt would report it
EXPECT_EQ(dist::resolve(in).effective, dist::Contract::SelfContained);
in.fullStaticLibc = false;
in.msvcStaticCrt = false;
in.requested = dist::Contract::HostCoupled;
EXPECT_FALSE(dist::resolve(in).degraded);
// ...and the DEFAULT must be quiet. Most roles default to the
// self-contained contract, so a project that never mentioned the CRT gets
// /MD and no complaint: a diagnostic is for a broken promise, not for a
// default nobody asked about.
in.requested = dist::Contract::SelfContained;
in.explicitRequest = false;
auto quiet = dist::resolve(in);
EXPECT_FALSE(quiet.degraded);
EXPECT_TRUE(quiet.diagnostic.empty());
EXPECT_EQ(quiet.effective, dist::Contract::HostCoupled);
}
// `toolchain-coupled` on the MSVC runtime used to be a flat refusal, and the
// sentence it refused with conflated two different DLLs:
//
// ucrtbase.dll an OS component since Win10 — the refusal was right
// vcruntime140.dll the TOOLSET's own, sitting in VC\Redist\MSVC\… inside
// every toolset mcpp installs — the refusal was wrong
//
// The second one is exactly the relationship gcc has to libstdc++.so, so it
// takes the same contract. What differs is the MECHANISM: PE has no rpath, so
// the DLL travels by being copied beside the artifact.
TEST(Distribution, MsvcToolchainCoupledStagesTheToolsetCrt) {
dist::MechanismInput in;
in.format = dist::Format::Pe;
in.stdlibId = "msvc";
in.explicitRequest = true;
in.requested = dist::Contract::ToolchainCoupled;
// /MD: the artifact HAS a vcruntime140.dll dependency, so the contract is
// deliverable — and delivering it means staging files, not adding flags.
in.msvcStaticCrt = false;
auto coupled = dist::resolve(in);
EXPECT_EQ(coupled.effective, dist::Contract::ToolchainCoupled);
EXPECT_FALSE(coupled.degraded);
EXPECT_TRUE(coupled.diagnostic.empty());
EXPECT_TRUE(coupled.deployToolchainRuntime);
// The CRT model is a compile flag on every TU; nothing goes on the link line.
EXPECT_TRUE(coupled.unitFlags.empty());
// /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. The message has to say which one won.
in.msvcStaticCrt = true;
auto contradiction = dist::resolve(in);
EXPECT_EQ(contradiction.effective, dist::Contract::SelfContained);
EXPECT_TRUE(contradiction.degraded);
EXPECT_FALSE(contradiction.deployToolchainRuntime)
<< "a /MT build has no CRT DLL dependency; staging one is dead weight";
EXPECT_NE(contradiction.diagnostic.find("/MT"), std::string::npos)
<< contradiction.diagnostic;
EXPECT_NE(contradiction.diagnostic.find("self-contained"), std::string::npos)
<< contradiction.diagnostic;
}
// Nothing but PE+MSVC+toolchain-coupled may ask for files to be staged. The
// flag reaches a copy step, so a stray `true` puts DLLs in an output tree on a
// platform that has no such thing.
TEST(Distribution, NothingElseAsksForStagedRuntimeFiles) {
const dist::Contract contracts[] = {dist::Contract::SelfContained,
dist::Contract::ToolchainCoupled,
dist::Contract::HostCoupled};
const dist::Format formats[] = {dist::Format::Elf, dist::Format::MachO,
dist::Format::Pe};
const std::string_view stdlibs[] = {"libstdc++", "libc++", "msvc", "surprise"};
for (auto fmt : formats)
for (auto sl : stdlibs)
for (auto c : contracts)
for (bool mt : {false, true})
for (bool explicitly : {false, true}) {
dist::MechanismInput in;
in.format = fmt;
in.stdlibId = sl;
in.requested = c;
in.msvcStaticCrt = mt;
in.explicitRequest = explicitly;
in.mingw = (fmt == dist::Format::Pe
&& sl == "libstdc++");
auto m = dist::resolve(in);
if (!m.deployToolchainRuntime) continue;
EXPECT_EQ(fmt, dist::Format::Pe);
EXPECT_NE(sl, std::string_view("libstdc++"));
EXPECT_EQ(c, dist::Contract::ToolchainCoupled);
EXPECT_FALSE(mt);
// Staging files is a promise KEPT. A degraded cell did
// not deliver the contract, so it must not act as if
// it had.
EXPECT_FALSE(m.degraded);
EXPECT_EQ(m.effective, dist::Contract::ToolchainCoupled);
}
}
// ---------------------------------------------------------------------------
// INV-1, stated as a property rather than a list: the table is TOTAL, and
// every cell that does not deliver what was asked explains itself. A future
// stdlib/format/contract combination that forgets one or the other fails here.
TEST(Distribution, TableIsTotalAndEveryDowngradeExplainsItself) {
const dist::Contract contracts[] = {dist::Contract::SelfContained,
dist::Contract::ToolchainCoupled,
dist::Contract::HostCoupled};
const dist::Format formats[] = {dist::Format::Elf, dist::Format::MachO,
dist::Format::Pe};
const std::string_view stdlibs[] = {"libstdc++", "libc++", "msvc", "surprise"};
const dist::Role roles[] = {dist::Role::Distributable, dist::Role::Test,
dist::Role::Intermediate};
for (auto c : contracts)
for (auto fmt : formats)
for (auto sl : stdlibs)
for (auto role : roles)
for (bool archives : {false, true}) {
dist::MechanismInput in;
in.requested = c;
in.format = fmt;
in.stdlibId = sl;
in.role = role;
in.macosFloor = true;
in.explicitRequest = true; // the question is "what if you ASK for it"
if (archives) {
in.libcxxArchive = "/a.a";
in.libcxxAbiArchive = "/b.a";
in.libunwindArchive = "/c.a";
in.streamInitSymbolPresent = true;
}
auto m = dist::resolve(in);
auto where = std::format("contract={} format={} stdlib={} role={} archives={}",
dist::to_string(c), static_cast<int>(fmt), sl,
dist::to_string(role), archives);
// Totality: every cell answers with a contract it actually delivered.
EXPECT_TRUE(m.effective == dist::Contract::SelfContained
|| m.effective == dist::Contract::ToolchainCoupled
|| m.effective == dist::Contract::HostCoupled) << where;
// Honesty: a downgrade is never silent.
if (m.degraded) EXPECT_FALSE(m.diagnostic.empty()) << where;
if (m.effective != c && role != dist::Role::Intermediate)
EXPECT_TRUE(m.degraded) << where;
// The ordering shim is a Mach-O static-libc++ concern and nothing else.
if (m.streamInitShim) {
EXPECT_EQ(fmt, dist::Format::MachO) << where;
EXPECT_EQ(m.effective, dist::Contract::SelfContained) << where;
}
}
}
// The generated shim is load-bearing in three specific ways; a well-meaning
// edit that drops any of them turns it into a silent no-op (or worse, an
// unconditional link failure on a toolchain that spells the symbol
// differently). See issue #336 for the disassembly this rests on.
TEST(Distribution, StreamInitShimKeepsItsThreeLoadBearingProperties) {
auto src = std::string(dist::stream_init_shim_source());
// 1. weak_import — Mach-O's weak-UNDEFINED form. Plain `weak` is not it;
// the first CI round proved that by failing every macOS link.
EXPECT_NE(src.find("__attribute__((weak_import))"), std::string::npos);
// 2. ios_base::Init::Init, NOT DoIOSInit::DoIOSInit — the former is
// guarded by __cxa_guard, so libc++'s own initializer later becomes a
// no-op instead of placement-new'ing over live streams.
// ...spelled with TWO leading underscores: an __asm__ label is used
// verbatim, so Mach-O's global `_` prefix has to be written out.
EXPECT_NE(src.find("\"__ZNSt3__18ios_base4InitC1Ev\""), std::string::npos);
EXPECT_EQ(src.find("DoIOSInit"), std::string::npos);
// 3. a constructor, and it must be guarded by the weak null check.
EXPECT_NE(src.find("__attribute__((constructor))"), std::string::npos);
EXPECT_NE(src.find("if (mcpp_libcxx_ios_init)"), std::string::npos);
// C, not C++: no standard library, no module flags, no ABI of its own.
EXPECT_EQ(src.find("#include"), std::string::npos);
}
// The shim binds a libc++ INTERNAL symbol. If that symbol is not in the
// archive, mcpp must NOT generate the reference — an undefined symbol fails
// the link outright (ld64.lld does not treat a weak declaration as an
// optional undefined). The absence is reported instead, because the startup
// hazard is still there.
TEST(Distribution, ShimIsNotGeneratedWhenTheSymbolIsAbsent) {
auto in = macos_input();
in.streamInitSymbolPresent = false;
auto m = dist::resolve(in);
EXPECT_FALSE(m.streamInitShim);
EXPECT_FALSE(m.diagnostic.empty());
// The contract itself is still honored — only the ordering aid is gone.
EXPECT_EQ(m.effective, dist::Contract::SelfContained);
EXPECT_NE(m.unitFlags.find("-load_hidden"), std::string::npos);
}
// ---------------------------------------------------------------------------
// Shared libraries.
//
// A .so is not a small executable: it is loaded INTO a process that already
// has a C++ runtime. On ELF that matters because there is ONE global symbol
// namespace and the first definition loaded wins — a .so that statically
// embedded libstdc++ exports ~3000 std symbols unversioned, the linker
// resolves the EXECUTABLE's std references against it (`-lfoo` precedes the
// driver's `-lstdc++`, so the archive member is never pulled), and the
// executable's own `-static-libstdc++` becomes a no-op. Swap that .so for
// another build of the same SONAME and `std::runtime_error::what()` is gone.
//
// The whole table is asserted cell by cell rather than "the ELF case", because
// the reason the other two formats keep the old answer is a real argument
// about each of them and a regression there would be silent.
TEST(Distribution, SharedLibraryDefaultIsFormatSpecific) {
EXPECT_EQ(dist::default_contract(dist::Role::SharedLibrary, dist::Format::Elf),
dist::Contract::ToolchainCoupled);
// Mach-O: self-contained ALREADY means hidden (-load_hidden), so dyld
// cannot unify the symbols; and toolchain-coupled is a documented dead end
// there (#202). PE: no global namespace at all, imports resolve per-DLL.
EXPECT_EQ(dist::default_contract(dist::Role::SharedLibrary, dist::Format::MachO),
dist::Contract::SelfContained);
EXPECT_EQ(dist::default_contract(dist::Role::SharedLibrary, dist::Format::Pe),
dist::Contract::SelfContained);
}
// The ELF mechanism for the new default: nothing. The driver links
// libstdc++.so and the toolchain's lib directory is already an -L and an rpath
// entry on the line. "No flag" has to be asserted or a future edit that adds
// one back would look like an improvement.
TEST(Distribution, SharedLibraryOnElfEmbedsNothing) {
auto in = linux_gcc_input();
in.role = dist::Role::SharedLibrary;
in.requested = dist::default_contract(dist::Role::SharedLibrary, dist::Format::Elf);
auto m = dist::resolve(in);
EXPECT_EQ(m.effective, dist::Contract::ToolchainCoupled);
EXPECT_EQ(m.unitFlags, "");
EXPECT_FALSE(m.degraded);
EXPECT_TRUE(m.diagnostic.empty());
}
// The escape hatch stays usable — and is guarded. Asking for a self-contained
// .so is legitimate; letting it export the embedded runtime is not.
TEST(Distribution, ExplicitSelfContainedSharedLibraryHidesTheEmbeddedRuntime) {
auto in = linux_gcc_input();
in.role = dist::Role::SharedLibrary;
in.requested = dist::Contract::SelfContained;
in.explicitRequest = true;
auto m = dist::resolve(in);
EXPECT_EQ(m.effective, dist::Contract::SelfContained);
EXPECT_NE(m.unitFlags.find("-static-libstdc++"), std::string::npos) << m.unitFlags;
EXPECT_NE(m.unitFlags.find("-Wl,--exclude-libs,libstdc++.a"), std::string::npos)
<< m.unitFlags;
// No diagnostic: this is honored exactly as asked, not degraded.
EXPECT_FALSE(m.degraded);
}
// The guard is for shared libraries only. An executable's static libstdc++ is
// already local (ld exports only what a loaded object references and mcpp
// passes no -rdynamic), so hiding it would be noise — and `--exclude-libs` on
// an executable link is a flag whose absence is part of the contract.
TEST(Distribution, ExecutablesDoNotGetTheExcludeLibsGuard) {
auto in = linux_gcc_input();
in.role = dist::Role::Distributable;
in.requested = dist::Contract::SelfContained;
auto m = dist::resolve(in);
EXPECT_EQ(m.unitFlags, " -static-libstdc++");
}
// Same rule on the libc++/ELF path: the archives are linked by PATH, but
// --exclude-libs matches the archive BASENAME, so the names are spelled out.
TEST(Distribution, LibcxxSelfContainedSharedLibraryHidesItsArchives) {
dist::MechanismInput in;
in.format = dist::Format::Elf;
in.stdlibId = "libc++";
in.role = dist::Role::SharedLibrary;
in.requested = dist::Contract::SelfContained;
in.explicitRequest = true;
in.libcxxArchive = "/tc/lib/libc++.a";
in.libcxxAbiArchive = "/tc/lib/libc++abi.a";
in.libunwindArchive = "/tc/lib/libunwind.a";
auto m = dist::resolve(in);
for (auto needle : {"-Wl,--exclude-libs,libc++.a",
"-Wl,--exclude-libs,libc++abi.a",
"-Wl,--exclude-libs,libunwind.a"})
EXPECT_NE(m.unitFlags.find(needle), std::string::npos) << needle
<< " / " << m.unitFlags;
}
// An archive embeds no runtime, so the role returns before any mechanism runs.
// Adding a fourth role must not have perturbed that early exit.
TEST(Distribution, IntermediateStillCarriesNoMechanism) {
auto in = linux_gcc_input();
in.role = dist::Role::Intermediate;
in.requested = dist::Contract::SelfContained;
auto m = dist::resolve(in);
EXPECT_EQ(m.unitFlags, "");
EXPECT_FALSE(m.degraded);
}
// `ldStdlibByRole` indexes by the enum value. If a role is ever inserted
// rather than appended, every stored contract silently re-maps.
TEST(Distribution, RoleCountCoversEveryRole) {
EXPECT_EQ(static_cast<std::size_t>(dist::Role::SharedLibrary) + 1,
dist::kRoleCount);
for (auto r : {dist::Role::Distributable, dist::Role::Test,
dist::Role::Intermediate, dist::Role::SharedLibrary}) {
EXPECT_LT(static_cast<std::size_t>(r), dist::kRoleCount);
EXPECT_FALSE(dist::to_string(r).empty());
}
}
// ── The two short-circuits: when there is no C++ runtime to distribute WITH ──
//
// Every cell of the table above answers "how does this artefact carry its C++
// runtime", and all of the answers name a runtime to LINK — the system's, the
// toolchain's, or a static form of one. Two situations make all of them wrong
// rather than merely unnecessary, and in both the archives the table would
// reach for belong to the HOST.
TEST(Distribution, FreestandingCarriesNoRuntimeToDistribute) {
dist::MechanismInput in;
in.format = dist::Format::Elf;
in.stdlibId = "libc++";
in.freestanding = true;
in.requested = dist::Contract::SelfContained;
in.role = dist::Role::Distributable;
// Deliberately present: the point is that they are NOT reached.
in.libcxxArchive = "/tc/lib/libc++.a";
in.libcxxAbiArchive = "/tc/lib/libc++abi.a";
auto m = dist::resolve(in);
EXPECT_EQ(m.effective, dist::Contract::SelfContained);
EXPECT_EQ(m.unitFlags, " -nostdlib++");
EXPECT_EQ(m.unitFlags.find("libc++.a"), std::string::npos);
EXPECT_FALSE(m.degraded);
}
// The hosted form of the same fact. A package in the graph has compiled a
// C++ runtime FOR THIS TARGET and its objects are already on the link line, so
// there is no library to name and nothing to look for. Measured before this
// existed: `ld64.lld: error: library not found for -lc++`.
TEST(Distribution, GraphSuppliedRuntimeCarriesNoLibraryToName) {
auto in = macos_input();
in.graphCxxRuntime = true;
in.requested = dist::Contract::SelfContained;
in.role = dist::Role::Distributable;
auto m = dist::resolve(in);
EXPECT_EQ(m.effective, dist::Contract::SelfContained);
EXPECT_EQ(m.unitFlags, " -nostdlib++");
EXPECT_EQ(m.unitFlags.find("-lc++"), std::string::npos);
EXPECT_EQ(m.unitFlags.find("load_hidden"), std::string::npos);
}
// The same on every format, because the fact is about the graph and not about
// the object format.
TEST(Distribution, GraphSuppliedRuntimeIsFormatIndependent) {
for (auto fmt : {dist::Format::Elf, dist::Format::MachO, dist::Format::Pe}) {
dist::MechanismInput in;
in.format = fmt;
in.stdlibId = "libc++";
in.graphCxxRuntime = true;
in.requested = dist::Contract::SelfContained;
in.role = dist::Role::Distributable;
auto m = dist::resolve(in);
EXPECT_EQ(m.unitFlags, " -nostdlib++") ;
EXPECT_EQ(m.effective, dist::Contract::SelfContained);
}
}
// And it does not depend on the contract the project asked for: a
// host-coupled request cannot be honoured by naming the system's runtime when
// the graph's is already inside the artefact.
TEST(Distribution, GraphSuppliedRuntimeIgnoresTheRequestedContract) {
for (auto c : {dist::Contract::SelfContained,
dist::Contract::ToolchainCoupled,
dist::Contract::HostCoupled}) {
auto in = macos_input();
in.graphCxxRuntime = true;
in.requested = c;
in.role = dist::Role::Distributable;
auto m = dist::resolve(in);
EXPECT_EQ(m.unitFlags, " -nostdlib++");
}
}
// ── format_for: which format a target produces ──────────────────────────────
//
// This was a lambda inside a fifteen-hundred-line function and therefore had
// no test, and what it got wrong was found by running three hosts against three
// targets. The assertions below are the ones that would have found it in a
// second: the canonical spellings mcpp itself uses contain neither `apple` nor
// `darwin`, and the answer must not depend on the fallback.
TEST(Distribution, FormatIsTakenFromTheTargetAndNotTheFallback) {
// Every fallback, so that a target the vocabulary knows can never be
// decided by the machine doing the building.
for (auto fb : {dist::Format::Elf, dist::Format::MachO, dist::Format::Pe}) {
EXPECT_EQ(dist::format_for("aarch64-macos", fb), dist::Format::MachO);
EXPECT_EQ(dist::format_for("x86_64-macos", fb), dist::Format::MachO);
// iOS shares macOS's Mach-O format. Before `format_for` asked
// `is_mach_o()` instead of `os == "macos"`, this triple matched none
// of the branches inside the parsed-triple block and fell through to
// `fb` itself -- so the assertion below would have failed for two of
// the three fallbacks in this loop, the exact "decided by the
// machine doing the building" defect this test exists to catch.
EXPECT_EQ(dist::format_for("aarch64-ios", fb), dist::Format::MachO);
EXPECT_EQ(dist::format_for("x86_64-windows-gnu", fb), dist::Format::Pe);
EXPECT_EQ(dist::format_for("x86_64-linux-gnu", fb), dist::Format::Elf);
EXPECT_EQ(dist::format_for("aarch64-linux-musl", fb), dist::Format::Elf);
EXPECT_EQ(dist::format_for("riscv64-none-elf", fb), dist::Format::Elf);
}
}
// The `[target.X]` escape hatch: a spelling the vocabulary cannot parse is all
// there is to go on, so LLVM's words are recognised there.
TEST(Distribution, FormatFallsBackToSpellingForAnUnparseableTriple) {
EXPECT_EQ(dist::format_for("arm64-apple-macos14.0", dist::Format::Elf),
dist::Format::MachO);
EXPECT_EQ(dist::format_for("x86_64-w64-mingw32", dist::Format::Elf),
dist::Format::Pe);
EXPECT_EQ(dist::format_for("x86_64-unknown-darwin", dist::Format::Elf),
dist::Format::MachO);
}
// And only then the host. A triple that says nothing at all is the one case
// where the machine doing the building is the best available answer.
TEST(Distribution, FormatUsesTheFallbackOnlyWhenTheTripleSaysNothing) {
EXPECT_EQ(dist::format_for("", dist::Format::MachO), dist::Format::MachO);
EXPECT_EQ(dist::format_for("", dist::Format::Pe), dist::Format::Pe);
EXPECT_EQ(dist::format_for("nonsense", dist::Format::Elf), dist::Format::Elf);
}
// ---------------------------------------------------------------------------
// A SECOND C++ RUNTIME ON THE LINE (mcpp#596).
//
// A lane whose device compiler is configured against libstdc++ puts libstdc++
// on the link line while the artifact links libc++ statically. Two things then
// have to change, and neither is a preference:
//
// * one unwinder. Linking libunwind.a pulls in only the archive members
// something referenced, so the interposition is partial by construction --
// measured, ten of libgcc's eighteen entry points came from the artifact
// and eight stayed in libgcc_s, including the accessors libstdc++'s
// personality routine calls. It read an LLVM libunwind context through
// libgcc's accessors and terminated past a matching handler.
// * the static archives' symbols hidden. The linker exports what a loaded
// object references, and a loaded libstdc++ references them: measured, 89
// exported symbols of which 68 were also defined by libstdc++ or libgcc_s.
//
// The byte-level assertions are the point, as they are for every other cell in
// this table: a build with no second runtime on its line must be unchanged.
TEST(Distribution, LinuxLibcxxWithAForeignRuntimeTakesOneUnwinder) {
dist::MechanismInput in;
in.format = dist::Format::Elf;
in.stdlibId = "libc++";
in.requested = dist::Contract::SelfContained;
in.libcxxArchive = "/tc/libc++.a";
in.libcxxAbiArchive = "/tc/libc++abi.a";
in.libunwindArchive = "/tc/libunwind.a";
// Unchanged when nothing else is on the line. Byte-for-byte the string the
// cell above asserts.
auto alone = dist::resolve(in);
EXPECT_EQ(alone.unitFlags,
" -nostdlib++ /tc/libc++.a /tc/libc++abi.a /tc/libunwind.a");
in.foreignCxxRuntime = true;
auto shared = dist::resolve(in);
// The contract is NOT degraded: the C++ runtime is still embedded, and
// libgcc_s is in the process either way because libstdc++ needs it.
EXPECT_EQ(shared.effective, dist::Contract::SelfContained);
EXPECT_FALSE(shared.degraded);
EXPECT_TRUE(shared.diagnostic.empty());
EXPECT_EQ(shared.unitFlags,
" -nostdlib++ /tc/libc++.a /tc/libc++abi.a"
" -Wl,--exclude-libs,libc++.a -Wl,--exclude-libs,libc++abi.a"
" --unwindlib=libgcc");
// The payload's unwinder archive is NOT on the line: linking it is what
// creates the second unwinder.
EXPECT_EQ(shared.unitFlags.find("libunwind.a"), std::string::npos);
}
// A shared library already hid these archives, and that path is untouched: the
// widened guard adds executables, it does not change what a .so emits.
TEST(Distribution, ASharedLibraryStillHidesTheArchivesWithoutASecondRuntime) {
dist::MechanismInput in;
in.format = dist::Format::Elf;
in.stdlibId = "libc++";
in.role = dist::Role::SharedLibrary;
in.requested = dist::Contract::SelfContained;
in.libcxxArchive = "/tc/libc++.a";
in.libcxxAbiArchive = "/tc/libc++abi.a";
in.libunwindArchive = "/tc/libunwind.a";
auto m = dist::resolve(in);
EXPECT_EQ(m.unitFlags,
" -nostdlib++ /tc/libc++.a /tc/libc++abi.a"
" -Wl,--exclude-libs,libc++.a -Wl,--exclude-libs,libc++abi.a"
" /tc/libunwind.a -Wl,--exclude-libs,libunwind.a");
}
// THE NAMES `--exclude-libs` MATCHES ARE THE ARCHIVES THE LINKER OPENS
// (#634 A6). The Android NDK's per-API `libc++.a` is a linker script,
// `INPUT(-lc++_static -lc++abi)`: its members come from `libc++_static.a`, and
// hiding `libc++.a` alone left 161 dynamic symbols in a self-contained shared
// library where naming `libc++_static.a` left 4 (measured). The paths on the
// line are unchanged; only the hidden names grow, and only when given.
TEST(Distribution, ALinkerScriptsArchivesAreTheNamesHidden) {
dist::MechanismInput in;
in.format = dist::Format::Elf;
in.stdlibId = "libc++";
in.role = dist::Role::SharedLibrary;
in.requested = dist::Contract::SelfContained;
in.libcxxArchive = "/ndk/sysroot/usr/lib/x86_64-linux-android/24/libc++.a";
in.libcxxAbiArchive = "/ndk/sysroot/usr/lib/x86_64-linux-android/libc++abi.a";
in.libunwindArchive = "/ndk/lib/clang/21/lib/linux/x86_64/libunwind.a";
in.libcxxLinkedArchiveNames = {"libc++.a", "libc++abi.a", "libc++_static.a"};
auto m = dist::resolve(in);
EXPECT_EQ(m.unitFlags,
" -nostdlib++ /ndk/sysroot/usr/lib/x86_64-linux-android/24/libc++.a"
" /ndk/sysroot/usr/lib/x86_64-linux-android/libc++abi.a"
" -Wl,--exclude-libs,libc++.a -Wl,--exclude-libs,libc++abi.a"
" -Wl,--exclude-libs,libc++_static.a"
" /ndk/lib/clang/21/lib/linux/x86_64/libunwind.a"
" -Wl,--exclude-libs,libunwind.a");
// An executable still carries no guard, whatever the names are.
in.role = dist::Role::Test;
EXPECT_EQ(dist::resolve(in).unitFlags.find("exclude-libs"), std::string::npos);
}
// ─── Format::Wasm, the member the module predicted and deferred ────────────
//
// `format_for`'s own comment said a wasm triple "falls out of every branch"
// and reaches `hostFallback`, answering the MACHINE's format -- "the same
// defect class its own header measured for macOS" -- and deferred the fourth
// member "to whoever gives this module a mechanism for it".
TEST(Distribution, FormatAnswersWasmForAWasmTargetAndNotTheHosts) {
using mcpp::build::dist::Format;
using mcpp::build::dist::format_for;
// Every host fallback, so this cannot pass by agreeing with the machine.
for (auto fallback : {Format::Elf, Format::MachO, Format::Pe})
EXPECT_EQ(format_for("wasm32-emscripten", fallback), Format::Wasm);
// The three it already answered stay unchanged.
EXPECT_EQ(format_for("x86_64-linux-gnu", Format::Pe), Format::Elf);
EXPECT_EQ(format_for("aarch64-macos", Format::Elf), Format::MachO);
EXPECT_EQ(format_for("x86_64-windows-gnu", Format::Elf), Format::Pe);
}
// THERE IS NOTHING TO BE COUPLED TO, so the contract is satisfied with
// nothing added and there is no degradation to report.
//
// While the member was missing, every wasm build printed a warning about a
// `libc++.so` that cannot exist for the target, on an artefact that has no
// run-time dependency of any kind. A diagnostic is for a BROKEN PROMISE; that
// one was a promise about a mechanism the format does not have.
TEST(Distribution, WasmIsSelfContainedByConstructionAndSaysNothing) {
mcpp::build::dist::MechanismInput in;
in.format = mcpp::build::dist::Format::Wasm;
in.stdlibId = "libc++";
in.requested = mcpp::build::dist::Contract::SelfContained;
in.role = mcpp::build::dist::Role::Distributable;
auto m = mcpp::build::dist::resolve(in);
EXPECT_EQ(m.effective, mcpp::build::dist::Contract::SelfContained);
EXPECT_FALSE(m.degraded);
EXPECT_TRUE(m.diagnostic.empty()) << m.diagnostic;
// And no flags: libc++ reaches a wasm link through `em++`'s own link line,
// so naming archives from a sysroot this module did not resolve would be a
// second answer to a question the driver has already answered.
EXPECT_TRUE(m.unitFlags.empty()) << m.unitFlags;
}
// ---------------------------------------------------------------------------
// #646 F3a -- one process, one C++ runtime.
//
// The ELF defaults give a program `self-contained` and a shared library
// `toolchain-coupled`. Each is right alone; a program that LOADS such a library
// held two C++ runtimes, and with llvm@22.1.8 it aborted with std::bad_cast.
// An unstated program or test contract takes the shared library's when its
// image loads a C++ shared library of the build.
TEST(Distribution, AProgramThatLoadsACxxSharedLibraryTakesItsContractOnElf) {
const dist::ContractStatement nothing{};
auto c = dist::role_contracts(nothing, dist::Format::Elf,
{.program = true, .tests = true});
EXPECT_EQ(c.shared, dist::Contract::ToolchainCoupled);
EXPECT_EQ(c.program, dist::Contract::ToolchainCoupled);
EXPECT_EQ(c.tests, dist::Contract::ToolchainCoupled);
// The intermediate role links no runtime and keeps the program default.
EXPECT_EQ(c.intermediate, dist::Contract::SelfContained);
EXPECT_FALSE(c.programStated);
EXPECT_FALSE(dist::runtime_split(c, dist::Format::Elf,
{.program = true, .tests = true}).has_value());
}
// Nothing moves for a program that loads no C++ shared library: the default
// that makes a shipped binary portable is untouched.
TEST(Distribution, AProgramThatLoadsNoCxxSharedLibraryKeepsItsDefault) {
auto c = dist::role_contracts({}, dist::Format::Elf, {});
EXPECT_EQ(c.program, dist::Contract::SelfContained);
EXPECT_EQ(c.tests, dist::Contract::SelfContained);
auto onlyTests = dist::role_contracts({}, dist::Format::Elf, {.tests = true});
EXPECT_EQ(onlyTests.program, dist::Contract::SelfContained);
EXPECT_EQ(onlyTests.tests, dist::Contract::ToolchainCoupled);
}
// Mach-O and PE keep their table: Mach-O's shared default is already
// self-contained (the F2 question is measured separately), and PE resolves
// imports per DLL.
TEST(Distribution, TheFollowingRuleIsElfOnly) {
for (auto f : {dist::Format::MachO, dist::Format::Pe, dist::Format::Wasm}) {
auto c = dist::role_contracts({}, f, {.program = true, .tests = true});
EXPECT_EQ(c.program, dist::Contract::SelfContained);
EXPECT_FALSE(dist::runtime_split(c, f, {.program = true}).has_value());
}
}
// A STATED contract is never moved. A stated self-contained program over a
// coupled C++ shared library is the split, and is named for the caller to
// refuse; a stated self-contained project (the library self-contained too) is
// the documented private-copy arrangement and is not.
TEST(Distribution, AStatedSelfContainedProgramOverACoupledLibraryIsASplit) {
const dist::ContractStatement split{
.cxxRuntime = "self-contained", .cxxRuntimeShared = "toolchain-coupled"};
auto c = dist::role_contracts(split, dist::Format::Elf, {.program = true});
EXPECT_EQ(c.program, dist::Contract::SelfContained);
EXPECT_TRUE(c.programStated);
auto role = dist::runtime_split(c, dist::Format::Elf, {.program = true});
ASSERT_TRUE(role.has_value());
EXPECT_EQ(*role, dist::Role::Distributable);
const dist::ContractStatement whole{.cxxRuntime = "self-contained"};
auto w = dist::role_contracts(whole, dist::Format::Elf, {.program = true});
EXPECT_EQ(w.shared, dist::Contract::SelfContained);
EXPECT_FALSE(dist::runtime_split(w, dist::Format::Elf, {.program = true}).has_value());
const dist::ContractStatement tests{.cxxRuntimeTests = "self-contained"};
auto t = dist::role_contracts(tests, dist::Format::Elf, {.tests = true});
auto testRole = dist::runtime_split(t, dist::Format::Elf, {.tests = true});
ASSERT_TRUE(testRole.has_value());
EXPECT_EQ(*testRole, dist::Role::Test);
}
// `static_stdlib = false` is a statement for every role: all of them are
// host-coupled and nothing splits.
TEST(Distribution, TheOlderSpellingStatesEveryRole) {
const dist::ContractStatement host{.staticStdlib = false};
auto c = dist::role_contracts(host, dist::Format::Elf, {.program = true, .tests = true});
EXPECT_EQ(c.program, dist::Contract::HostCoupled);
EXPECT_EQ(c.shared, dist::Contract::HostCoupled);
EXPECT_FALSE(dist::runtime_split(c, dist::Format::Elf, {.program = true}).has_value());
}
// ---------------------------------------------------------------------------
// #649 E10 -- clang on the MSVC ABI is given no CRT model, and its driver links
// the static CRT (`-defaultlib:libcmt`). The table recorded `host-coupled`
// beside an artifact that imports no vcruntime DLL. It now records what the
// row delivers, and an explicit request the row does not deliver says so.
TEST(Distribution, ClangOnTheMsvcAbiRecordsTheStaticCrtItsDriverLinks) {
dist::MechanismInput in;
in.format = dist::Format::Pe;
in.stdlibId = "msvc";
in.msvcCrtModelEmitted = false;
for (auto requested : {dist::Contract::SelfContained,
dist::Contract::HostCoupled,
dist::Contract::ToolchainCoupled}) {
in.requested = requested;
in.explicitRequest = false;
auto quiet = dist::resolve(in);
EXPECT_EQ(quiet.effective, dist::Contract::SelfContained);
EXPECT_FALSE(quiet.degraded);
EXPECT_FALSE(quiet.deployToolchainRuntime);
EXPECT_TRUE(quiet.unitFlags.empty());
}
in.explicitRequest = true;
in.requested = dist::Contract::HostCoupled;
auto undelivered = dist::resolve(in);
EXPECT_EQ(undelivered.effective, dist::Contract::SelfContained);
EXPECT_TRUE(undelivered.degraded);
EXPECT_NE(undelivered.diagnostic.find("not delivered"), std::string::npos)
<< undelivered.diagnostic;
EXPECT_NE(undelivered.diagnostic.find("libcmt"), std::string::npos)
<< undelivered.diagnostic;
in.requested = dist::Contract::SelfContained;
EXPECT_FALSE(dist::resolve(in).degraded);
}