Skip to content

Commit bd1888f

Browse files
committed
Re-implement SDK discovery instead of using xcrun
1 parent 168951c commit bd1888f

File tree

11 files changed

+294
-42
lines changed

11 files changed

+294
-42
lines changed

compiler/rustc_codegen_ssa/messages.ftl

+59-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,65 @@ codegen_ssa_apple_deployment_target_invalid =
88
codegen_ssa_apple_deployment_target_too_low =
99
deployment target in {$env_var} was set to {$version}, but the minimum supported by `rustc` is {$os_min}
1010
11-
codegen_ssa_apple_sdk_error_sdk_path = failed to get {$sdk_name} SDK path: {$error}
11+
codegen_ssa_apple_sdk_error_failed_reading =
12+
failed reading `{$path}` while looking for SDK root: {$error}
13+
14+
codegen_ssa_apple_sdk_error_missing =
15+
failed finding SDK for platform `{$sdk_name}`. It looks like you have not installed Xcode?
16+
17+
{ $sdk_name ->
18+
[MacOSX] You should install Xcode via the App Store, or run `xcode-select --install` to install the Command Line Tools if you only intend on developing for macOS.
19+
*[other] You should install Xcode via the App Store.
20+
}
21+
22+
codegen_ssa_apple_sdk_error_missing_commandline_tools =
23+
failed finding SDK at `{$sdkroot}` in Command Line Tools installation.
24+
25+
{ $sdk_name ->
26+
[MacOSX] Perhaps you need to reinstall it with `xcode-select --install`?
27+
*[other] When compiling for iOS, tvOS, visionOS or watchOS, you will need a full installation of Xcode.
28+
}
29+
30+
codegen_ssa_apple_sdk_error_missing_cross_compile_non_macos =
31+
failed finding Apple SDK with name `{$sdk_name}`.
32+
33+
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.
34+
35+
The SDK can be downloaded and extracted from https://developer.apple.com/download/all/?q=xcode (requires an Apple ID).
36+
37+
The full Xcode bundle should contain the SDK in `Xcode.app/Contents/Developer/Platforms/{$sdk_name}.platform/Developer/SDKs/{$sdk_name}.sdk`{ $sdk_name ->
38+
[MacOSX] , but downloading just the Command Line Tools for Xcode should also be sufficient to obtain the macOS SDK.
39+
*[other] .
40+
}
41+
42+
You will then need to tell `rustc` about it using the `SDKROOT` environment variables.
43+
44+
Furthermore, you might need to install a linker capable of linking Mach-O files, or at least ensure that `rustc` is configured to use the bundled `lld`.
45+
46+
{ $sdk_name ->
47+
[MacOSX] {""}
48+
*[other] Beware that cross-compilation to iOS, tvOS, visionOS or watchOS is generally ill supported on non-macOS hosts.
49+
}
50+
51+
codegen_ssa_apple_sdk_error_missing_developer_dir =
52+
failed finding SDK inside active developer directory `{$dir}` set by the DEVELOPER_DIR environment variable. Looked in:
53+
- `{$sdkroot}`
54+
- `{$sdkroot_bare}`
55+
56+
codegen_ssa_apple_sdk_error_missing_xcode =
57+
failed finding SDK at `{$sdkroot}` in Xcode installation.
58+
59+
{ $sdk_name ->
60+
[MacOSX] {""}
61+
*[other] Perhaps you need a newer version of Xcode?
62+
}
63+
64+
codegen_ssa_apple_sdk_error_missing_xcode_select =
65+
failed finding SDK inside active developer directory `{$dir}` set by `xcode-select`. Looked in:
66+
- `{$sdkroot}`
67+
- `{$sdkroot_bare}`
68+
69+
Consider using `sudo xcode-select --switch path/to/Xcode.app` or `sudo xcode-select --reset` to select a valid path.
1270
1371
codegen_ssa_archive_build_failure = failed to build archive at `{$path}`: {$error}
1472

compiler/rustc_codegen_ssa/src/back/apple.rs

+133-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
use std::env;
21
use std::fmt::{Display, from_fn};
2+
use std::io::ErrorKind;
33
use std::num::ParseIntError;
4+
use std::path::{Path, PathBuf};
5+
use std::{env, fs};
46

57
use rustc_session::Session;
68
use rustc_target::spec::Target;
79

8-
use crate::errors::AppleDeploymentTarget;
10+
use crate::errors::{AppleDeploymentTarget, AppleSdkError};
911

1012
#[cfg(test)]
1113
mod tests;
@@ -186,3 +188,132 @@ pub(super) fn add_version_to_llvm_target(
186188
format!("{arch}-{vendor}-{os}{major}.{minor}.{patch}")
187189
}
188190
}
191+
192+
// TOCTOU is not _really_ an issue with our use of `try_exists` in here, we mostly use it for
193+
// diagnostics, and these directories are global state that the user can change anytime anyhow in
194+
// ways that are going to interfere much more with the compilation process.
195+
fn try_exists(path: &Path) -> Result<bool, AppleSdkError> {
196+
path.try_exists().map_err(|error| AppleSdkError::FailedReading { path: path.to_owned(), error })
197+
}
198+
199+
/// Get the SDK path for an SDK under `/Library/Developer/CommandLineTools`.
200+
fn sdk_root_in_sdks_dir(sdks_dir: impl Into<PathBuf>, sdk_name: &str) -> PathBuf {
201+
let mut path = sdks_dir.into();
202+
path.push("SDKs");
203+
path.push(sdk_name);
204+
path.set_extension("sdk");
205+
path
206+
}
207+
208+
/// Get the SDK path for an SDK under `/Applications/Xcode.app/Contents/Developer`.
209+
fn sdk_root_in_developer_dir(developer_dir: impl Into<PathBuf>, sdk_name: &str) -> PathBuf {
210+
let mut path = developer_dir.into();
211+
path.push("Platforms");
212+
path.push(sdk_name);
213+
path.set_extension("platform");
214+
path.push("Developer");
215+
path.push("SDKs");
216+
path.push(sdk_name);
217+
path.set_extension("sdk");
218+
path
219+
}
220+
221+
/// Find a SDK root from the user's environment for the given SDK name.
222+
///
223+
/// We do this by searching (purely by names in the filesystem, without reading SDKSettings.json)
224+
/// for a matching SDK in the following places:
225+
/// - `DEVELOPER_DIR`
226+
/// - `/var/db/xcode_select_link`
227+
/// - `/Applications/Xcode.app`
228+
/// - `/Library/Developer/CommandLineTools`
229+
///
230+
/// This does roughly the same thing as `xcrun -sdk $sdk_name -show-sdk-path` (see `man xcrun` for
231+
/// a few details on the search algorithm).
232+
///
233+
/// The reason why we implement this logic ourselves is:
234+
/// - Reading these directly is faster than spawning a new process.
235+
/// - `xcrun` can be fairly slow to start up after a reboot.
236+
/// - In the future, we will be able to integrate this better with the compiler's change tracking
237+
/// mechanisms, allowing rebuilds when the involved env vars and paths here change. See #118204.
238+
/// - It's easier for us to emit better error messages.
239+
///
240+
/// Though a downside is that `xcrun` might be expanded in the future to check more places, and then
241+
/// `rustc` would have to be changed to keep up. Furthermore, `xcrun`'s exact algorithm is
242+
/// undocumented, so it might be doing more things than we do here.
243+
pub(crate) fn find_sdk_root(sdk_name: &'static str) -> Result<PathBuf, AppleSdkError> {
244+
// Only try this if the host OS is macOS.
245+
if !cfg!(target_os = "macos") {
246+
return Err(AppleSdkError::MissingCrossCompileNonMacOS { sdk_name });
247+
}
248+
249+
// NOTE: We could consider walking upwards in `SDKROOT` assuming Xcode directory structure, but
250+
// that isn't what `xcrun` does, and might still not yield the desired result (e.g. if using an
251+
// old SDK to compile for an old ARM iOS arch, we don't want `rustc` to pick a macOS SDK from
252+
// the old Xcode).
253+
254+
// Try reading from `DEVELOPER_DIR`.
255+
if let Some(dir) = std::env::var_os("DEVELOPER_DIR") {
256+
let dir = PathBuf::from(dir);
257+
let sdkroot = sdk_root_in_developer_dir(&dir, sdk_name);
258+
259+
if try_exists(&sdkroot)? {
260+
return Ok(sdkroot);
261+
} else {
262+
let sdkroot_bare = sdk_root_in_sdks_dir(&dir, sdk_name);
263+
if try_exists(&sdkroot_bare)? {
264+
return Ok(sdkroot_bare);
265+
} else {
266+
return Err(AppleSdkError::MissingDeveloperDir { dir, sdkroot, sdkroot_bare });
267+
}
268+
}
269+
}
270+
271+
// Next, try to read the link that `xcode-select` sets.
272+
//
273+
// FIXME(madsmtm): Support cases where `/var/db/xcode_select_link` contains a relative path?
274+
let path = Path::new("/var/db/xcode_select_link");
275+
match fs::read_link(path) {
276+
Ok(dir) => {
277+
let sdkroot = sdk_root_in_developer_dir(&dir, sdk_name);
278+
if try_exists(&sdkroot)? {
279+
return Ok(sdkroot);
280+
} else {
281+
let sdkroot_bare = sdk_root_in_sdks_dir(&dir, sdk_name);
282+
if try_exists(&sdkroot_bare)? {
283+
return Ok(sdkroot_bare);
284+
} else {
285+
return Err(AppleSdkError::MissingXcodeSelect { dir, sdkroot, sdkroot_bare });
286+
}
287+
}
288+
}
289+
Err(err) if err.kind() == ErrorKind::NotFound => {
290+
// Intentionally ignore not found errors, if `xcode-select --reset` is called the
291+
// link will not exist.
292+
}
293+
Err(error) => return Err(AppleSdkError::FailedReading { path: path.into(), error }),
294+
}
295+
296+
// Next, fall back to reading from `/Applications/Xcode.app`.
297+
let dir = Path::new("/Applications/Xcode.app/Contents/Developer");
298+
if try_exists(dir)? {
299+
let sdkroot = sdk_root_in_developer_dir(dir, sdk_name);
300+
if try_exists(&sdkroot)? {
301+
return Ok(sdkroot);
302+
} else {
303+
return Err(AppleSdkError::MissingXcode { sdkroot, sdk_name });
304+
}
305+
}
306+
307+
// Finally, fall back to reading from `/Library/Developer/CommandLineTools`.
308+
let dir = Path::new("/Library/Developer/CommandLineTools");
309+
if try_exists(dir)? {
310+
let sdkroot = sdk_root_in_sdks_dir(dir, sdk_name);
311+
if try_exists(&sdkroot)? {
312+
return Ok(sdkroot);
313+
} else {
314+
return Err(AppleSdkError::MissingCommandlineTools { sdkroot, sdk_name });
315+
}
316+
}
317+
318+
Err(AppleSdkError::Missing { sdk_name })
319+
}

compiler/rustc_codegen_ssa/src/back/apple/tests.rs

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use super::{add_version_to_llvm_target, parse_version};
1+
use std::io;
2+
use std::path::PathBuf;
3+
use std::process::Command;
4+
5+
use super::{add_version_to_llvm_target, find_sdk_root, parse_version};
26

37
#[test]
48
fn test_add_version_to_llvm_target() {
@@ -19,3 +23,56 @@ fn test_parse_version() {
1923
assert_eq!(parse_version("10.12.6"), Ok((10, 12, 6)));
2024
assert_eq!(parse_version("9999.99.99"), Ok((9999, 99, 99)));
2125
}
26+
27+
fn find_sdk_root_xcrun(sdk_name: &str) -> io::Result<PathBuf> {
28+
let output = Command::new("xcrun")
29+
.arg("-sdk")
30+
.arg(sdk_name.to_lowercase())
31+
.arg("-show-sdk-path")
32+
.output()?;
33+
if output.status.success() {
34+
// FIXME(madsmtm): If using this for real, we should not error on non-UTF-8 paths.
35+
let output = std::str::from_utf8(&output.stdout).unwrap();
36+
Ok(PathBuf::from(output.trim()))
37+
} else {
38+
let error = String::from_utf8(output.stderr);
39+
let error = format!("process exit with error: {}", error.unwrap());
40+
Err(io::Error::new(io::ErrorKind::Other, error))
41+
}
42+
}
43+
44+
/// Ensure that our `find_sdk_root` matches `xcrun`'s behaviour.
45+
///
46+
/// `xcrun` is quite slow the first time it's run after a reboot, so this test may take some time.
47+
#[test]
48+
#[cfg_attr(not(target_os = "macos"), ignore = "xcrun is only available on macOS")]
49+
fn test_find_sdk_root() {
50+
let sdks = [
51+
"MacOSX",
52+
"AppleTVOS",
53+
"AppleTVSimulator",
54+
"iPhoneOS",
55+
"iPhoneSimulator",
56+
"WatchOS",
57+
"WatchSimulator",
58+
"XROS",
59+
"XRSimulator",
60+
];
61+
for sdk_name in sdks {
62+
if let Ok(expected) = find_sdk_root_xcrun(sdk_name) {
63+
// `xcrun` prefers `MacOSX14.0.sdk` over `MacOSX.sdk`, so let's compare canonical paths.
64+
let expected = std::fs::canonicalize(expected).unwrap();
65+
let actual = find_sdk_root(sdk_name).unwrap();
66+
let actual = std::fs::canonicalize(actual).unwrap();
67+
assert_eq!(expected, actual);
68+
} else {
69+
// The macOS SDK must always be findable in Rust's CI.
70+
//
71+
// The other SDKs are allowed to not be found in the current developer directory when
72+
// running this test.
73+
if sdk_name == "MacOSX" {
74+
panic!("Could not find macOS SDK with `xcrun -sdk macosx -show-sdk-path`");
75+
}
76+
}
77+
}
78+
}

compiler/rustc_codegen_ssa/src/back/link.rs

+10-28
Original file line numberDiff line numberDiff line change
@@ -3154,26 +3154,26 @@ fn add_apple_sdk(cmd: &mut dyn Linker, sess: &Session, flavor: LinkerFlavor) ->
31543154
// This is admittedly a bit strange, as on most targets
31553155
// `-isysroot` only applies to include header files, but on Apple
31563156
// targets this also applies to libraries and frameworks.
3157-
cmd.cc_args(&["-isysroot", &sdk_root]);
3157+
cmd.cc_arg("-isysroot");
3158+
cmd.cc_arg(&sdk_root);
31583159
}
31593160
LinkerFlavor::Darwin(Cc::No, _) => {
3160-
cmd.link_args(&["-syslibroot", &sdk_root]);
3161+
cmd.link_arg("-syslibroot");
3162+
cmd.link_arg(&sdk_root);
31613163
}
31623164
_ => unreachable!(),
31633165
}
31643166

3165-
Some(sdk_root.into())
3167+
Some(sdk_root)
31663168
}
31673169

3168-
fn get_apple_sdk_root(sdk_name: &str) -> Result<String, errors::AppleSdkRootError<'_>> {
3170+
fn get_apple_sdk_root(sdk_name: &'static str) -> Result<PathBuf, errors::AppleSdkError> {
31693171
// Following what clang does
31703172
// (https://github.com/llvm/llvm-project/blob/
31713173
// 296a80102a9b72c3eda80558fb78a3ed8849b341/clang/lib/Driver/ToolChains/Darwin.cpp#L1661-L1678)
3172-
// to allow the SDK path to be set. (For clang, xcrun sets
3173-
// SDKROOT; for rustc, the user or build system can set it, or we
3174-
// can fall back to checking for xcrun on PATH.)
3174+
// to allow the SDK path to be set.
31753175
if let Ok(sdkroot) = env::var("SDKROOT") {
3176-
let p = Path::new(&sdkroot);
3176+
let p = PathBuf::from(&sdkroot);
31773177
match &*sdk_name.to_lowercase() {
31783178
// Ignore `SDKROOT` if it's clearly set for the wrong platform.
31793179
"appletvos"
@@ -3202,29 +3202,11 @@ fn get_apple_sdk_root(sdk_name: &str) -> Result<String, errors::AppleSdkRootErro
32023202
if sdkroot.contains("XROS.platform") || sdkroot.contains("MacOSX.platform") => {}
32033203
// Ignore `SDKROOT` if it's not a valid path.
32043204
_ if !p.is_absolute() || p == Path::new("/") || !p.exists() => {}
3205-
_ => return Ok(sdkroot),
3205+
_ => return Ok(p),
32063206
}
32073207
}
32083208

3209-
let res = Command::new("xcrun")
3210-
.arg("--show-sdk-path")
3211-
.arg("-sdk")
3212-
.arg(sdk_name.to_lowercase())
3213-
.output()
3214-
.and_then(|output| {
3215-
if output.status.success() {
3216-
Ok(String::from_utf8(output.stdout).unwrap())
3217-
} else {
3218-
let error = String::from_utf8(output.stderr);
3219-
let error = format!("process exit with error: {}", error.unwrap());
3220-
Err(io::Error::new(io::ErrorKind::Other, &error[..]))
3221-
}
3222-
});
3223-
3224-
match res {
3225-
Ok(output) => Ok(output.trim().to_string()),
3226-
Err(error) => Err(errors::AppleSdkRootError::SdkPath { sdk_name, error }),
3227-
}
3209+
apple::find_sdk_root(sdk_name)
32283210
}
32293211

32303212
/// When using the linker flavors opting in to `lld`, add the necessary paths and arguments to

compiler/rustc_codegen_ssa/src/errors.rs

+22-4
Original file line numberDiff line numberDiff line change
@@ -541,10 +541,28 @@ pub(crate) enum AppleDeploymentTarget {
541541
TooLow { env_var: &'static str, version: String, os_min: String },
542542
}
543543

544-
#[derive(Diagnostic)]
545-
pub(crate) enum AppleSdkRootError<'a> {
546-
#[diag(codegen_ssa_apple_sdk_error_sdk_path)]
547-
SdkPath { sdk_name: &'a str, error: Error },
544+
#[derive(Diagnostic, Debug)]
545+
pub(crate) enum AppleSdkError {
546+
#[diag(codegen_ssa_apple_sdk_error_failed_reading)]
547+
FailedReading { path: PathBuf, error: std::io::Error },
548+
549+
#[diag(codegen_ssa_apple_sdk_error_missing)]
550+
Missing { sdk_name: &'static str },
551+
552+
#[diag(codegen_ssa_apple_sdk_error_missing_commandline_tools)]
553+
MissingCommandlineTools { sdkroot: PathBuf, sdk_name: &'static str },
554+
555+
#[diag(codegen_ssa_apple_sdk_error_missing_cross_compile_non_macos)]
556+
MissingCrossCompileNonMacOS { sdk_name: &'static str },
557+
558+
#[diag(codegen_ssa_apple_sdk_error_missing_developer_dir)]
559+
MissingDeveloperDir { dir: PathBuf, sdkroot: PathBuf, sdkroot_bare: PathBuf },
560+
561+
#[diag(codegen_ssa_apple_sdk_error_missing_xcode)]
562+
MissingXcode { sdkroot: PathBuf, sdk_name: &'static str },
563+
564+
#[diag(codegen_ssa_apple_sdk_error_missing_xcode_select)]
565+
MissingXcodeSelect { dir: PathBuf, sdkroot: PathBuf, sdkroot_bare: PathBuf },
548566
}
549567

550568
#[derive(Diagnostic)]

src/doc/rustc/src/platform-support/apple-darwin.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,5 @@ the `-mmacosx-version-min=...`, `-miphoneos-version-min=...` or similar flags
5858
to disambiguate.
5959

6060
The path to the SDK can be passed to `rustc` using the common `SDKROOT`
61-
environment variable.
61+
environment variable, or will be inferred when compiling on host macOS using
62+
roughly the same logic as `xcrun -sdk macosx -show-sdk-path`.

src/doc/rustc/src/platform-support/apple-ios-macabi.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ These targets are cross-compiled, and require the corresponding macOS SDK
2020
iOS-specific headers, as provided by Xcode 11 or higher.
2121

2222
The path to the SDK can be passed to `rustc` using the common `SDKROOT`
23-
environment variable.
23+
environment variable, or will be inferred when compiling on host macOS using
24+
roughly the same logic as `xcrun -sdk macosx -show-sdk-path`.
2425

2526
### OS version
2627

0 commit comments

Comments
 (0)