rucc_sysroot/wall.rs
1//! The two targets whose system headers are somebody else's to license.
2//!
3//! Design: `spec/cross-compile/13-distribution.md` section 13.4 and
4//! `spec/cross-compile/08-sysroots.md` section 8.6.
5//!
6//! Almost everything a target needs from us is ours to ship. glibc's headers are LGPL, musl's are
7//! MIT, mingw-w64's are permissive, the kernel's come with the system call note that says a program
8//! using the interface is not covered by the GPL, and the rest of section 8.2's table is under a BSD
9//! licence. Two rows are not. Apple's SDK is under the Xcode licence, which limits its use to Apple
10//! branded hardware, and Microsoft's Windows SDK and universal CRT are not redistributable at all.
11//!
12//! So for those two there is no tree we may bundle, no artifact a release may pin, and nothing to
13//! download on somebody's behalf, and that is a different situation from a tree that has not been
14//! built yet. A compiler that said "there is no sysroot for this target" would send a person looking
15//! for a command that will never exist, so the answer names the licence and the lawful ways to get
16//! what is behind it.
17//!
18//! # Why this is an enum and not a flag
19//!
20//! Because the two walls bound different things for the person who hit one. A Windows program has a
21//! fully redistributable alternative, which is mingw-w64, and a build for that environment needs
22//! nothing installed at all, which is why the default Windows environment for a cross build is `gnu`.
23//! A macOS program has no alternative: the two lawful ways to get the SDK are to compile on a mac,
24//! where the installed one is found by asking `xcrun`, or to download Xcode yourself under its
25//! licence, and both of them end at a path that somebody has to name. The Windows one is found the
26//! same way on its own platform, by asking the installer that put it there rather than by looking in
27//! a fixed place, because neither the toolchain nor the kit is in one.
28//!
29//! # What a wall is not
30//!
31//! It is not a statement about the back end. We emit Mach-O and we emit COFF, and section 8.6 calls
32//! that targeting a platform rather than compiling for it. What is missing in both cases is headers
33//! and import libraries, which is why this sits beside the search path rather than anywhere near the
34//! code generator.
35
36use std::fmt;
37
38use rucc_tuple::{Env, Os, TargetTuple};
39
40/// A target whose system headers and link inputs are not ours to distribute.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Wall {
43 /// Apple's, which is macOS and iOS.
44 Apple,
45 /// Microsoft's, which is a Windows target in the MSVC environment and not a mingw-w64 one.
46 Microsoft,
47}
48
49impl Wall {
50 /// The wall this target is behind, or [`None`] for the rest of the table.
51 ///
52 /// The environment decides the Windows answer and the operating system decides the Apple one,
53 /// because the two Windows environments are two different sets of link inputs under two
54 /// different licences while every Apple platform reads one SDK.
55 #[must_use]
56 pub const fn of(target: TargetTuple) -> Option<Self> {
57 match (target.os(), target.env()) {
58 (Os::MacOs | Os::IOs, _) => Some(Wall::Apple),
59 (Os::Windows, Env::Msvc) => Some(Wall::Microsoft),
60 _ => None,
61 }
62 }
63
64 /// What is behind the wall, named the way the platform's own documentation names it.
65 #[must_use]
66 pub const fn sdk(self) -> &'static str {
67 match self {
68 Wall::Apple => "a macOS SDK",
69 Wall::Microsoft => "the Windows SDK and its universal CRT",
70 }
71 }
72
73 /// The licence that puts it there, as a clause a sentence can be built around.
74 #[must_use]
75 pub const fn licence(self) -> &'static str {
76 match self {
77 Wall::Apple => {
78 "it is under Apple's Xcode licence, which limits its use to Apple branded hardware"
79 }
80 Wall::Microsoft => "Microsoft does not allow it to be redistributed",
81 }
82 }
83
84 /// The licence as an identifier rather than as a clause, which is what a record carries.
85 ///
86 /// Two spellings of one fact, and they are both here because a sentence and a field want
87 /// different things. [`Wall::licence`] is what a person is told and reads as English.
88 /// This is what [`crate::distribution`] writes into a column, where a clause would be
89 /// unreadable and a second vocabulary of licence names would be a thing to keep in step with
90 /// [`crate::Licence`].
91 #[must_use]
92 pub const fn under(self) -> crate::Licence {
93 match self {
94 Wall::Apple => crate::Licence::AppleSdk,
95 Wall::Microsoft => crate::Licence::MicrosoftSdk,
96 }
97 }
98
99 /// The lawful ways to get what is behind the wall, which every message here ends with.
100 ///
101 /// A flag in each, because section 8.6's third rule is that the fetch is never automatic and
102 /// never silent, so every one of these paths is a person naming a path once.
103 #[must_use]
104 pub const fn ways(self) -> &'static str {
105 match self {
106 Wall::Apple => {
107 "Compile on a macOS machine, where the installed SDK is found by asking xcrun, or \
108 download Xcode yourself under that licence and name the SDK with -isysroot <dir> \
109 or in SDKROOT"
110 }
111 Wall::Microsoft => {
112 "Build for the mingw-w64 environment instead, which is fully redistributable and \
113 needs nothing installed, or install Visual Studio and the Windows SDK, which are \
114 found without being named on a Windows machine and are what INCLUDE names \
115 anywhere else, or point --sysroot=<dir> at a tree laid out with crt/include and \
116 sdk/include"
117 }
118 }
119 }
120
121 /// Why a compile for `target` has no library headers to read.
122 ///
123 /// The target is spelled by the caller rather than taken from the tuple, so that the message
124 /// says the target the way the command line said it.
125 #[must_use]
126 pub fn no_headers(self, target: &str) -> String {
127 format!(
128 "{target} needs {} to compile against and none of it is on this machine. This compiler \
129 does not ship it and will not download it for you, because {}. {}, or pass -nostdinc \
130 for a program that includes none of the library",
131 self.sdk(),
132 self.licence(),
133 self.ways()
134 )
135 }
136
137 /// Why a fetch cannot get the sysroot for `target`, which no release of this compiler will pin.
138 #[must_use]
139 pub fn no_fetch(self, target: &str) -> String {
140 format!(
141 "there is nothing to fetch for {target} and there never will be. What a program for it \
142 compiles against is {}, and {}, so no release of this compiler pins it. {}",
143 self.sdk(),
144 self.licence(),
145 self.ways()
146 )
147 }
148}
149
150impl fmt::Display for Wall {
151 /// Whose wall it is, for a message that has already said what is behind it.
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 f.write_str(match self {
154 Wall::Apple => "Apple's licence wall",
155 Wall::Microsoft => "Microsoft's licence wall",
156 })
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 fn target(tuple: &str) -> TargetTuple {
165 tuple.parse().expect("a target this understands")
166 }
167
168 #[test]
169 fn the_apple_platforms_are_behind_one_wall_and_the_msvc_environment_behind_the_other() {
170 assert_eq!(Wall::of(target("aarch64-macos")), Some(Wall::Apple));
171 assert_eq!(Wall::of(target("x86_64-macos")), Some(Wall::Apple));
172 assert_eq!(Wall::of(target("aarch64-ios")), Some(Wall::Apple));
173 assert_eq!(Wall::of(target("x86_64-windows-msvc")), Some(Wall::Microsoft));
174 assert_eq!(Wall::of(target("arm64ec-windows-msvc")), Some(Wall::Microsoft));
175 }
176
177 #[test]
178 fn the_windows_target_that_needs_nothing_installed_is_not_behind_a_wall() {
179 // The whole reason the default Windows environment for a cross build is `gnu`. mingw-w64's
180 // headers and import libraries are ours to ship, and the target next to it is not.
181 assert_eq!(Wall::of(target("x86_64-pc-windows-gnu")), None);
182 assert_eq!(Wall::of(target("aarch64-windows-gnu")), None);
183 for tuple in ["x86_64-linux-gnu", "riscv64-linux-musl", "armv7m-none-eabi", "wasm32-wasi"] {
184 assert_eq!(Wall::of(target(tuple)), None, "{tuple}");
185 }
186 }
187
188 #[test]
189 fn the_licence_behind_each_wall_is_the_one_that_is_never_redistributable() {
190 for wall in [Wall::Apple, Wall::Microsoft] {
191 assert!(!wall.under().redistributable(), "{wall}");
192 }
193 assert_eq!(Wall::Apple.under().as_str(), "apple-sdk");
194 assert_eq!(Wall::Microsoft.under().as_str(), "microsoft-sdk");
195 }
196
197 #[test]
198 fn each_message_names_the_target_the_licence_and_a_way_out() {
199 let said = Wall::Apple.no_headers("aarch64-macos");
200 assert!(said.contains("aarch64-macos"), "{said}");
201 assert!(said.contains("Xcode licence"), "{said}");
202 assert!(said.contains("-isysroot"), "{said}");
203 // And the escape hatch for a program that reads no library headers at all, which is what
204 // section 8.6 means by being able to target the platform without the SDK.
205 assert!(said.contains("-nostdinc"), "{said}");
206
207 let said = Wall::Microsoft.no_fetch("x86_64-windows-msvc");
208 assert!(said.contains("x86_64-windows-msvc"), "{said}");
209 assert!(said.contains("mingw-w64"), "{said}");
210 // The part that tells this apart from a target whose artifact has not been published yet.
211 assert!(said.contains("there never will be"), "{said}");
212 }
213}