Skip to content

Commit f22c389

Browse files
authored
Rollup merge of rust-lang#131477 - madsmtm:sdkroot-via-env-var, r=nnethercote
Apple: Always pass SDK root when linking with `cc`, and pass it via `SDKROOT` env var Fixes rust-lang#80817, fixes rust-lang#96943, and generally simplifies our linker invocation on Apple platforms. Part of rust-lang#129432. ### Necessary background on trampoline binaries The developer binaries such as `/usr/bin/cc` and `/usr/bin/clang` are actually trampolines (similar in spirit to the Rust binaries in `~/.cargo/bin`) which effectively invokes `xcrun` to get the current Xcode developer directory, which allows it to find the actual binary under `/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/*`. This binary is then launched with the following environment variables set (but none of them are set if `SDKROOT` is set explicitly): - `SDKROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk` - `LIBRARY_PATH=/usr/local/lib` (appended) - `CPATH=/usr/local/include` (appended) - `MANPATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/share/man:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/usr/share/man:/Applications/Xcode.app/Contents/Developer/usr/share/man:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/share/man:` (prepended) This allows the user to type e.g. `clang foo.c` in their terminal on macOS, and have it automatically pick up a suitable Clang binary and SDK from either an installed Xcode.app or the Xcode Command Line Tools. (It acts roughly as-if you typed `xcrun -sdk macosx clang foo.c`). ### Finding a suitable SDK All compilation on macOS is cross-compilation using SDKs, there are no system headers any more (`/usr/include` is gone), and the system libraries are elsewhere in the file system (`/usr/lib` is basically empty). Instead, the logic for finding the SDK is handled by the `/usr/bin/cc` trampoline (see above). But relying on the `cc` trampoline doesn't work when: - Cross-compiling, since a different SDK is needed there. - Invoking the linker directly, since the linker doesn't understand `SDKROOT`. - Linking build scripts inside Xcode (see rust-lang#80817), since Xcode prepends `/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin` to `PATH`, which means `cc` refers to the _actual_ Clang binary, and we end up with the wrong SDK root specified. Basically, we cannot rely on the trampoline at all, so the last commit removes the special-casing that was done when linking with `cc` for macOS (i.e. the most common path), so that **we now always invoke `xcrun` (if `SDKROOT` is not explicitly specified) to find the SDK root**. Making sure this is non-breaking has a few difficulties though, namely that the user might not have Xcode installed, and that the compiler driver may not understand the `-isysroot` flag. These difficulties are explored below. #### No Xcode There are several compiler drivers which work without Xcode by bundling their own SDK, including `zig cc`, Nixpkgs' `clang` and Homebrew's `llvm` package. Additionally, `xcrun` is rarely available when cross-compiling from non-macOS and instead the user might provide a downloaded SDK manually with `-Clink-args=...`. We do still want to _try_ to invoke `xcrun` if possible, since it is usually the SDK that the user wants (and if not, the environment should override `xcrun`, such as is done by Nixpkgs). But we do not want failure to invoke `xcrun` to stop the linking process. This is changed in the second-to-last commit. #### `SDKROOT` vs. `-isysroot` The exact reasoning why we do not always pass the SDK root when linking on macOS eludes me (the git history dead ends in rust-lang#100286), but I suspect it's because we want to support compiler drivers which do not support the `-isysroot` option. To make sure that such use-cases continue to work, we now pass the SDK root via the `SDKROOT` environment variable. This way, compiler drivers that support setting the SDK root (such as Clang and GCC) can use it, while compiler drivers that don't (presumably because they figure out the SDK in some other way) can just ignore it. One small danger here would be if there's some compiler driver out there which works with the `-isysroot` flag, but not with the `SDKROOT` environment variable. I am not aware of any? In a sense, this also shifts the blame; if a compiler driver does not understand `SDKROOT`, it won't work with e.g. `xcrun -sdk macosx15.0 $tool` either, so it can more clearly be argued that this is incorrect behaviour on the part of the tool. Note also that this overrides the behaviour discussed above (`/usr/bin/cc` sets some extra environment variables), I will argue that is fine since `MANPATH` and `CPATH` is useless when linking, and `/usr/local/lib` is empty on a default system at least since macOS 10.14 (it might be filled by extra libraries installed by the user, but I'll argue that if we want it to be part of the default library search path, we should set it explicitly so that it's also set when linking with `-Clinker=ld`). ### Considered alternatives - Invoke `/usr/bin/cc` instead of `cc`. - This breaks many other use-cases though where overriding `cc` in the PATH is desired. - Look up `which cc`, and do special logic if in Xcode toolchain. - Seems brittle, and besides, it's not the `cc` in the Xcode toolchain that's wrong, it's the `/usr/bin/cc` behaviour that is a bit too magical. - Invoke `xcrun --sdk macosx cc`. - This completely ignores `SDKROOT`, so we'd still have to parse that first to figure out if it's suitable or not, but would probably be workable. - Maybe somehow configure the linker with extra flags such that it'll be able to link regardless of linking for macOS or e.g. iOS? Though I doubt this is possible. - Bundle the SDK, similar to `zig-cc`. - Comes with it's own host of problems. ### Testing Tested that this works with the following `-Clinker=...`: - [x] Default (`cc`) - [x] `/usr/bin/ld` - [x] Actual Clang from Xcode (`/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang`) - [x] `/usr/bin/clang` (invoked via `clang` instead of `cc`) - [x] Homebrew's `llvm` package (ignores `SDKROOT`, uses their own SDK) - [x] Homebrew's `gcc` package (`SDKROOT` is preferred over their own SDK) - [x] ~Macports `clang`~ Couldn't get it to build - [x] Macports `gcc` (`SDKROOT` is preferred over their own SDK) - [x] Zig CC installed via. homebrew (ignores both `-isysroot` and `SDKROOT`, uses their own SDK) - [x] Nixpkgs `clang` (ignores `SDKROOT`, uses their own SDK) - [x] Nixpkgs `gcc` (ignores `SDKROOT`, uses their own SDK) - [x] ~[`cosmocc`](https://github.com/jart/cosmopolitan)?~ Doesn't accept common flags (like `-arch`) CC ```````@BlackHoleFox``````` ```````@thomcc```````
2 parents a153133 + 1d13162 commit f22c389

File tree

7 files changed

+115
-54
lines changed

7 files changed

+115
-54
lines changed

compiler/rustc_codegen_ssa/messages.ftl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,9 @@ codegen_ssa_version_script_write_failure = failed to write version script: {$err
401401
402402
codegen_ssa_visual_studio_not_installed = you may need to install Visual Studio build tools with the "C++ build tools" workload
403403
404+
codegen_ssa_xcrun_about =
405+
the SDK is needed by the linker to know where to find symbols in system libraries and for embedding the SDK version in the final object file
406+
404407
codegen_ssa_xcrun_command_line_tools_insufficient =
405408
when compiling for iOS, tvOS, visionOS or watchOS, you need a full installation of Xcode
406409

compiler/rustc_codegen_ssa/src/back/apple.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ pub(super) fn add_version_to_llvm_target(
160160
pub(super) fn get_sdk_root(sess: &Session) -> Option<PathBuf> {
161161
let sdk_name = sdk_name(&sess.target);
162162

163+
// Attempt to invoke `xcrun` to find the SDK.
164+
//
165+
// Note that when cross-compiling from e.g. Linux, the `xcrun` binary may sometimes be provided
166+
// as a shim by a cross-compilation helper tool. It usually isn't, but we still try nonetheless.
163167
match xcrun_show_sdk_path(sdk_name, sess.verbose_internals()) {
164168
Ok((path, stderr)) => {
165169
// Emit extra stderr, such as if `-verbose` was passed, or if `xcrun` emitted a warning.
@@ -169,7 +173,19 @@ pub(super) fn get_sdk_root(sess: &Session) -> Option<PathBuf> {
169173
Some(path)
170174
}
171175
Err(err) => {
172-
let mut diag = sess.dcx().create_err(err);
176+
// Failure to find the SDK is not a hard error, since the user might have specified it
177+
// in a manner unknown to us (moreso if cross-compiling):
178+
// - A compiler driver like `zig cc` which links using an internally bundled SDK.
179+
// - Extra linker arguments (`-Clink-arg=-syslibroot`).
180+
// - A custom linker or custom compiler driver.
181+
//
182+
// Though we still warn, since such cases are uncommon, and it is very hard to debug if
183+
// you do not know the details.
184+
//
185+
// FIXME(madsmtm): Make this a lint, to allow deny warnings to work.
186+
// (Or fix <https://github.com/rust-lang/rust/issues/21204>).
187+
let mut diag = sess.dcx().create_warn(err);
188+
diag.note(fluent::codegen_ssa_xcrun_about);
173189

174190
// Recognize common error cases, and give more Rust-specific error messages for those.
175191
if let Some(developer_dir) = xcode_select_developer_dir() {
@@ -209,6 +225,8 @@ fn xcrun_show_sdk_path(
209225
sdk_name: &'static str,
210226
verbose: bool,
211227
) -> Result<(PathBuf, String), XcrunError> {
228+
// Intentionally invoke the `xcrun` in PATH, since e.g. nixpkgs provide an `xcrun` shim, so we
229+
// don't want to require `/usr/bin/xcrun`.
212230
let mut cmd = Command::new("xcrun");
213231
if verbose {
214232
cmd.arg("--verbose");
@@ -280,7 +298,7 @@ fn stdout_to_path(mut stdout: Vec<u8>) -> PathBuf {
280298
}
281299
#[cfg(unix)]
282300
let path = <OsString as std::os::unix::ffi::OsStringExt>::from_vec(stdout);
283-
#[cfg(not(unix))] // Unimportant, this is only used on macOS
284-
let path = OsString::from(String::from_utf8(stdout).unwrap());
301+
#[cfg(not(unix))] // Not so important, this is mostly used on macOS
302+
let path = OsString::from(String::from_utf8(stdout).expect("stdout must be UTF-8"));
285303
PathBuf::from(path)
286304
}

compiler/rustc_codegen_ssa/src/back/link.rs

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3194,39 +3194,60 @@ fn add_apple_link_args(cmd: &mut dyn Linker, sess: &Session, flavor: LinkerFlavo
31943194
}
31953195

31963196
fn add_apple_sdk(cmd: &mut dyn Linker, sess: &Session, flavor: LinkerFlavor) -> Option<PathBuf> {
3197-
let os = &sess.target.os;
3198-
if sess.target.vendor != "apple"
3199-
|| !matches!(os.as_ref(), "ios" | "tvos" | "watchos" | "visionos" | "macos")
3200-
|| !matches!(flavor, LinkerFlavor::Darwin(..))
3201-
{
3197+
if !sess.target.is_like_darwin {
32023198
return None;
32033199
}
3204-
3205-
if os == "macos" && !matches!(flavor, LinkerFlavor::Darwin(Cc::No, _)) {
3200+
let LinkerFlavor::Darwin(cc, _) = flavor else {
32063201
return None;
3207-
}
3208-
3209-
let sdk_root = sess.time("get_apple_sdk_root", || get_apple_sdk_root(sess))?;
3202+
};
32103203

3211-
match flavor {
3212-
LinkerFlavor::Darwin(Cc::Yes, _) => {
3213-
// Use `-isysroot` instead of `--sysroot`, as only the former
3214-
// makes Clang treat it as a platform SDK.
3215-
//
3216-
// This is admittedly a bit strange, as on most targets
3217-
// `-isysroot` only applies to include header files, but on Apple
3218-
// targets this also applies to libraries and frameworks.
3219-
cmd.cc_arg("-isysroot");
3220-
cmd.cc_arg(&sdk_root);
3221-
}
3222-
LinkerFlavor::Darwin(Cc::No, _) => {
3223-
cmd.link_arg("-syslibroot");
3224-
cmd.link_arg(&sdk_root);
3225-
}
3226-
_ => unreachable!(),
3204+
// The default compiler driver on macOS is at `/usr/bin/cc`. This is a trampoline binary that
3205+
// effectively invokes `xcrun cc` internally to look up both the compiler binary and the SDK
3206+
// root from the current Xcode installation. When cross-compiling, when `rustc` is invoked
3207+
// inside Xcode, or when invoking the linker directly, this default logic is unsuitable, so
3208+
// instead we invoke `xcrun` manually.
3209+
//
3210+
// (Note that this doesn't mean we get a duplicate lookup here - passing `SDKROOT` below will
3211+
// cause the trampoline binary to skip looking up the SDK itself).
3212+
let sdkroot = sess.time("get_apple_sdk_root", || get_apple_sdk_root(sess))?;
3213+
3214+
if cc == Cc::Yes {
3215+
// There are a few options to pass the SDK root when linking with a C/C++ compiler:
3216+
// - The `--sysroot` flag.
3217+
// - The `-isysroot` flag.
3218+
// - The `SDKROOT` environment variable.
3219+
//
3220+
// `--sysroot` isn't actually enough to get Clang to treat it as a platform SDK, you need
3221+
// to specify `-isysroot`. This is admittedly a bit strange, as on most targets `-isysroot`
3222+
// only applies to include header files, but on Apple targets it also applies to libraries
3223+
// and frameworks.
3224+
//
3225+
// This leaves the choice between `-isysroot` and `SDKROOT`. Both are supported by Clang and
3226+
// GCC, though they may not be supported by all compiler drivers. We choose `SDKROOT`,
3227+
// primarily because that is the same interface that is used when invoking the tool under
3228+
// `xcrun -sdk macosx $tool`.
3229+
//
3230+
// In that sense, if a given compiler driver does not support `SDKROOT`, the blame is fairly
3231+
// clearly in the tool in question, since they also don't support being run under `xcrun`.
3232+
//
3233+
// Additionally, `SDKROOT` is an environment variable and thus optional. It also has lower
3234+
// precedence than `-isysroot`, so a custom compiler driver that does not support it and
3235+
// instead figures out the SDK on their own can easily do so by using `-isysroot`.
3236+
//
3237+
// (This in particular affects Clang built with the `DEFAULT_SYSROOT` CMake flag, such as
3238+
// the one provided by some versions of Homebrew's `llvm` package. Those will end up
3239+
// ignoring the value we set here, and instead use their built-in sysroot).
3240+
cmd.cmd().env("SDKROOT", &sdkroot);
3241+
} else {
3242+
// When invoking the linker directly, we use the `-syslibroot` parameter. `SDKROOT` is not
3243+
// read by the linker, so it's really the only option.
3244+
//
3245+
// This is also what Clang does.
3246+
cmd.link_arg("-syslibroot");
3247+
cmd.link_arg(&sdkroot);
32273248
}
32283249

3229-
Some(sdk_root)
3250+
Some(sdkroot)
32303251
}
32313252

32323253
fn get_apple_sdk_root(sess: &Session) -> Option<PathBuf> {
@@ -3255,7 +3276,13 @@ fn get_apple_sdk_root(sess: &Session) -> Option<PathBuf> {
32553276
}
32563277
"macosx"
32573278
if sdkroot.contains("iPhoneOS.platform")
3258-
|| sdkroot.contains("iPhoneSimulator.platform") => {}
3279+
|| sdkroot.contains("iPhoneSimulator.platform")
3280+
|| sdkroot.contains("AppleTVOS.platform")
3281+
|| sdkroot.contains("AppleTVSimulator.platform")
3282+
|| sdkroot.contains("WatchOS.platform")
3283+
|| sdkroot.contains("WatchSimulator.platform")
3284+
|| sdkroot.contains("XROS.platform")
3285+
|| sdkroot.contains("XRSimulator.platform") => {}
32593286
"watchos"
32603287
if sdkroot.contains("WatchSimulator.platform")
32613288
|| sdkroot.contains("MacOSX.platform") => {}

compiler/rustc_target/src/spec/base/apple/mod.rs

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::borrow::Cow;
2-
use std::env;
32
use std::fmt::{Display, from_fn};
43
use std::num::ParseIntError;
54
use std::str::FromStr;
@@ -209,29 +208,10 @@ fn link_env_remove(os: &'static str) -> StaticCow<[StaticCow<str>]> {
209208
// that's only applicable to cross-OS compilation. Always leave anything for the
210209
// host OS alone though.
211210
if os == "macos" {
212-
let mut env_remove = Vec::with_capacity(2);
213-
// Remove the `SDKROOT` environment variable if it's clearly set for the wrong platform, which
214-
// may occur when we're linking a custom build script while targeting iOS for example.
215-
if let Ok(sdkroot) = env::var("SDKROOT") {
216-
if sdkroot.contains("iPhoneOS.platform")
217-
|| sdkroot.contains("iPhoneSimulator.platform")
218-
|| sdkroot.contains("AppleTVOS.platform")
219-
|| sdkroot.contains("AppleTVSimulator.platform")
220-
|| sdkroot.contains("WatchOS.platform")
221-
|| sdkroot.contains("WatchSimulator.platform")
222-
|| sdkroot.contains("XROS.platform")
223-
|| sdkroot.contains("XRSimulator.platform")
224-
{
225-
env_remove.push("SDKROOT".into())
226-
}
227-
}
228-
// Additionally, `IPHONEOS_DEPLOYMENT_TARGET` must not be set when using the Xcode linker at
211+
// `IPHONEOS_DEPLOYMENT_TARGET` must not be set when using the Xcode linker at
229212
// "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld",
230213
// although this is apparently ignored when using the linker at "/usr/bin/ld".
231-
env_remove.push("IPHONEOS_DEPLOYMENT_TARGET".into());
232-
env_remove.push("TVOS_DEPLOYMENT_TARGET".into());
233-
env_remove.push("XROS_DEPLOYMENT_TARGET".into());
234-
env_remove.into()
214+
cvs!["IPHONEOS_DEPLOYMENT_TARGET", "TVOS_DEPLOYMENT_TARGET", "XROS_DEPLOYMENT_TARGET"]
235215
} else {
236216
// Otherwise if cross-compiling for a different OS/SDK (including Mac Catalyst), remove any part
237217
// of the linking environment that's wrong and reversed.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# `SDKROOT`
22

33
This environment variable is used on Apple targets.
4-
It is passed through to the linker (currently either as `-isysroot` or `-syslibroot`).
4+
It is passed through to the linker (currently either directly or via the `-syslibroot` flag).
55

66
Note that this variable is not always respected. When the SDKROOT is clearly wrong (e.g. when the platform of the SDK does not match the `--target` used by rustc), this is ignored and rustc does its own detection.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fn main() {}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Test that linking works under an environment similar to what Xcode sets up.
2+
//!
3+
//! Regression test for https://github.com/rust-lang/rust/issues/80817.
4+
5+
//@ only-apple
6+
7+
use run_make_support::{cmd, rustc, target};
8+
9+
fn main() {
10+
// Fetch toolchain `/usr/bin` directory. Usually:
11+
// /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
12+
let clang_bin = cmd("xcrun").arg("--find").arg("clang").run().stdout_utf8();
13+
let toolchain_bin = clang_bin.trim().strip_suffix("/clang").unwrap();
14+
15+
// Put toolchain directory at the front of PATH.
16+
let path = format!("{}:{}", toolchain_bin, std::env::var("PATH").unwrap());
17+
18+
// Check that compiling and linking still works.
19+
//
20+
// Removing `SDKROOT` is necessary for the test to excercise what we want, since bootstrap runs
21+
// under `/usr/bin/python3`, which will set SDKROOT for us.
22+
rustc().target(target()).env_remove("SDKROOT").env("PATH", &path).input("foo.rs").run();
23+
24+
// Also check linking directly with the system linker.
25+
rustc()
26+
.target(target())
27+
.env_remove("SDKROOT")
28+
.env("PATH", &path)
29+
.input("foo.rs")
30+
.arg("-Clinker-flavor=ld")
31+
.run();
32+
}

0 commit comments

Comments
 (0)