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 run rucc --fetch-msvc-sdk <tuple>, which prints that \
114                 licence and downloads what is behind it once you have accepted it, or install \
115                 Visual Studio and the Windows SDK, which are found without being named on a \
116                 Windows machine and are what INCLUDE names anywhere else, or point \
117                 --sysroot=<dir> at a tree laid out with crt/include and sdk/include"
118            }
119        }
120    }
121
122    /// Why a compile for `target` has no library headers to read.
123    ///
124    /// The target is spelled by the caller rather than taken from the tuple, so that the message
125    /// says the target the way the command line said it.
126    #[must_use]
127    pub fn no_headers(self, target: &str) -> String {
128        format!(
129            "{target} needs {} to compile against and none of it is on this machine. This compiler \
130             does not ship it and never downloads it as part of a compile, because {}. {}, or pass \
131             -nostdinc for a program that includes none of the library",
132            self.sdk(),
133            self.licence(),
134            self.ways()
135        )
136    }
137
138    /// Why a fetch cannot get the sysroot for `target`, which no release of this compiler will pin.
139    #[must_use]
140    pub fn no_fetch(self, target: &str) -> String {
141        format!(
142            "there is nothing to fetch for {target} and there never will be. What a program for it \
143             compiles against is {}, and {}, so no release of this compiler pins it. {}",
144            self.sdk(),
145            self.licence(),
146            self.ways()
147        )
148    }
149}
150
151impl fmt::Display for Wall {
152    /// Whose wall it is, for a message that has already said what is behind it.
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        f.write_str(match self {
155            Wall::Apple => "Apple's licence wall",
156            Wall::Microsoft => "Microsoft's licence wall",
157        })
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    fn target(tuple: &str) -> TargetTuple {
166        tuple.parse().expect("a target this understands")
167    }
168
169    #[test]
170    fn the_apple_platforms_are_behind_one_wall_and_the_msvc_environment_behind_the_other() {
171        assert_eq!(Wall::of(target("aarch64-macos")), Some(Wall::Apple));
172        assert_eq!(Wall::of(target("x86_64-macos")), Some(Wall::Apple));
173        assert_eq!(Wall::of(target("aarch64-ios")), Some(Wall::Apple));
174        assert_eq!(Wall::of(target("x86_64-windows-msvc")), Some(Wall::Microsoft));
175        assert_eq!(Wall::of(target("arm64ec-windows-msvc")), Some(Wall::Microsoft));
176    }
177
178    #[test]
179    fn the_windows_target_that_needs_nothing_installed_is_not_behind_a_wall() {
180        // The whole reason the default Windows environment for a cross build is `gnu`. mingw-w64's
181        // headers and import libraries are ours to ship, and the target next to it is not.
182        assert_eq!(Wall::of(target("x86_64-pc-windows-gnu")), None);
183        assert_eq!(Wall::of(target("aarch64-windows-gnu")), None);
184        for tuple in ["x86_64-linux-gnu", "riscv64-linux-musl", "armv7m-none-eabi", "wasm32-wasi"] {
185            assert_eq!(Wall::of(target(tuple)), None, "{tuple}");
186        }
187    }
188
189    #[test]
190    fn the_licence_behind_each_wall_is_the_one_that_is_never_redistributable() {
191        for wall in [Wall::Apple, Wall::Microsoft] {
192            assert!(!wall.under().redistributable(), "{wall}");
193        }
194        assert_eq!(Wall::Apple.under().as_str(), "apple-sdk");
195        assert_eq!(Wall::Microsoft.under().as_str(), "microsoft-sdk");
196    }
197
198    #[test]
199    fn each_message_names_the_target_the_licence_and_a_way_out() {
200        let said = Wall::Apple.no_headers("aarch64-macos");
201        assert!(said.contains("aarch64-macos"), "{said}");
202        assert!(said.contains("Xcode licence"), "{said}");
203        assert!(said.contains("-isysroot"), "{said}");
204        // And the escape hatch for a program that reads no library headers at all, which is what
205        // section 8.6 means by being able to target the platform without the SDK.
206        assert!(said.contains("-nostdinc"), "{said}");
207
208        let said = Wall::Microsoft.no_fetch("x86_64-windows-msvc");
209        assert!(said.contains("x86_64-windows-msvc"), "{said}");
210        assert!(said.contains("mingw-w64"), "{said}");
211        // The part that tells this apart from a target whose artifact has not been published yet.
212        assert!(said.contains("there never will be"), "{said}");
213    }
214}