rucc-driver 0.10.68

Command line, phase graph and job scheduling for the rucc C compiler.
Documentation
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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
//! Finding a linker and telling it what to link.
//!
//! Design: `spec/04-driver-and-cli.md` section 4.9. There is no linker of our own before 1.0, so
//! this finds one on the machine and builds the command line it wants.
//!
//! The linker is invoked directly rather than through the system compiler driver. Going through
//! `cc` would be shorter to write and would borrow that compiler's idea of where everything is,
//! and it would also mean this compiler cannot link on a machine that has no other compiler on
//! it, which is most of the machines a compiler ends up on. It would also make `-###` output a
//! line that does not say what happens, since the interesting half would be inside the program
//! being spawned.
//!
//! # What is not decided here
//!
//! The startup files and the library directories are looked for rather than configured, for the
//! same reason `library` looks for the headers: gcc settles this when it is built because a gcc
//! is built for the machine it will run on, and this is one binary that runs wherever it is
//! copied. So the shape of the answer is a list of candidates per platform of which the ones
//! that exist are taken, and a cross build says where the rest is with `--sysroot`.
//!
//! # The compiler's own runtime
//!
//! `crtbegin`, `crtend` and the runtime libraries are found the same way, on the machine rather
//! than by configuration. Ours is `librucc_builtins.a`, looked for beside the compiler, and the
//! machine's `libgcc` goes on after it for the parts we have not written, which today is the
//! unwinder and its personality routine. The C library goes in front of both, so that on a target
//! that has one its `memcpy` is the one that answers rather than ours. `-fno-builtins-lib` leaves
//! ours off, for somebody who wants libgcc to answer for everything.
//!
//! On a static link the three archives go inside `--start-group`, because `libc.a` refers to the
//! unwinder and the unwinder refers back to `libc.a`, and a linker walking a list once resolves
//! whichever of the two it reaches first and leaves the other undefined. That circularity is the
//! whole reason `-static` failed before this, and it is issue #277.
//!
//! # Linking for a machine that is not this one
//!
//! Everything above describes a link against the machine running the compiler, and it is what runs
//! when the target is that machine. A target that is not is a different problem: there is no
//! `crt1.o` for it in `/usr/lib`, the `libc.so` there is the wrong architecture, and a line built
//! out of what is lying around either fails at the first input or, worse, links. So a cross link
//! does not look at this machine at all. It is built by [`rucc_sysroot::argv`] out of the target
//! and a sysroot under the cache directory, and `spec/cross-compile/11-linking.md` section 11.3 is
//! the design. [`cross_sysroot`] is the one place that decides which of the two it is.
//!
//! Two conditions keep that out of the way of everything that works today. The target has to differ
//! from the host, and `--sysroot` must not have been given: somebody who assembled a tree and named
//! it is asking for the line above with their own root in front of every path, which is what a
//! cross compile with a real distribution tree in it has always been.
//!
//! That second condition is also the escape hatch for a machine which has a distribution's own cross
//! files installed, where `/usr/lib/aarch64-linux-gnu` really does hold an AArch64 `crt1.o`.
//! `--sysroot=/` takes the line above, and then every directory it decides is that machine's again.
//!
//! # What is not here yet
//!
//! Darwin, and Windows in Microsoft's ABI. `ld64` wants a platform version load command and a
//! different set of default libraries, and `lld-link` wants a `/`-style command line and an import
//! library set out of an SDK nobody may redistribute. Each arrives with the target that needs it,
//! and a cross link to either is refused by name rather than approximated. A mingw-w64 target does
//! have a line, because PE in that environment is written in the GNU style and the import libraries
//! for it are ours to produce.
//!
//! The headers are the other half of a cross compile and [`crate::library::header_dirs`] is where
//! they are decided. It asks [`cross_sysroot`] the same question this file asks it, which is the
//! point: a compile that took its libc from the sysroot and its declarations from this machine would
//! be wrong in the quietest way available, and one function answering for both is what stops that
//! being possible.

use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use rucc_sysroot::layout::{Kernel, Sysroot};
use rucc_sysroot::{LinkMode, argv};
use rucc_target::{Arch, Env, Os, Triple};
use rucc_tuple::TargetTuple;

/// What the command line said about linking.
///
/// Kept apart from `Options` because none of it reaches the compilation. A flag here changes what
/// the linker is told and changes nothing about the object files handed to it, which is why `-lm`
/// on a `-c` line is a note rather than an error.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct LinkOptions {
    /// `-fuse-ld=<name>`, which names a linker rather than a path to one.
    pub use_ld: Option<String>,
    /// `-L<dir>`, in order, because the linker takes the first library it finds.
    pub search: Vec<PathBuf>,
    /// `-B<prefix>`, which is where to look for the linker before looking on the path.
    pub prefixes: Vec<PathBuf>,
    /// `--sysroot=<dir>`, which prefixes the directories this looks in.
    pub sysroot: Option<PathBuf>,
    /// Where the generated sysroots are, which is [`crate::cache::dir`] on a real command line.
    ///
    /// [`None`] is a caller that was not given one, which outside a test is nothing, and then there
    /// is no cross link line and a foreign target is refused the way it was before there was one.
    /// It is a field rather than a call inside this module because a link line that read the
    /// environment could only be tested on a machine whose environment said the right thing.
    pub cache: Option<PathBuf>,
    /// `-static`.
    pub is_static: bool,
    /// `-shared`.
    pub shared: bool,
    /// `-pie` or `-no-pie`, and the platform's default when neither was written.
    pub pie: Option<bool>,
    /// `-nostdlib`, which is `-nostartfiles` and `-nodefaultlibs` together.
    pub no_stdlib: bool,
    /// `-nostartfiles`.
    pub no_startfiles: bool,
    /// `-nodefaultlibs`.
    pub no_defaultlibs: bool,
    /// `-rdynamic`, which puts every symbol in the dynamic table so a program can look itself up.
    pub export_dynamic: bool,
    /// `-s`, which drops the symbol table.
    pub strip: bool,
    /// `-fno-builtins-lib`, which leaves our own runtime off the line so that the machine's
    /// libgcc answers for everything instead.
    pub no_builtins_lib: bool,
    /// The whole ten field target when `--target=` spelled one, which is where a pinned libc
    /// release is.
    ///
    /// [`None`] is a command line that named no target at all, and then there is nothing pinned and
    /// this machine is the target. A `Triple` has room for an architecture, an OS and an
    /// environment and nowhere to put a release, so the release arrives here instead of there, and
    /// [`cross_sysroot`] reads it for both of the things it decides: whether this is a cross link
    /// and which directory under the cache it is against.
    pub pinned: Option<TargetTuple>,
    /// `-pg`, which changes the link as well as the code.
    ///
    /// The counts a profiled program keeps have to be started before `main` runs and written out
    /// after it returns, and what does both is a start file of its own. So a build that compiles
    /// with the flag and links without it produces a program that calls the hook on every function
    /// and never writes a profile.
    pub profile: bool,
}

impl LinkOptions {
    /// Whether the startup files go on the line.
    fn wants_startfiles(&self) -> bool {
        !self.no_stdlib && !self.no_startfiles
    }

    /// Whether the library the program was written against goes on the line.
    fn wants_defaultlibs(&self) -> bool {
        !self.no_stdlib && !self.no_defaultlibs
    }

    /// Whether the compiler's own runtime goes on the line.
    ///
    /// The same switch as the C library, because `-nodefaultlibs` in GCC means the compiler's
    /// runtime too, and a link that keeps `libgcc` while dropping `libc` is not a thing anyone
    /// asks for on purpose.
    fn wants_runtime(&self) -> bool {
        !self.no_stdlib && !self.no_defaultlibs
    }
}

/// One item on the link line, in the order it was written, because link order is semantic.
///
/// A library named before the object that needs it is not found on a static link, which is the
/// oldest surprise in the toolchain and the reason this is one ordered list rather than a list of
/// files and a list of libraries.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Item {
    /// A file: an object this compilation produced, or one named on the command line.
    File(String),
    /// `-l<name>`, which the linker resolves against its search path.
    Library(String),
    /// One word from `-Wl,` or `-Xlinker`, handed to the linker where the user wrote it.
    ///
    /// Here rather than in a list of its own because a great many of the linker's options are a
    /// bracket around the files after them, and an option moved away from what it brackets means
    /// something else or nothing at all. `--whole-archive` says that every member of every archive
    /// named after it goes in whether anything referenced it or not, `--start-group` says that the
    /// archives after it are searched again until nothing more comes out, and `-Bstatic` says which
    /// half of a library that ships both is wanted. Collecting them and appending them to the end
    /// leaves each of those pointing at nothing.
    Linker(String),
}

impl std::fmt::Display for Item {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Item::File(path) => f.write_str(path),
            Item::Library(name) => write!(f, "-l{name}"),
            Item::Linker(arg) => write!(f, "-Wl,{arg}"),
        }
    }
}

/// Why a link could not be run.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
    /// No linker was found, after looking everywhere there was to look.
    NoLinker {
        /// The names that were tried, in the order they were tried.
        tried: Vec<String>,
    },
    /// `-fuse-ld=` named one that is not on this machine.
    Named {
        /// What it named.
        name: String,
    },
    /// A target this does not know how to build a link line for.
    Target {
        /// The triple that was asked for.
        triple: String,
    },
    /// A cross link this scheme cannot produce, which [`rucc_sysroot::argv`] has explained.
    ///
    /// The reason is carried as a sentence rather than as a variant per cause, because the causes
    /// live in `rucc-sysroot` and a second enumeration here would be a second thing to keep in step
    /// with them. What this adds is that the sentence came from a link rather than from a
    /// compilation.
    Cross {
        /// Why, in full, ready to print.
        why: String,
    },
    /// The sysroot a cross link needs is not on this machine.
    Sysroot {
        /// The target that was asked for.
        target: String,
        /// Where its sysroot would be.
        dir: String,
        /// Whether this release pins an artifact for that target, which decides whether the message
        /// can name a command that would fix it.
        pinned: bool,
    },
    /// The linker was found and cannot do this target's link.
    ///
    /// Separate from [`Error::NoLinker`] because the linker is there and runs, and separate from
    /// [`Error::Refused`] because the refusal is ours rather than its own: this is the case the
    /// linker would not complain about at all.
    TooOld {
        /// What it was found as, which is what to look for when replacing it.
        name: String,
        /// The major version it reported.
        found: u32,
        /// The target whose link it cannot do.
        target: String,
    },
    /// The linker was found and could not be started.
    Spawn {
        /// Where it was.
        path: String,
        /// What the operating system said.
        why: String,
    },
    /// The linker ran and said no.
    Refused {
        /// What it exited with, or a description when it was killed instead.
        status: String,
    },
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::NoLinker { tried } => {
                write!(f, "no linker was found; tried {}", tried.join(", "))
            }
            Error::Named { name } => {
                write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
            }
            Error::Target { triple } => {
                write!(f, "there is no link line for {triple} in this compiler yet")
            }
            Error::Cross { why } => f.write_str(why),
            // Two sentences and the second one changes, because a person whose link just failed
            // wants the command that fixes it and there is only a command to name when this release
            // pins an artifact for that target. Section 13.8's rule is that a compile which is
            // missing a sysroot says what to run rather than running it, and this is where it says
            // it.
            Error::Sysroot { target, dir, pinned: true } => write!(
                f,
                "there is no sysroot for {target} at {dir}, so there is nothing to link it \
                 against. `rucc --fetch {target}` gets the one this release pins, or pass \
                 --sysroot=<dir> to name a tree you have already"
            ),
            Error::Sysroot { target, dir, pinned: false } => write!(
                f,
                "there is no sysroot for {target} at {dir}, so there is nothing to link it \
                 against, and this release pins none for it to fetch. Pass --sysroot=<dir> to name \
                 a tree you have already, or see spec/cross-compile/13-distribution.md section \
                 13.2 for the cache that will hold one"
            ),
            // The whole message, because the person reading it has a linker that works, a link that
            // succeeded on their last try, and no reason to suspect the thing that is wrong.
            Error::TooOld { name, found, target } => write!(
                f,
                "{name} is lld {found} and cannot link for {target}. mingw-w64 writes a few hundred \
                 of its aliases, `_crt_atexit == atexit` among them, as IMPORT_NAME_EXPORTAS \
                 records in its import libraries, which lld learned to read in {LLD_EXPORTAS}. An \
                 older one neither reads them nor says so: it writes an import by ordinal zero, the \
                 link succeeds, and the program dies at startup. Install lld {LLD_EXPORTAS} or \
                 newer, or name one with -fuse-ld="
            ),
            Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
            Error::Refused { status } => write!(f, "the linker {status}"),
        }
    }
}

impl std::error::Error for Error {}

/// A linker, found.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Linker {
    /// The name it is known by, which is what `--print-config` reports.
    pub name: String,
    /// Where it is, which is what gets spawned.
    pub path: PathBuf,
}

/// The names to look for, in the order section 4.9 gives.
///
/// `mold` first because it is dramatically faster, and a compiler that is twice the speed of
/// another one while the link takes twelve seconds has not helped anybody. Then `lld`, then the
/// platform's own. Each is looked for under both the bare name and the `ld.` prefix, because a
/// distribution installs `mold` under its own name and `ld.mold` for exactly this lookup.
#[must_use]
pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
    if let Some(named) = &opts.use_ld {
        // A name rather than a path, so `-fuse-ld=mold` finds a `mold` that is not `ld.mold`.
        return vec![format!("ld.{named}"), named.clone()];
    }
    if cross_sysroot(target, opts).is_some() {
        return cross_order(target);
    }
    match target.os {
        Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
        _ => vec![
            "ld.mold".to_owned(),
            "mold".to_owned(),
            "ld.lld".to_owned(),
            "lld".to_owned(),
            "ld".to_owned(),
        ],
    }
}

/// The names to look for when the target is not this machine.
///
/// A shorter list than the one above and a different one, because most of that list cannot do this.
/// `spec/cross-compile/11-linking.md` section 11.2 settles it: `ld.lld` is the ELF cross linker,
/// since one binary of it links for every architecture it was built with and that is all of them.
/// mold is off the list because it links for the host and `wild` likewise, which is why section 11.2
/// has them as `-fuse-ld=` choices for a native link rather than as defaults. The platform's own
/// `ld` is off it for the same reason: a distribution's `/usr/bin/ld` is built for one architecture,
/// and `-fuse-ld=` is still there for somebody whose is not.
///
/// A cross binutils under its prefixed name is last, because a machine that has
/// `aarch64-linux-gnu-ld` installed has it on purpose. The prefix is a distribution convention and
/// there are two of them: a Linux target is filed under its multiarch name and a mingw-w64 one under
/// `<arch>-w64-mingw32`, which is what every distribution's mingw packages install. `ld.lld` is the
/// same binary for both, because its MinGW mode is a mode of the one linker rather than a second one.
fn cross_order(target: Triple) -> Vec<String> {
    let mut names = vec!["ld.lld".to_owned(), "lld".to_owned()];
    match (target.os, target.env) {
        (Os::Linux, _) => names.push(format!("{}-ld", multiarch(target))),
        (Os::Windows, Env::Gnu) => names.push(format!("{}-w64-mingw32-ld", target.arch.as_str())),
        _ => {}
    }
    names
}

/// The sysroot a cross link would use, or [`None`] for a link against this machine.
///
/// The one place the two paths are told apart, so that the linker that is looked for and the line it
/// is handed cannot disagree about which kind of link this is.
///
/// Three conditions, and two of them are about leaving working configurations alone. A target that
/// is this machine is linked against this machine, which is what every native compile has always
/// done and what the directories under `/usr/lib` are for. A `--sysroot` the user wrote is taken as
/// the root of a tree they assembled, and the line above prefixes every path it decides with it,
/// which is what cross compiling against a real distribution tree has always meant here. The third
/// is that there has to be a cache directory to look in, which on a real command line there always
/// is.
///
/// An unknown host counts as different from every target. A machine this compiler cannot name is a
/// machine whose `/usr/lib` it should not be guessing at.
///
/// # A pinned release is a cross compile
///
/// The first of those three conditions is about the machine and not about the triple, and a target
/// that names a libc release is not this machine even when it is this architecture. Somebody on a
/// 2.44 box writing `--target=x86_64-linux-gnu.2.28` is asking for a binary that runs on a 2.28
/// machine, and handing them their own headers and their own libc gives them a binary that does not.
/// So the condition is the triple being the host *and* no release named, and what it costs is that a
/// pin equal to this machine's own release also stops using this machine's libc. That is not a loss:
/// the two should be the same text, and if they are not then this machine's copy is patched and the
/// bundled tree is the one the pin asked for. tamnd/rucc#956.
#[must_use]
pub fn cross_sysroot(target: Triple, opts: &LinkOptions) -> Option<Sysroot> {
    cross_for(target, opts, Triple::host())
}

/// The same answer with the host as a parameter, so that both branches are testable on one machine.
fn cross_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Sysroot> {
    if opts.sysroot.is_some() {
        return None;
    }
    let tuple = target_tuple(target, opts);
    if host == Some(target) && tuple.env_version().is_none() {
        return None;
    }
    let cache = opts.cache.as_deref()?;
    Some(Sysroot::in_cache(cache, tuple))
}

/// The target as the model that has room for a release, which is what names the cache directory.
///
/// The pinned spelling when there is one, because `x86_64-linux-gnu` and `x86_64-linux-gnu.2.28` are
/// two sysroots and not one: the release is in the tuple for the reason
/// `spec/cross-compile/03-target-model.md` section 3.2 admits a field at all, which is that it
/// changes what is compiled. A command line that named no target, or one whose spelling the ten
/// field parser did not take, falls back to what the three field one did.
fn target_tuple(target: Triple, opts: &LinkOptions) -> TargetTuple {
    opts.pinned.unwrap_or_else(|| target.tuple())
}

/// The kernel headers that go with [`cross_sysroot`], for the targets that have any.
///
/// The same three conditions, asked through the same function, because the two halves of one
/// target's system headers have to be decided together or a compile could read glibc's `sys/stat.h`
/// against this machine's `asm/stat.h`. A `None` here on a Linux target where the sysroot is `Some`
/// means only one thing, which is that the cache has no kernel tree for that architecture, and the
/// directory is still named for the reason [`crate::library::header_dirs`] gives.
///
/// Not under the sysroot, because `linux/` and `asm-generic/` are the same nine megabytes for every
/// target that shares an architecture, and a copy per target is eight copies of one thing.
#[must_use]
pub fn cross_kernel(target: Triple, opts: &LinkOptions) -> Option<Kernel> {
    kernel_for(target, opts, Triple::host())
}

/// The same answer with the host as a parameter, for the same reason as [`cross_for`].
fn kernel_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Kernel> {
    cross_for(target, opts, host)?;
    Kernel::for_target(opts.cache.as_deref()?, target.tuple())
}

/// How the result is linked, as the five cases a sysroot link line is written over.
///
/// Four booleans reach here and five cases leave, because static and position independent are not
/// independent of each other and the start file differs in four of the five. The default for `pie`
/// is the one the native line above uses, so that a command line that says neither gets the same
/// answer whichever path it takes.
fn mode(opts: &LinkOptions) -> LinkMode {
    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
    if opts.shared {
        LinkMode::Shared
    } else if opts.is_static {
        if pie { LinkMode::StaticPie } else { LinkMode::Static }
    } else if pie {
        LinkMode::Dynamic
    } else {
        LinkMode::DynamicNoPie
    }
}

/// The line for a machine that is not this one, from the target and the sysroot and nothing else.
///
/// Everything this knows is already in `opts`, and all it does is say it in the shape
/// [`rucc_sysroot::argv`] is written over. There is deliberately no decision here: a second place
/// that decided what goes on a cross link line would be a second place to get it wrong, and the
/// recorded lines under `tests/link-lines` would stop describing what this compiler does.
fn cross_line(
    target: Triple,
    opts: &LinkOptions,
    items: &[Item],
    output: &str,
    sysroot: &Sysroot,
) -> Result<Vec<String>, Error> {
    if opts.profile {
        // `gcrt1.o` is a compiled object out of the C library's own sources, and a generated sysroot
        // has the names a libc exports rather than the bodies behind them. Said here rather than
        // left to the linker, because what the linker would say is that `main` is undefined.
        return Err(Error::Cross {
            why: format!(
                "-pg needs gcrt1.o, or gcrt2.o on Windows, the startup file that starts and stops \
                 the counting, and a generated sysroot for {target} does not have one. Profile on \
                 the host, or pass --sysroot=<dir> naming a tree that has it"
            ),
        });
    }
    let inputs: Vec<argv::Item> = items
        .iter()
        .map(|item| match item {
            Item::File(path) => argv::Item::File(PathBuf::from(path)),
            Item::Library(name) => argv::Item::Library(name.clone()),
            Item::Linker(arg) => argv::Item::Linker(arg.clone()),
        })
        .collect();
    let output = PathBuf::from(output);
    // Ours, from beside the compiler, because that is where `cargo xtask builtins` writes it and a
    // fetched sysroot will never hold it. The cross line used to name it inside the sysroot, which
    // is a file nothing puts there, so every cross link either failed at the linker or quietly ran
    // against somebody else's `libgcc` copied in under the name. tamnd/rucc#1514.
    let ours = builtins_archive(target, &opts.prefixes);
    if ours.is_none() && opts.wants_runtime() && !opts.no_builtins_lib {
        // Said here rather than left to the linker, which on a Windows target says `___chkstk_ms`
        // is undefined and names mingw-w64's objects as the callers, and on a musl one says
        // `__udivti3` is. Neither of those is a person's first guess at a missing archive.
        let tuple = target.tuple().to_canonical_string();
        return Err(Error::Cross {
            why: format!(
                "a cross link ends with librucc_builtins.a, this compiler's own runtime for \
                 {tuple}, and there is none beside the compiler or under a -B prefix. A sysroot \
                 does not carry it, because it is our output rather than the platform's. Build it \
                 with `cargo xtask builtins --target={tuple}`, or pass -fno-builtins-lib to link \
                 without it"
            ),
        });
    }
    let invocation = argv::Invocation {
        inputs: &inputs,
        output: Some(&output),
        mode: mode(opts),
        search: &opts.search,
        no_startfiles: !opts.wants_startfiles(),
        no_defaultlibs: !opts.wants_defaultlibs(),
        no_builtins_lib: opts.no_builtins_lib,
        builtins: ours.as_deref(),
        export_dynamic: opts.export_dynamic,
        strip: opts.strip,
    };
    argv::argv(target.tuple(), sysroot, &invocation)
        .map_err(|why| Error::Cross { why: why.to_string() })
}

/// Whether this link can be run at all, asked before anything is compiled.
///
/// Two questions that have answers before the first object exists: whether there is a line for this
/// target and mode at all, and whether the sysroot it would read is on the machine. Both are worth a
/// second at the start rather than a message after a minute of compiling, which is the same reason
/// the linker itself is looked for first.
///
/// The line is built rather than inspected, with no inputs and a name nothing will be written to,
/// because the refusals belong to the one function that builds it. A link against this machine has
/// nothing to answer here: its directories are looked for as the line is built and a missing one is
/// simply a directory that is not offered.
///
/// # Errors
///
/// [`Error::Cross`] for a target or a mode that has no line, and [`Error::Sysroot`] when the sysroot
/// it would be linked against is not there.
pub fn preflight(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
    let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
    // Whether there is a line for this target and mode at all, asked with our own runtime left off
    // it. Otherwise a target nothing here can link and a machine where nobody built the runtime
    // report the same thing, and the archive is the smaller of the two problems by a long way.
    let shape = LinkOptions { no_builtins_lib: true, ..opts.clone() };
    cross_line(target, &shape, &[], "a.out", &sysroot)?;
    // The library directory rather than the root, because the root of a cache directory that has
    // been created and never populated is there and holds nothing. Section 11.6's rule is that
    // suitable is checked and not assumed, and this is the cheapest form of that.
    if !sysroot.lib().is_dir() {
        let tuple = target_tuple(target, opts).to_canonical_string();
        return Err(Error::Sysroot {
            dir: sysroot.root().display().to_string(),
            pinned: rucc_sysroot::pinned_for(&tuple).is_some(),
            target: tuple,
        });
    }
    // And now the whole line, which is the sysroot's files plus ours, so that a missing runtime is
    // said here rather than by the linker after everything has been compiled.
    cross_line(target, opts, &[], "a.out", &sysroot)?;
    Ok(())
}

/// The linker to use, looked for where a linker is.
///
/// `-B` prefixes first, since the point of one is to put a toolchain in front of the machine's,
/// then the path. A name that contains a separator is a path and is taken as one, which is what
/// gcc does with `-fuse-ld=/usr/bin/ld.gold` and what a build system relying on that expects.
///
/// # Errors
///
/// [`Error::Named`] when `-fuse-ld=` asked for one that is not here, and [`Error::NoLinker`] when
/// nothing was, which name the candidates so that the message says what was looked for.
pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
    let tried = order(target, opts);
    for name in &tried {
        if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
            let path = PathBuf::from(name);
            if path.is_file() {
                return Ok(Linker { name: name.clone(), path });
            }
            continue;
        }
        for dir in &opts.prefixes {
            let path = dir.join(name);
            if path.is_file() {
                return Ok(Linker { name: name.clone(), path });
            }
        }
        if let Some(path) = on_path(name) {
            return Ok(Linker { name: name.clone(), path });
        }
    }
    match &opts.use_ld {
        Some(name) => Err(Error::Named { name: name.clone() }),
        None => Err(Error::NoLinker { tried }),
    }
}

/// The first lld that reads `IMPORT_NAME_EXPORTAS`, which is what a windows-gnu link needs.
///
/// 18 does not read it and does not say so, so the number is not a convenience: below it the
/// answer is wrong rather than absent. tamnd/rucc#1515.
pub const LLD_EXPORTAS: u32 = 19;

/// Whether a found linker can do this target's link, asked before it is handed anything.
///
/// Section 11.6's rule is that suitable is checked and not assumed, and this is the one check that
/// cannot be made by looking at a file. A windows-gnu link reads import libraries that mingw-w64's
/// `==` aliases compiled into `IMPORT_NAME_EXPORTAS` records, which lld reads from
/// [`LLD_EXPORTAS`] on. An older lld writes an import by ordinal zero instead, without a warning
/// and with a successful exit, so nothing later in the toolchain has anything to notice: the
/// program is wrong at startup and the link that made it said nothing. Ubuntu 24.04 is the current
/// LTS and ships 18, so the machine this happens on is an ordinary one.
///
/// Every other target is left alone, and so is anything that is not an lld, because this is the one
/// version of the one linker that is known to answer wrongly rather than not at all.
///
/// A linker that will not run or whose version cannot be read is allowed through. What the check
/// can establish is that a specific old lld is here, and it should not turn every unusual linker
/// into a refusal on the strength of failing to recognise it.
///
/// # Errors
///
/// [`Error::TooOld`] when the linker is an lld older than [`LLD_EXPORTAS`] and the target is
/// windows-gnu.
pub fn suitable(target: Triple, linker: &Linker) -> Result<(), Error> {
    if (target.os, target.env) != (Os::Windows, Env::Gnu) {
        return Ok(());
    }
    let Some(found) = lld_major(&reported_version(&linker.path)) else { return Ok(()) };
    if found >= LLD_EXPORTAS {
        return Ok(());
    }
    Err(Error::TooOld {
        name: linker.name.clone(),
        found,
        target: target.tuple().to_canonical_string(),
    })
}

/// What `<linker> --version` prints, or an empty string when it will not say.
///
/// A linker that cannot be started is not this function's problem to report, because the link is
/// about to start it again and say so properly. What this returns for such a one is nothing to
/// read, which is the same as a linker that ran and said something unrecognisable.
fn reported_version(path: &Path) -> String {
    let Ok(out) = Command::new(path).arg("--version").output() else { return String::new() };
    String::from_utf8_lossy(&out.stdout).into_owned()
}

/// The major version in an lld's `--version`, when the program that printed it was an lld.
///
/// What lld prints is `LLD 18.1.8 (compatible with GNU linkers)`, with a distribution's own prefix
/// in front of it often enough that the word is looked for rather than the line starting with it:
/// Ubuntu's says `Ubuntu LLD 18.1.3`. Binutils prints `GNU ld (GNU Binutils for Ubuntu) 2.42` and
/// mold prints its own name, and neither has the word, so both come back as [`None`] and are left
/// alone.
fn lld_major(text: &str) -> Option<u32> {
    let mut words = text.split_whitespace();
    words.find(|word| *word == "LLD")?;
    words.next()?.split('.').next()?.parse().ok()
}

/// The first executable of that name on `PATH`.
///
/// Executability is checked rather than assumed, because a directory of that name on `PATH` is
/// not a thing to try to run and neither is a file nobody may execute.
fn on_path(name: &str) -> Option<PathBuf> {
    let path = std::env::var_os("PATH")?;
    std::env::split_paths(&path).map(|dir| dir.join(name)).find(|p| executable(p))
}

/// Whether a path is a file this process could run.
#[cfg(unix)]
fn executable(path: &Path) -> bool {
    use std::os::unix::fs::PermissionsExt as _;
    path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
}

/// Whether a path is a file this process could run.
///
/// Windows has no executable bit and decides by extension, and the names looked for above carry
/// theirs, so being a file is the whole of the question here.
#[cfg(not(unix))]
fn executable(path: &Path) -> bool {
    path.is_file()
}

/// What the linker is told, in order, not counting the linker itself.
///
/// Two lines and [`cross_sysroot`] picks which: the one above for this machine, and
/// [`rucc_sysroot::argv`]'s for any other. Nothing about the machine is read on the second path, so
/// `-###` prints the same line on every host and prints it whether the sysroot has been built or
/// not, which is what makes it worth printing.
///
/// # Errors
///
/// [`Error::Target`] for a platform there is no native line for yet, which is every one but Linux,
/// and [`Error::Cross`] for a cross link that cannot be produced at all.
pub fn line(
    target: Triple,
    opts: &LinkOptions,
    items: &[Item],
    output: &str,
) -> Result<Vec<String>, Error> {
    if let Some(sysroot) = cross_sysroot(target, opts) {
        return cross_line(target, opts, items, output, &sysroot);
    }
    if target.os != Os::Linux {
        return Err(Error::Target { triple: target.to_string() });
    }
    let machine = emulation(target);
    let root = opts.sysroot.as_deref();
    let dirs = library_dirs(target, root);
    // Where a gcc on this machine keeps its own runtime, which is a different place from where
    // the C library keeps its own, and where our runtime is if it was built for this target.
    let runtime = runtime_dirs(target, root);
    let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
    let mut args = vec![
        "-o".to_owned(),
        output.to_owned(),
        // Which of the several formats one `ld` can write is meant. A linker built for more than
        // one machine guesses from its first input otherwise, and a link of no objects at all has
        // nothing to guess from.
        "-m".to_owned(),
        machine.to_owned(),
        // The table a program unwinds through, which a C program with no exceptions in it still
        // needs because `backtrace` and every crash handler read it.
        "--eh-frame-hdr".to_owned(),
        // The symbol hash a dynamic loader from this century reads. The old one is still written
        // alongside by default on some distributions, and asking for this one is what stops a link
        // from carrying a table nothing has needed since 2006.
        "--hash-style=gnu".to_owned(),
    ];

    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
    if opts.shared {
        args.push("-shared".to_owned());
    } else if opts.is_static {
        args.push("-static".to_owned());
    } else if pie {
        args.push("-pie".to_owned());
    } else {
        args.push("-no-pie".to_owned());
    }
    if !opts.is_static && !opts.shared {
        args.push("-dynamic-linker".to_owned());
        args.push(target_path(root, loader(target)));
    }
    if opts.export_dynamic {
        args.push("--export-dynamic".to_owned());
    }
    if opts.strip {
        args.push("-s".to_owned());
    }

    if opts.wants_startfiles() {
        for name in startfile(opts, pie).into_iter().chain(["crti.o"]) {
            if let Some(path) = find_file(&dirs, name) {
                args.push(path.display().to_string());
            }
        }
        // The compiler's own startup file, which runs the static constructors. Three spellings
        // of the same thing, and which one is right is about how the code in it refers to
        // itself: `S` for a position independent result, `T` for a static one, plain for the
        // rest. Skipped when there is no gcc on the machine to take it from, because a program
        // with no constructor in it does not miss it.
        let begin = if opts.shared || pie {
            "crtbeginS.o"
        } else if opts.is_static {
            "crtbeginT.o"
        } else {
            "crtbegin.o"
        };
        if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
        {
            args.push(path.display().to_string());
        }
    }

    for dir in &opts.search {
        args.push(format!("-L{}", dir.display()));
    }
    for dir in &dirs {
        args.push(format!("-L{}", dir.display()));
    }
    // Where `libgcc.a` and `libgcc_eh.a` are, which is not where the C library is. Nothing is
    // added when there is no gcc on the machine, and then the `-l` names below are left off too.
    for dir in &runtime {
        args.push(format!("-L{}", dir.display()));
    }

    for item in items {
        match item {
            Item::File(path) => args.push(path.clone()),
            Item::Library(name) => args.push(format!("-l{name}")),
            Item::Linker(arg) => args.push(arg.clone()),
        }
    }
    // After the objects, because a static archive is searched for what is undefined at the point
    // it is reached and a library named before the object that needs it contributes nothing.
    args.extend(runtime_items(opts, &runtime, ours.as_deref()));

    if opts.wants_startfiles() {
        // The other end of `crtbegin`, and it goes before `crtn.o` for the same reason `crti.o`
        // goes before `crtbegin`: the four are two nested pairs and not four separate files.
        let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
        if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
            args.push(path.display().to_string());
        }
        if let Some(path) = find_file(&dirs, "crtn.o") {
            args.push(path.display().to_string());
        }
    }

    Ok(args)
}

/// The startup file the C library brings, or `None` for a link that calls nothing.
///
/// This is what calls `main` and what passes it the arguments, so a shared object takes none of
/// them: nothing starts one and it has no `main` to be started at. `Scrt1.o` rather than `crt1.o`
/// when the result moves, because the two differ in whether the reference to `main` in them is one
/// a loader may relocate.
///
/// A profiled program gets a different one again, which does all of that and starts and stops the
/// counting around it. There are two of those rather than three: the one that relocates itself is
/// only needed by a static position independent link, and every other link takes the plain one,
/// which is what gcc does with the same flag.
fn startfile(opts: &LinkOptions, pie: bool) -> Option<&'static str> {
    if opts.shared {
        None
    } else if opts.profile {
        Some(if pie && opts.is_static { "grcrt1.o" } else { "gcrt1.o" })
    } else if pie {
        Some("Scrt1.o")
    } else {
        Some("crt1.o")
    }
}

/// The libraries the compiler's own runtime contributes, in the order the linker wants them.
///
/// The C library first, then ours, then the machine's `libgcc`. Order inside this list is not
/// about whether a symbol resolves, it is about which archive supplies one that more than one of
/// them defines, and the two places that happens both have a right answer.
///
/// `memcpy` and its three neighbours are in the C library on a hosted target and in ours only for
/// a freestanding one, which is what `spec/12-abi-and-runtime.md` section 12.8 says they are for.
/// glibc's are written in assembly per microarchitecture and ours is a word at a time loop, so a
/// link that took ours over glibc's would be slower at the one routine every program reaches.
///
/// The wide arithmetic is in ours and in `libgcc` both, and the two are ABI-identical on purpose,
/// so which one answers is not a correctness question. Ours comes first because it is ours, and
/// `-fno-builtins-lib` leaves it off for somebody who would rather it were not.
///
/// A static link puts the whole list inside `--start-group`. `libc.a` refers to `_Unwind_Resume`,
/// and the unwinder refers back into `libc.a`, so a linker walking the list once resolves
/// whichever it reaches first and reports the other as undefined. That is exactly the failure
/// issue #277 describes and the group is the fix for it.
///
/// A dynamic link needs no group, because the shared `libc` resolves its own references inside
/// itself. `libgcc_s` is asked for `--as-needed` there, the way gcc asks for it, so a program that
/// never unwinds does not acquire a dependency on it.
fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
    let mut args = Vec::new();
    if !opts.wants_defaultlibs() && !opts.wants_runtime() {
        return args;
    }
    // Only when there is a gcc to take them from. On a machine without one the names would be an
    // error about a library that was never going to be there, and a program that needs neither
    // the unwinder nor a wide divide links and runs without them.
    let has_gcc = find_file(runtime, "libgcc.a").is_some();

    if opts.is_static {
        args.push("--start-group".to_owned());
    }
    if opts.wants_defaultlibs() {
        args.push("-lc".to_owned());
    }
    if opts.wants_runtime() {
        if let Some(path) = ours {
            args.push(path.display().to_string());
        }
        if has_gcc {
            args.push("-lgcc".to_owned());
            if opts.is_static {
                args.push("-lgcc_eh".to_owned());
            }
        }
    }
    if opts.is_static {
        args.push("--end-group".to_owned());
    } else if opts.wants_runtime() && has_gcc {
        // The shared half, and only if something still wants it after everything above.
        args.push("--as-needed".to_owned());
        args.push("-lgcc_s".to_owned());
        args.push("--no-as-needed".to_owned());
    }
    args
}

/// Where a gcc on this machine keeps `crtbegin.o`, `crtend.o` and `libgcc.a`, newest first.
///
/// This is not where the C library's files are. A distribution puts them under a directory named
/// for the gcc version, and there may be several, so the answer is every one that exists with the
/// highest version in front. Newest first because a newer `libgcc` is a superset of an older one
/// and because that is the one the C library on the same machine was built against.
#[must_use]
pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
    let libc = match target.env {
        Env::Musl => "musl",
        Env::None | Env::Gnu | Env::Msvc => "gnu",
    };
    let arch = target.arch.as_str();
    // The spellings the distributions use for the same triple. Debian and Ubuntu drop the vendor
    // field, the source builds and Arch keep `pc`, and Red Hat and SUSE write their own name in
    // it, so all of them are looked for and the ones that are there are taken.
    let names = [
        format!("{arch}-linux-{libc}"),
        format!("{arch}-pc-linux-{libc}"),
        format!("{arch}-redhat-linux"),
        format!("{arch}-suse-linux"),
        format!("{arch}-alpine-linux-{libc}"),
    ];
    let mut found = Vec::new();
    for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
        for name in &names {
            let dir = under(sysroot, &format!("{base}/{name}"));
            let Ok(entries) = fs::read_dir(&dir) else { continue };
            let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
                .flatten()
                .map(|e| e.path())
                .filter(|p| p.is_dir())
                .map(|p| (version_key(&p), p))
                .collect();
            // Descending, so the highest version is the first place `find_file` looks. Ties keep
            // the order the directory gave, which is arbitrary and does not matter because two
            // directories that sort the same hold the same version.
            versions.sort_by(|a, b| b.0.cmp(&a.0));
            found.extend(versions.into_iter().map(|(_, path)| path));
        }
    }
    found
}

/// A directory name read as a version, so that `13` sorts above `9` and `10.2` above `10`.
///
/// A name that is not a version at all sorts below every name that is, rather than being left
/// out, because a directory holding a `libgcc.a` is worth looking in whatever it is called.
fn version_key(dir: &Path) -> Vec<u64> {
    let name = dir.file_name().unwrap_or_default().to_string_lossy();
    name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
}

/// Our own runtime library for this target, if it was built.
///
/// Looked for beside the compiler rather than at a path decided when the compiler was built, for
/// the same reason everything else here is looked for: one binary runs wherever it is copied. A
/// `-B` prefix is asked first, because that is what a `-B` prefix is for.
#[must_use]
pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
    // The name from the crate that puts it on a line, rather than a second spelling of it here,
    // which is what that constant asks of anybody who needs the name.
    const NAME: &str = rucc_sysroot::link::BUILTINS;
    let triple = target.to_string();
    let mut places: Vec<PathBuf> = Vec::new();
    for prefix in prefixes {
        places.push(prefix.join(&triple).join(NAME));
        places.push(prefix.join(NAME));
    }
    if let Some(dir) =
        std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
    {
        // An install: the compiler in `bin` and its runtime in `lib/rucc/<triple>`.
        if let Some(up) = dir.parent() {
            places.push(up.join("lib").join("rucc").join(&triple).join(NAME));
            // A build tree: the compiler in `target/release` and the runtime, which is built for
            // the target and not the host, in `target/<triple>/release`.
            for profile in ["release", "debug"] {
                places.push(up.join(&triple).join(profile).join(NAME));
            }
        }
        places.push(dir.join(NAME));
    }
    places.into_iter().find(|path| path.is_file())
}

/// Which output format this `ld` should write, in the name `ld` knows it by.
fn emulation(target: Triple) -> &'static str {
    match target.arch {
        Arch::X86_64 => "elf_x86_64",
        Arch::Aarch64 => "aarch64linux",
        Arch::Riscv64 => "elf64lriscv",
    }
}

/// The program that starts a dynamically linked program, whose path is part of the file.
///
/// It is a per-target constant rather than something to look for, because the name is fixed by
/// the platform's ABI and a program naming a different one does not start.
fn loader(target: Triple) -> &'static str {
    match (target.arch, target.env) {
        (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
        (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
        (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
        (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
        (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
        (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
    }
}

/// Where the library's own files might be, in search order.
///
/// The multiarch directory first for the reason it comes first in the header search: it is where
/// a distribution that can hold two architectures at once puts the one being asked for, and a
/// distribution that cannot simply does not have it. `lib64` after it, which is what the
/// distributions that split by word size use instead, and `lib` last, which is every other one.
#[must_use]
pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
    let multiarch = multiarch(target);
    [
        format!("/usr/lib/{multiarch}"),
        format!("/lib/{multiarch}"),
        "/usr/lib64".to_owned(),
        "/lib64".to_owned(),
        "/usr/lib".to_owned(),
        "/lib".to_owned(),
    ]
    .into_iter()
    .map(|dir| under(sysroot, &dir))
    .collect()
}

/// The name a distribution that holds two architectures at once files this target under.
///
/// `x86_64-linux-gnu` and its friends, which is what `gcc -print-multiarch` prints and what a
/// build system pastes into a path when it is looking for a library itself.
#[must_use]
pub fn multiarch(target: Triple) -> String {
    let libc = match target.env {
        Env::Musl => "musl",
        Env::None | Env::Gnu | Env::Msvc => "gnu",
    };
    format!("{}-linux-{libc}", target.arch.as_str())
}

/// The candidates that are there.
fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
    candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
}

/// Where a library is looked for, in the order it is looked for in.
///
/// The command line first and the target's own after it, which is the order the linker is handed
/// and therefore the order `-print-search-dirs` has to print.
#[must_use]
pub fn search_dirs(link: &LinkOptions, target: Triple) -> Vec<PathBuf> {
    let mut dirs = link.search.clone();
    // A cross link searches one directory and it is the sysroot's, so this is that and not the
    // machine's. What `-print-search-dirs` says is what a build system pastes into a link line of its
    // own, and an answer that named `/usr/lib` for a target whose link line never goes near it would
    // be worse than no answer at all.
    if let Some(sysroot) = cross_sysroot(target, link) {
        dirs.push(sysroot.lib());
        return dirs;
    }
    dirs.extend(candidates(target, link.sysroot.as_deref()));
    dirs
}

/// The full path of a file with that name, when one of the search directories holds it.
///
/// What `-print-file-name=` answers. GCC prints the name back unchanged when it finds nothing,
/// which is what makes the flag safe to paste into a link line either way.
#[must_use]
pub fn find_in_search(link: &LinkOptions, target: Triple, name: &str) -> Option<PathBuf> {
    find_file(&search_dirs(link, target), name)
}

/// The first of those directories holding a file of that name.
fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
    dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
}

/// A path under the sysroot, when there is one.
fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
    match sysroot {
        // `strip_prefix` because joining an absolute path replaces the root rather than extending
        // it, which would make every entry the unprefixed one.
        Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
        None => PathBuf::from(path),
    }
}

/// A path on the machine that will run the program, rather than on the one compiling it.
///
/// Written with the separator of the target and not of the host, which matters for the one path
/// that is not looked at here but stored in the file and read by something else later: the loader
/// a dynamic program names. A Windows host joining it would put a backslash in the middle of a
/// name that a Linux loader has to find, and the program would not start.
fn target_path(sysroot: Option<&Path>, path: &str) -> String {
    match sysroot {
        Some(root) => {
            let root = root.display().to_string();
            format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
        }
        None => path.to_owned(),
    }
}

/// The whole invocation as one line, quoted the way `-###` prints it.
#[must_use]
pub fn render(linker: &Linker, args: &[String]) -> String {
    let mut out = linker.path.display().to_string();
    for arg in args {
        out.push(' ');
        if arg.is_empty() || arg.contains(char::is_whitespace) {
            out.push('"');
            out.push_str(arg);
            out.push('"');
        } else {
            out.push_str(arg);
        }
    }
    out
}

/// Runs the linker and waits for it.
///
/// # Errors
///
/// [`Error::Spawn`] when it could not be started, which is a machine problem, and
/// [`Error::Refused`] when it ran and said no, which is a program problem and one the linker has
/// already explained on its own error output.
pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
    let args: Vec<OsString> = args.iter().map(OsString::from).collect();
    let status = Command::new(&linker.path).args(&args).status().map_err(|why| Error::Spawn {
        path: linker.path.display().to_string(),
        why: why.to_string(),
    })?;
    if status.success() {
        return Ok(());
    }
    // Nothing is added to what the linker printed. It has already named the symbol or the file,
    // and a second message from here saying that linking failed would only push the first one
    // further up the screen.
    Err(Error::Refused {
        status: match status.code() {
            Some(code) => format!("exited with status {code}"),
            None => "was killed before it finished".to_owned(),
        },
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    fn linux() -> Triple {
        Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
    }

    fn one(name: &str) -> Vec<Item> {
        vec![Item::File(name.to_owned())]
    }

    #[test]
    fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
        let names = order(linux(), &LinkOptions::default());
        assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
        assert_eq!(names.last().map(String::as_str), Some("ld"));
    }

    #[test]
    fn naming_one_is_the_whole_of_the_order() {
        let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
        assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
    }

    #[test]
    fn a_dynamic_program_names_the_loader_that_will_start_it() {
        let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
        assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
    }

    #[test]
    fn a_static_program_names_no_loader_because_nothing_will_start_it() {
        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
        assert!(args.contains(&"-static".to_owned()), "{args:?}");
        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
    }

    #[test]
    fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
        let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
        let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
        let named = |opts: &LinkOptions| {
            line(linux(), opts, &one("a.o"), "a.out")
                .expect("a line")
                .iter()
                .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
                .find(|n| n.ends_with("crt1.o"))
        };
        // Only when the machine running this has them, which is what makes this two assertions
        // rather than one: a machine with no glibc development files has neither to find.
        if let Some(name) = named(&moving) {
            assert_eq!(name, "Scrt1.o");
            assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
        }
    }

    /// A profiled program is started by a startup file of its own.
    ///
    /// The counts it keeps have to be started before `main` runs and written out after it returns,
    /// and what does both is this file rather than anything the compiler wrote. So a build that
    /// compiles with the flag and links without it produces a program that calls the hook on every
    /// function and never writes a profile, which is the failure this is here to keep out.
    ///
    /// A shared object takes none of them either way, since nothing starts one.
    #[test]
    fn a_profiled_program_is_started_by_the_startup_file_that_counts() {
        let profile = LinkOptions { profile: true, ..LinkOptions::default() };
        assert_eq!(startfile(&profile, false), Some("gcrt1.o"));
        assert_eq!(startfile(&profile, true), Some("gcrt1.o"));
        let still = LinkOptions { is_static: true, ..profile.clone() };
        assert_eq!(startfile(&still, true), Some("grcrt1.o"));
        assert_eq!(startfile(&still, false), Some("gcrt1.o"));
        let shared = LinkOptions { shared: true, ..profile };
        assert_eq!(startfile(&shared, false), None);
    }

    /// And a program that is not profiled is started by the one it always was.
    #[test]
    fn a_program_that_is_not_profiled_is_started_by_the_usual_one() {
        let plain = LinkOptions::default();
        assert_eq!(startfile(&plain, false), Some("crt1.o"));
        assert_eq!(startfile(&plain, true), Some("Scrt1.o"));
    }

    #[test]
    fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
        let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
        assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
        // And still links against the library, because that is the other flag.
        assert!(args.contains(&"-lc".to_owned()), "{args:?}");
    }

    #[test]
    fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
        let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
    }

    #[test]
    fn the_library_comes_after_the_objects_that_need_it() {
        let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
        let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
        let obj = args.iter().position(|a| a == "a.o").expect("the object");
        let m = args.iter().position(|a| a == "-lm").expect("the library");
        let c = args.iter().position(|a| a == "-lc").expect("the library");
        assert!(obj < m && m < c, "{args:?}");
    }

    #[test]
    fn what_the_user_told_the_linker_stays_where_the_user_wrote_it() {
        // The pair libtool writes around a set of convenience archives, which is what found this.
        // Both words are about the files between them, so a line that collects them and puts them
        // at the end has two options that do nothing and an archive whose members were all dropped.
        let items = vec![
            Item::File("a.o".to_owned()),
            Item::Linker("--whole-archive".to_owned()),
            Item::File("libaesni.a".to_owned()),
            Item::Linker("--no-whole-archive".to_owned()),
            Item::Library("m".to_owned()),
        ];
        let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
        let at = |what: &str| args.iter().position(|a| a == what).expect(what);
        assert!(at("a.o") < at("--whole-archive"), "{args:?}");
        assert!(at("--whole-archive") < at("libaesni.a"), "{args:?}");
        assert!(at("libaesni.a") < at("--no-whole-archive"), "{args:?}");
        assert!(at("--no-whole-archive") < at("-lm"), "{args:?}");
        assert!(at("-lm") < at("-lc"), "{args:?}");
    }

    #[test]
    fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
        let opts = LinkOptions {
            sysroot: Some(PathBuf::from("/nowhere-at-all")),
            search: vec![PathBuf::from("/opt/mine")],
            ..LinkOptions::default()
        };
        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
        assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
        assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
    }

    #[test]
    fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
        for triple in [
            Triple::new(Arch::X86_64, Os::Darwin, Env::Gnu),
            Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
        ] {
            let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
                .expect_err("no line for it");
            assert!(matches!(error, Error::Target { .. }), "{error:?}");
        }
    }

    #[test]
    fn the_line_is_printed_the_way_it_would_be_typed() {
        let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
        let args = ["-o".to_owned(), "a b".to_owned()];
        assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
    }

    #[test]
    fn a_linker_that_is_not_there_is_said_by_name() {
        let opts = LinkOptions {
            use_ld: Some("a-linker-nobody-has".to_owned()),
            ..LinkOptions::default()
        };
        let error = find(linux(), &opts).expect_err("not on this machine");
        assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
    }
    /// A directory with a `libgcc.a` in it, so a test can say what a machine with a gcc on it
    /// looks like without needing one.
    fn a_gcc_dir(name: &str) -> PathBuf {
        let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
        fs::create_dir_all(&dir).expect("a temporary directory");
        fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
        dir
    }

    #[test]
    fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
        let gcc = a_gcc_dir("order");
        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
        let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
        // glibc's `memcpy` is assembly per microarchitecture and ours is a word at a time loop,
        // so on a target that has one, its is the one that should answer.
        assert!(at_libc < at_ours, "{args:?}");
    }

    #[test]
    fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
        let gcc = a_gcc_dir("group");
        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
        let args = runtime_items(&opts, &[gcc], None);
        assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
        assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
        // The unwinder, which is what `libc.a` refers to and what a static link fails on without
        // it. Issue #277.
        assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
    }

    #[test]
    fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
        let gcc = a_gcc_dir("dynamic");
        let args = runtime_items(&LinkOptions::default(), &[gcc], None);
        assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
        assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
        let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
        assert_eq!(args[at - 1], "--as-needed", "{args:?}");
        assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
    }

    #[test]
    fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
        let gcc = a_gcc_dir("ours");
        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
        let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
        assert!(at_ours < at_gcc, "{args:?}");
    }

    #[test]
    fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
        let gcc = a_gcc_dir("theirs");
        let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
        assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
        // And the machine's half is still decided the same way it was, from the directories
        // that are there, which on the machine running this test may be none.
        assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
    }

    #[test]
    fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
        let gcc = a_gcc_dir("none");
        let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
        assert!(runtime_items(&opts, &[gcc], None).is_empty());
    }

    #[test]
    fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
        let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
        let args = runtime_items(&LinkOptions::default(), &[empty], None);
        assert_eq!(args, ["-lc"], "{args:?}");
    }

    #[test]
    fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
        assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
        assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
        // Something that is not a version at all still sorts, and sorts below one that is.
        assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
    }

    /// A command line that has a cache to find generated sysroots in, which a real one always has.
    ///
    /// And a `-B` prefix with our runtime in it, because a cross link refuses without one and
    /// every machine that does this for real has the archive `cargo xtask builtins` wrote. What
    /// happens when it is missing is its own test below.
    fn cached() -> LinkOptions {
        LinkOptions {
            cache: Some(PathBuf::from("/cache")),
            prefixes: vec![a_builtins_dir()],
            ..LinkOptions::default()
        }
    }

    /// A directory with our runtime archive in it, so that a test can say what a machine where the
    /// runtime was built looks like without building one.
    ///
    /// One directory for every test rather than one each, since none of them writes to it and the
    /// name of the file is the whole of what they read.
    fn a_builtins_dir() -> PathBuf {
        let dir = std::env::temp_dir().join(format!("rucc-link-ours-{}", std::process::id()));
        fs::create_dir_all(&dir).expect("a temporary directory");
        fs::write(dir.join("librucc_builtins.a"), b"not really an archive").expect("a file in it");
        dir
    }

    /// Where that cache would keep this target's sysroot.
    fn a_sysroot(target: Triple) -> Sysroot {
        Sysroot::in_cache(Path::new("/cache"), target.tuple())
    }

    /// A target that is not the machine running this test, whatever machine that is.
    ///
    /// A freestanding one, because [`Triple::host`] answers Linux, Darwin or Windows and never
    /// `Os::None`. Every other triple is somebody's host, so a test that wants the cross path out of
    /// [`line`] itself has to use this one and the rest go through [`cross_line`].
    fn foreign() -> Triple {
        Triple::new(Arch::X86_64, Os::None, Env::None)
    }

    #[test]
    fn a_cross_link_reads_the_targets_own_sysroot_and_nothing_of_this_machine() {
        let target = Triple::new(Arch::Aarch64, Os::Linux, Env::Musl);
        let sysroot = a_sysroot(target);
        // The paths as this host spells them, because what is being checked is which directory the
        // files are in and a Windows separator is a backslash.
        let root = sysroot.root().display().to_string();
        let lib = sysroot.lib();
        let args = cross_line(target, &cached(), &one("a.o"), "a.out", &sysroot).expect("a line");
        assert!(args.contains(&format!("--sysroot={root}")), "{args:?}");
        assert!(args.contains(&format!("-L{}", lib.display())), "{args:?}");
        assert!(args.contains(&lib.join("libc.a").display().to_string()), "{args:?}");
        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the loader");
        assert_eq!(args[at + 1], "/lib/ld-musl-aarch64.so.1", "{args:?}");
        // The whole point of the other path not being taken: not one directory of this machine is
        // on the line, so the line is the same on every host and the recorded ones describe it.
        for arg in &args {
            assert!(!arg.contains("/usr/lib"), "{arg} in {args:?}");
            assert!(!arg.contains("/lib64"), "{arg} in {args:?}");
        }
    }

    #[test]
    fn a_freestanding_target_links_against_our_runtime_instead_of_being_refused() {
        let args = line(foreign(), &cached(), &one("a.o"), "a.out").expect("a line");
        assert!(args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
        // No libc, because there is not one, and no start file either: what runs before `main` on a
        // freestanding target comes from whatever is being built.
        assert!(!args.iter().any(|a| a.ends_with("libc.a")), "{args:?}");
        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
    }

    /// And with nothing to find sysroots in it is refused, which is what it was before this.
    #[test]
    fn a_driver_with_no_cache_to_look_in_says_so_rather_than_guessing() {
        let error = line(foreign(), &LinkOptions::default(), &one("a.o"), "a.out")
            .expect_err("no line for it");
        assert!(matches!(error, Error::Target { .. }), "{error:?}");
    }

    #[test]
    fn a_static_link_against_a_libc_that_is_a_stub_is_refused_rather_than_attempted() {
        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
        let opts = LinkOptions { is_static: true, ..cached() };
        let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
            .expect_err("there is no libc.a in a stub sysroot");
        let Error::Cross { why } = &error else { panic!("{error:?}") };
        // Because a stub carries the names a library exports and none of the bodies, which is
        // everything a dynamic link reads and nothing a static one does.
        assert!(why.contains("stub"), "{why}");
    }

    #[test]
    fn a_target_whose_linker_wants_a_different_line_is_refused_by_name() {
        for target in [
            Triple::new(Arch::Aarch64, Os::Darwin, Env::None),
            Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
        ] {
            let error = cross_line(target, &cached(), &one("a.o"), "a.out", &a_sysroot(target))
                .expect_err("no line for that format");
            let Error::Cross { why } = &error else { panic!("{error:?}") };
            assert!(why.contains(&target.tuple().to_canonical_string()), "{why}");
        }
    }

    #[test]
    fn a_mingw_target_links_and_looks_for_a_linker_that_can_write_a_pe_image() {
        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
        let args = cross_line(target, &cached(), &one("a.o"), "a.exe", &a_sysroot(target))
            .expect("a line for mingw-w64");
        let at = |flag: &str| args.iter().position(|arg| arg == flag).expect(flag);
        assert_eq!(args[at("-m") + 1], "i386pep");
        assert_eq!(args[at("--subsystem") + 1], "console");
        assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
        // And the prefixed name a distribution files its mingw binutils under, which is not the
        // multiarch one.
        let names = cross_order(target);
        assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
        assert!(names.contains(&"x86_64-w64-mingw32-ld".to_owned()), "{names:?}");
    }

    #[test]
    fn our_runtime_comes_from_beside_the_compiler_rather_than_from_inside_the_sysroot() {
        // The two halves of tamnd/rucc#1514. The line used to name it under the sysroot's `lib`,
        // where nothing ever put it: it is this compiler's output for the target and a sysroot
        // fetched from a release holds the platform's files and not ours. So the path on the line
        // is the one the driver found, and the only `librucc_builtins.a` on the line is that one.
        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
        let sysroot = a_sysroot(target);
        let opts = cached();
        let args = cross_line(target, &opts, &one("a.o"), "a.exe", &sysroot).expect("a line");
        let ours: Vec<&String> =
            args.iter().filter(|arg| arg.ends_with("librucc_builtins.a")).collect();
        assert_eq!(ours.len(), 1, "{args:?}");
        assert_eq!(ours[0], &opts.prefixes[0].join("librucc_builtins.a").display().to_string());
        assert!(!ours[0].starts_with(&sysroot.lib().display().to_string()), "{args:?}");
        // And it is still last, after everything that calls into it.
        assert_eq!(args.last(), Some(ours[0]), "{args:?}");
    }

    #[test]
    fn a_cross_link_with_no_runtime_to_find_says_which_command_writes_one() {
        // What the linker would say instead is that `___chkstk_ms` is undefined, referenced from
        // mingw-w64's own objects, which is tamnd/rucc#1513 and is nobody's first guess at a
        // missing archive.
        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
        let opts = LinkOptions { prefixes: Vec::new(), ..cached() };
        let error = cross_line(target, &opts, &one("a.o"), "a.exe", &a_sysroot(target))
            .expect_err("there is no runtime for it to find");
        let Error::Cross { why } = &error else { panic!("{error:?}") };
        assert!(why.contains("cargo xtask builtins"), "{why}");
        assert!(why.contains("-fno-builtins-lib"), "{why}");

        // And that flag is the way through it, for somebody who meant to link without ours.
        let without = LinkOptions { no_builtins_lib: true, ..opts };
        let args = cross_line(target, &without, &one("a.o"), "a.exe", &a_sysroot(target))
            .expect("a line without ours on it");
        assert!(!args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
    }

    #[test]
    fn the_version_an_lld_prints_is_read_and_nothing_elses_is() {
        // What each of these programs actually prints, because the word being in the line is the
        // whole of how one is told from another.
        assert_eq!(lld_major("LLD 18.1.8 (compatible with GNU linkers)\n"), Some(18));
        assert_eq!(lld_major("Ubuntu LLD 18.1.3 (compatible with GNU linkers)\n"), Some(18));
        assert_eq!(lld_major("LLD 20.1.2 (compatible with GNU linkers)\n"), Some(20));

        // Binutils and mold do not have it, and neither of them has this problem, so the answer
        // for both is that this check has nothing to say about them.
        assert_eq!(lld_major("GNU ld (GNU Binutils for Ubuntu) 2.42\n"), None);
        assert_eq!(lld_major("mold 2.4.1 (compatible with GNU ld)\n"), None);
        assert_eq!(lld_major(""), None);
    }

    /// A program that prints `text` and exits, which is as much of a linker as this check reads.
    ///
    /// Named after what it says, so that two of them in one test are two files.
    #[cfg(unix)]
    fn a_linker_that_says(tag: &str, text: &str) -> Linker {
        use std::os::unix::fs::PermissionsExt as _;
        let dir = std::env::temp_dir().join(format!("rucc-link-ld-{}", std::process::id()));
        fs::create_dir_all(&dir).expect("a temporary directory");
        let path = dir.join(format!("ld.lld-{tag}"));
        fs::write(&path, format!("#!/bin/sh\necho '{text}'\n")).expect("a script");
        fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("an executable one");
        Linker { name: "ld.lld".to_owned(), path }
    }

    #[test]
    #[cfg(unix)]
    fn an_lld_too_old_to_read_exportas_is_refused_for_windows_gnu_and_nowhere_else() {
        // The failure this replaces has no diagnostic at all: 18 writes an import by ordinal zero,
        // exits successfully, and the program dies at startup under wine. tamnd/rucc#1515.
        let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
        let old = a_linker_that_says("18", "LLD 18.1.8 (compatible with GNU linkers)");
        let error = suitable(windows, &old).expect_err("18 cannot link this");
        let Error::TooOld { name, found, target } = &error else { panic!("{error:?}") };
        assert_eq!((name.as_str(), *found, target.as_str()), ("ld.lld", 18, "x86_64-windows-gnu"));
        assert!(error.to_string().contains("IMPORT_NAME_EXPORTAS"), "{error}");

        // The same linker for a target whose import libraries have no such records in them, which
        // is every other target, since this is one encoding in one format.
        let linux = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
        assert_eq!(suitable(linux, &old), Ok(()));

        // And the first one that reads them.
        let new = a_linker_that_says("19", "LLD 19.1.0 (compatible with GNU linkers)");
        assert_eq!(suitable(windows, &new), Ok(()));
    }

    #[test]
    #[cfg(unix)]
    fn a_linker_that_will_not_say_what_it_is_is_left_alone() {
        // Every linker that is not an lld reaches this check too, and what it can establish is
        // that a specific old lld is here rather than that anything else is fit. Turning "I did
        // not recognise this" into a refusal would break machines this problem never touched.
        let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
        let quiet = a_linker_that_says("gnu", "GNU ld (GNU Binutils for Ubuntu) 2.42");
        assert_eq!(suitable(windows, &quiet), Ok(()));

        let missing = Linker { name: "ld.lld".to_owned(), path: PathBuf::from("/no/such/linker") };
        assert_eq!(suitable(windows, &missing), Ok(()));
    }

    #[test]
    fn profiling_a_cross_link_is_refused_because_the_startup_file_is_compiled_code() {
        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
        let opts = LinkOptions { profile: true, ..cached() };
        let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
            .expect_err("there is no gcrt1.o in a generated sysroot");
        let Error::Cross { why } = &error else { panic!("{error:?}") };
        assert!(why.contains("gcrt1.o"), "{why}");
    }

    #[test]
    fn the_host_takes_the_host_line_and_a_tree_the_user_named_takes_it_too() {
        let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
        let other = Triple::new(Arch::Riscv64, Os::Linux, Env::Musl);
        assert!(cross_for(host, &cached(), Some(host)).is_none());
        assert!(cross_for(other, &cached(), Some(host)).is_some());
        // A tree somebody assembled and named is what `--sysroot` has always meant here, and the
        // native line prefixes every path it decides with it.
        let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..cached() };
        assert!(cross_for(other, &named, Some(host)).is_none());
        // A host this compiler cannot name is a host whose directories it should not be guessing at.
        assert!(cross_for(other, &cached(), None).is_some());
    }

    #[test]
    fn a_pinned_release_on_this_machines_own_target_is_a_cross_compile() {
        // The case that used to be dropped on the floor. `--target=x86_64-linux-gnu.2.28` on an
        // x86-64 glibc machine read that machine's headers and linked that machine's libc, and the
        // release reached nothing, so what came out was a binary for whatever release the build
        // machine happened to have. A pin is the one thing a person writes to say otherwise.
        let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
        let pinned = LinkOptions {
            pinned: Some(
                "x86_64-linux-gnu.2.28".parse::<TargetTuple>().expect("a spelling with a release"),
            ),
            ..cached()
        };
        let at = cross_for(host, &pinned, Some(host)).expect("a pin is a cross compile");
        // And against the release's own directory, because the release is in the cache key: a tree
        // produced for 2.28 and a tree produced for 2.44 are two trees and the path has to say which.
        assert!(at.root().ends_with("x86_64-linux-gnu.2.28"), "{:?}", at.root());
        // The release is the whole of the difference. The same command line without it is this
        // machine, which is what every native compile has always been.
        let bare = LinkOptions { pinned: None, ..cached() };
        assert!(cross_for(host, &bare, Some(host)).is_none());
    }

    #[test]
    fn what_a_cross_link_searches_is_the_sysroot_and_not_this_machine() {
        let dirs = search_dirs(&cached(), foreign());
        // One directory, because that is what the line has, and the same one the line has, because
        // `-print-search-dirs` is what a build system reads to write a link line of its own.
        assert_eq!(dirs.len(), 1, "{dirs:?}");
        assert!(dirs[0].starts_with("/cache"), "{dirs:?}");
        assert!(dirs[0].ends_with("lib"), "{dirs:?}");
        // And what the user wrote still comes first, the way it does on the line itself.
        let mine = LinkOptions { search: vec![PathBuf::from("/opt/mine")], ..cached() };
        assert_eq!(search_dirs(&mine, foreign())[0], PathBuf::from("/opt/mine"));
    }

    #[test]
    fn the_linker_looked_for_on_a_cross_link_is_one_that_can_cross() {
        let names = cross_order(Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu));
        assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
        assert!(names.contains(&"aarch64-linux-gnu-ld".to_owned()), "{names:?}");
        // mold links for the machine it is running on, and so does a distribution's own `ld`, so
        // neither is a default here. `-fuse-ld=` is still there for somebody whose is different.
        assert!(!names.iter().any(|name| name.contains("mold")), "{names:?}");
        assert!(!names.contains(&"ld".to_owned()), "{names:?}");
        // And the lookup the driver really does for a target that is not this machine.
        assert_eq!(order(foreign(), &cached()), ["ld.lld", "lld"]);
    }

    #[test]
    fn the_four_flags_become_the_five_modes_they_describe() {
        let plain = LinkOptions::default();
        assert_eq!(mode(&plain), LinkMode::Dynamic);
        assert_eq!(
            mode(&LinkOptions { pie: Some(false), ..plain.clone() }),
            LinkMode::DynamicNoPie
        );
        assert_eq!(mode(&LinkOptions { is_static: true, ..plain.clone() }), LinkMode::Static);
        let both = LinkOptions { is_static: true, pie: Some(true), ..plain.clone() };
        assert_eq!(mode(&both), LinkMode::StaticPie);
        assert_eq!(mode(&LinkOptions { shared: true, ..plain }), LinkMode::Shared);
    }

    #[test]
    fn a_sysroot_that_has_not_been_built_is_named_before_anything_is_compiled() {
        let opts = LinkOptions {
            cache: Some(std::env::temp_dir().join("rucc-a-cache-nobody-filled")),
            ..LinkOptions::default()
        };
        let error = preflight(foreign(), &opts).expect_err("nothing has built one");
        let Error::Sysroot { dir, pinned, .. } = &error else { panic!("{error:?}") };
        assert!(dir.ends_with("x86_64-none"), "{dir}");
        // Nothing is pinned for that target, or for any target yet, so the message says that rather
        // than naming a command that would not work.
        assert!(!pinned, "nothing should be pinned for a bare metal target");
        let said = error.to_string();
        assert!(said.contains("pins none for it to fetch"), "{said}");
    }

    /// The other half of the same message, which is what a target this release does pin an artifact
    /// for is told. Built by hand rather than through `preflight`, because what is being checked is
    /// the message and not which targets `rucc_sysroot::artifact` happens to pin this release.
    #[test]
    fn a_sysroot_that_could_be_fetched_is_told_what_to_run() {
        let said = Error::Sysroot {
            target: "x86_64-linux-musl".to_owned(),
            dir: "/somewhere/sysroots/x86_64-linux-musl".to_owned(),
            pinned: true,
        }
        .to_string();
        assert!(said.contains("`rucc --fetch x86_64-linux-musl`"), "{said}");
        // And the other way out of it, because a person who has a tree already does not want a
        // download.
        assert!(said.contains("--sysroot=<dir>"), "{said}");
    }

    #[test]
    fn a_link_against_this_machine_has_nothing_to_check_before_it_starts() {
        // Its directories are looked for as the line is built, and one that is not there is simply
        // one that is not offered, so there is no question to answer early.
        assert!(preflight(linux(), &LinkOptions::default()).is_ok());
    }

    #[test]
    fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
        let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
        assert!(dirs.is_empty(), "{dirs:?}");
    }
}