Skip to main content

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}