Skip to content

Commit 89348e5

Browse files
committed
Emit better error messages when invoking xcrun
Also allow the SDK path to be non-UTF-8.
1 parent bd1ef0f commit 89348e5

File tree

6 files changed

+263
-49
lines changed

6 files changed

+263
-49
lines changed

compiler/rustc_codegen_ssa/messages.ftl

+17-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ codegen_ssa_apple_deployment_target_invalid =
1010
codegen_ssa_apple_deployment_target_too_low =
1111
deployment target in {$env_var} was set to {$version}, but the minimum supported by `rustc` is {$os_min}
1212
13-
codegen_ssa_apple_sdk_error_sdk_path = failed to get {$sdk_name} SDK path: {$error}
14-
1513
codegen_ssa_archive_build_failure = failed to build archive at `{$path}`: {$error}
1614
1715
codegen_ssa_atomic_compare_exchange = Atomic compare-exchange intrinsic missing failure memory ordering
@@ -400,3 +398,20 @@ codegen_ssa_use_cargo_directive = use the `cargo:rustc-link-lib` directive to sp
400398
codegen_ssa_version_script_write_failure = failed to write version script: {$error}
401399
402400
codegen_ssa_visual_studio_not_installed = you may need to install Visual Studio build tools with the "C++ build tools" workload
401+
402+
codegen_ssa_xcrun_command_line_tools_insufficient =
403+
when compiling for iOS, tvOS, visionOS or watchOS, you need a full installation of Xcode
404+
405+
codegen_ssa_xcrun_failed_invoking = invoking `{$command_formatted}` to find {$sdk_name}.sdk failed: {$error}
406+
407+
codegen_ssa_xcrun_found_developer_dir = found active developer directory at "{$developer_dir}"
408+
409+
# `xcrun` already outputs a message about missing Xcode installation, so we only augment it with details about env vars.
410+
codegen_ssa_xcrun_no_developer_dir =
411+
pass the path of an Xcode installation via the DEVELOPER_DIR environment variable, or an SDK with the SDKROOT environment variable
412+
413+
codegen_ssa_xcrun_sdk_path_warning = output of `xcrun` while finding {$sdk_name}.sdk
414+
.note = {$stderr}
415+
416+
codegen_ssa_xcrun_unsuccessful = failed running `{$command_formatted}` to find {$sdk_name}.sdk
417+
.note = {$stdout}{$stderr}

compiler/rustc_codegen_ssa/src/back/apple.rs

+135-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
use std::env;
2+
use std::ffi::OsString;
23
use std::fmt::{Display, from_fn};
34
use std::num::ParseIntError;
5+
use std::path::PathBuf;
6+
use std::process::Command;
47

8+
use itertools::Itertools;
59
use rustc_middle::middle::exported_symbols::SymbolExportKind;
610
use rustc_session::Session;
711
use rustc_target::spec::Target;
12+
use tracing::debug;
813

9-
use crate::errors::AppleDeploymentTarget;
14+
use crate::errors::{AppleDeploymentTarget, XcrunError, XcrunSdkPathWarning};
15+
use crate::fluent_generated as fluent;
1016

1117
#[cfg(test)]
1218
mod tests;
@@ -271,3 +277,131 @@ pub(super) fn add_version_to_llvm_target(
271277
format!("{arch}-{vendor}-{os}{major}.{minor}.{patch}")
272278
}
273279
}
280+
281+
pub(super) fn get_sdk_root(sess: &Session) -> Option<PathBuf> {
282+
let sdk_name = sdk_name(&sess.target);
283+
284+
match xcrun_show_sdk_path(sdk_name, sess.verbose_internals()) {
285+
Ok((path, stderr)) => {
286+
// Emit extra stderr, such as if `-verbose` was passed, or if `xcrun` emitted a warning.
287+
if !stderr.is_empty() {
288+
sess.dcx().emit_warn(XcrunSdkPathWarning { sdk_name, stderr });
289+
}
290+
Some(path)
291+
}
292+
Err(err) => {
293+
let mut diag = sess.dcx().create_err(err);
294+
295+
// Recognize common error cases, and give more Rust-specific error messages for those.
296+
if let Some(developer_dir) = xcode_select_developer_dir() {
297+
diag.arg("developer_dir", &developer_dir);
298+
diag.note(fluent::codegen_ssa_xcrun_found_developer_dir);
299+
if developer_dir.as_os_str().to_string_lossy().contains("CommandLineTools") {
300+
if sdk_name != "MacOSX" {
301+
diag.help(fluent::codegen_ssa_xcrun_command_line_tools_insufficient);
302+
}
303+
}
304+
} else {
305+
diag.help(fluent::codegen_ssa_xcrun_no_developer_dir);
306+
}
307+
308+
diag.emit();
309+
None
310+
}
311+
}
312+
}
313+
314+
/// Invoke `xcrun --sdk $sdk_name --show-sdk-path` to get the SDK path.
315+
///
316+
/// The exact logic that `xcrun` uses is unspecified (see `man xcrun` for a few details), and may
317+
/// change between macOS and Xcode versions, but it roughly boils down to finding the active
318+
/// developer directory, and then invoking `xcodebuild -sdk $sdk_name -version` to get the SDK
319+
/// details.
320+
///
321+
/// Finding the developer directory is roughly done by looking at, in order:
322+
/// - The `DEVELOPER_DIR` environment variable.
323+
/// - The `/var/db/xcode_select_link` symlink (set by `xcode-select --switch`).
324+
/// - `/Applications/Xcode.app` (hardcoded fallback path).
325+
/// - `/Library/Developer/CommandLineTools` (hardcoded fallback path).
326+
///
327+
/// Note that `xcrun` caches its result, but with a cold cache this whole operation can be quite
328+
/// slow, especially so the first time it's run after a reboot.
329+
fn xcrun_show_sdk_path(
330+
sdk_name: &'static str,
331+
verbose: bool,
332+
) -> Result<(PathBuf, String), XcrunError> {
333+
let mut cmd = Command::new("xcrun");
334+
if verbose {
335+
cmd.arg("--verbose");
336+
}
337+
// The `--sdk` parameter is the same as in xcodebuild, namely either an absolute path to an SDK,
338+
// or the (lowercase) canonical name of an SDK.
339+
cmd.arg("--sdk");
340+
cmd.arg(&sdk_name.to_lowercase());
341+
cmd.arg("--show-sdk-path");
342+
343+
// We do not stream stdout/stderr lines directly to the user, since whether they are warnings or
344+
// errors depends on the status code at the end.
345+
let output = cmd.output().map_err(|error| XcrunError::FailedInvoking {
346+
sdk_name,
347+
command_formatted: format!("{cmd:?}"),
348+
error,
349+
})?;
350+
351+
// It is fine to do lossy conversion here, non-UTF-8 paths are quite rare on macOS nowadays
352+
// (only possible with the HFS+ file system), and we only use it for error messages.
353+
let stderr = String::from_utf8_lossy_owned(output.stderr);
354+
if !stderr.is_empty() {
355+
debug!(stderr, "original xcrun stderr");
356+
}
357+
358+
// Some versions of `xcodebuild` output beefy errors when invoked via `xcrun`,
359+
// but these are usually red herrings.
360+
let stderr = stderr
361+
.lines()
362+
.filter(|line| {
363+
!line.contains("Writing error result bundle")
364+
&& !line.contains("Requested but did not find extension point with identifier")
365+
})
366+
.join("\n");
367+
368+
if output.status.success() {
369+
Ok((stdout_to_path(output.stdout), stderr))
370+
} else {
371+
// Output both stdout and stderr, since shims of `xcrun` (such as the one provided by
372+
// nixpkgs), do not always use stderr for errors.
373+
let stdout = String::from_utf8_lossy_owned(output.stdout).trim().to_string();
374+
Err(XcrunError::Unsuccessful {
375+
sdk_name,
376+
command_formatted: format!("{cmd:?}"),
377+
stdout,
378+
stderr,
379+
})
380+
}
381+
}
382+
383+
/// Invoke `xcode-select --print-path`, and return the current developer directory.
384+
///
385+
/// NOTE: We don't do any error handling here, this is only used as a canary in diagnostics (`xcrun`
386+
/// will have already emitted the relevant error information).
387+
fn xcode_select_developer_dir() -> Option<PathBuf> {
388+
let mut cmd = Command::new("xcode-select");
389+
cmd.arg("--print-path");
390+
let output = cmd.output().ok()?;
391+
if !output.status.success() {
392+
return None;
393+
}
394+
Some(stdout_to_path(output.stdout))
395+
}
396+
397+
fn stdout_to_path(mut stdout: Vec<u8>) -> PathBuf {
398+
// Remove trailing newline.
399+
if let Some(b'\n') = stdout.last() {
400+
let _ = stdout.pop().unwrap();
401+
}
402+
#[cfg(unix)]
403+
let path = <OsString as std::os::unix::ffi::OsStringExt>::from_vec(stdout);
404+
#[cfg(not(unix))] // Unimportant, this is only used on macOS
405+
let path = OsString::from(String::from_utf8(stdout).unwrap());
406+
PathBuf::from(path)
407+
}

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

+67-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{add_version_to_llvm_target, parse_version};
1+
use super::*;
22

33
#[test]
44
fn test_add_version_to_llvm_target() {
@@ -19,3 +19,69 @@ fn test_parse_version() {
1919
assert_eq!(parse_version("10.12.6"), Ok((10, 12, 6)));
2020
assert_eq!(parse_version("9999.99.99"), Ok((9999, 99, 99)));
2121
}
22+
23+
#[test]
24+
#[cfg_attr(not(target_os = "macos"), ignore = "xcode-select is only available on macOS")]
25+
fn lookup_developer_dir() {
26+
let _developer_dir = xcode_select_developer_dir().unwrap();
27+
}
28+
29+
#[test]
30+
#[cfg_attr(not(target_os = "macos"), ignore = "xcrun is only available on macOS")]
31+
fn lookup_sdk() {
32+
let (sdk_path, stderr) = xcrun_show_sdk_path("MacOSX", false).unwrap();
33+
// Check that the found SDK is valid.
34+
assert!(sdk_path.join("SDKSettings.plist").exists());
35+
assert_eq!(stderr, "");
36+
37+
// Test that the SDK root is a subdir of the developer directory.
38+
if let Some(developer_dir) = xcode_select_developer_dir() {
39+
// Only run this test if SDKROOT is not set (otherwise xcrun may look up via. that).
40+
if std::env::var_os("SDKROOT").is_some() {
41+
assert!(sdk_path.starts_with(&developer_dir));
42+
}
43+
}
44+
}
45+
46+
#[test]
47+
#[cfg_attr(not(target_os = "macos"), ignore = "xcrun is only available on macOS")]
48+
fn lookup_sdk_verbose() {
49+
let (_, stderr) = xcrun_show_sdk_path("MacOSX", true).unwrap();
50+
// Newer xcrun versions should emit something like this:
51+
//
52+
// xcrun: note: looking up SDK with 'xcodebuild -sdk macosx -version Path'
53+
// xcrun: note: xcrun_db = '/var/.../xcrun_db'
54+
// xcrun: note: lookup resolved to: '...'
55+
// xcrun: note: database key is: ...
56+
//
57+
// Or if the value is already cached, something like this:
58+
//
59+
// xcrun: note: database key is: ...
60+
// xcrun: note: lookup resolved in '/var/.../xcrun_db' : '...'
61+
assert!(
62+
stderr.contains("xcrun: note: lookup resolved"),
63+
"stderr should contain lookup note: {stderr}",
64+
);
65+
}
66+
67+
#[test]
68+
#[cfg_attr(not(target_os = "macos"), ignore = "xcrun is only available on macOS")]
69+
fn try_lookup_invalid_sdk() {
70+
// As a proxy for testing all the different ways that `xcrun` can fail,
71+
// test the case where an SDK was not found.
72+
let err = xcrun_show_sdk_path("invalid", false).unwrap_err();
73+
let XcrunError::Unsuccessful { stderr, .. } = err else {
74+
panic!("unexpected error kind: {err:?}");
75+
};
76+
// Either one of (depending on if using Command Line Tools or full Xcode):
77+
// xcrun: error: SDK "invalid" cannot be located
78+
// xcodebuild: error: SDK "invalid" cannot be located.
79+
assert!(
80+
stderr.contains(r#"error: SDK "invalid" cannot be located"#),
81+
"stderr should contain xcodebuild note: {stderr}",
82+
);
83+
assert!(
84+
stderr.contains("xcrun: error: unable to lookup item 'Path' in SDK 'invalid'"),
85+
"stderr should contain xcrun note: {stderr}",
86+
);
87+
}

compiler/rustc_codegen_ssa/src/back/link.rs

+20-39
Original file line numberDiff line numberDiff line change
@@ -3213,15 +3213,7 @@ fn add_apple_sdk(cmd: &mut dyn Linker, sess: &Session, flavor: LinkerFlavor) ->
32133213
return None;
32143214
}
32153215

3216-
let sdk_name = apple::sdk_name(&sess.target).to_lowercase();
3217-
3218-
let sdk_root = match sess.time("get_apple_sdk_root", || get_apple_sdk_root(&sdk_name)) {
3219-
Ok(s) => s,
3220-
Err(e) => {
3221-
sess.dcx().emit_err(e);
3222-
return None;
3223-
}
3224-
};
3216+
let sdk_root = sess.time("get_apple_sdk_root", || get_apple_sdk_root(sess))?;
32253217

32263218
match flavor {
32273219
LinkerFlavor::Darwin(Cc::Yes, _) => {
@@ -3231,28 +3223,32 @@ fn add_apple_sdk(cmd: &mut dyn Linker, sess: &Session, flavor: LinkerFlavor) ->
32313223
// This is admittedly a bit strange, as on most targets
32323224
// `-isysroot` only applies to include header files, but on Apple
32333225
// targets this also applies to libraries and frameworks.
3234-
cmd.cc_args(&["-isysroot", &sdk_root]);
3226+
cmd.cc_arg("-isysroot");
3227+
cmd.cc_arg(&sdk_root);
32353228
}
32363229
LinkerFlavor::Darwin(Cc::No, _) => {
3237-
cmd.link_args(&["-syslibroot", &sdk_root]);
3230+
cmd.link_arg("-syslibroot");
3231+
cmd.link_arg(&sdk_root);
32383232
}
32393233
_ => unreachable!(),
32403234
}
32413235

3242-
Some(sdk_root.into())
3236+
Some(sdk_root)
32433237
}
32443238

3245-
fn get_apple_sdk_root(sdk_name: &str) -> Result<String, errors::AppleSdkRootError<'_>> {
3246-
// Following what clang does
3247-
// (https://github.com/llvm/llvm-project/blob/
3248-
// 296a80102a9b72c3eda80558fb78a3ed8849b341/clang/lib/Driver/ToolChains/Darwin.cpp#L1661-L1678)
3249-
// to allow the SDK path to be set. (For clang, xcrun sets
3250-
// SDKROOT; for rustc, the user or build system can set it, or we
3251-
// can fall back to checking for xcrun on PATH.)
3239+
fn get_apple_sdk_root(sess: &Session) -> Option<PathBuf> {
32523240
if let Ok(sdkroot) = env::var("SDKROOT") {
3253-
let p = Path::new(&sdkroot);
3254-
match sdk_name {
3255-
// Ignore `SDKROOT` if it's clearly set for the wrong platform.
3241+
let p = PathBuf::from(&sdkroot);
3242+
3243+
// Ignore invalid SDKs, similar to what clang does:
3244+
// https://github.com/llvm/llvm-project/blob/llvmorg-19.1.6/clang/lib/Driver/ToolChains/Darwin.cpp#L2212-L2229
3245+
//
3246+
// NOTE: Things are complicated here by the fact that `rustc` can be run by Cargo to compile
3247+
// build scripts and proc-macros for the host, and thus we need to ignore SDKROOT if it's
3248+
// clearly set for the wrong platform.
3249+
//
3250+
// FIXME(madsmtm): Make this more robust (maybe read `SDKSettings.json` like Clang does?).
3251+
match &*apple::sdk_name(&sess.target).to_lowercase() {
32563252
"appletvos"
32573253
if sdkroot.contains("TVSimulator.platform")
32583254
|| sdkroot.contains("MacOSX.platform") => {}
@@ -3279,26 +3275,11 @@ fn get_apple_sdk_root(sdk_name: &str) -> Result<String, errors::AppleSdkRootErro
32793275
if sdkroot.contains("XROS.platform") || sdkroot.contains("MacOSX.platform") => {}
32803276
// Ignore `SDKROOT` if it's not a valid path.
32813277
_ if !p.is_absolute() || p == Path::new("/") || !p.exists() => {}
3282-
_ => return Ok(sdkroot),
3278+
_ => return Some(p),
32833279
}
32843280
}
3285-
let res =
3286-
Command::new("xcrun").arg("--show-sdk-path").arg("-sdk").arg(sdk_name).output().and_then(
3287-
|output| {
3288-
if output.status.success() {
3289-
Ok(String::from_utf8(output.stdout).unwrap())
3290-
} else {
3291-
let error = String::from_utf8(output.stderr);
3292-
let error = format!("process exit with error: {}", error.unwrap());
3293-
Err(io::Error::new(io::ErrorKind::Other, &error[..]))
3294-
}
3295-
},
3296-
);
32973281

3298-
match res {
3299-
Ok(output) => Ok(output.trim().to_string()),
3300-
Err(error) => Err(errors::AppleSdkRootError::SdkPath { sdk_name, error }),
3301-
}
3282+
apple::get_sdk_root(sess)
33023283
}
33033284

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

compiler/rustc_codegen_ssa/src/errors.rs

+23-6
Original file line numberDiff line numberDiff line change
@@ -746,12 +746,6 @@ pub(crate) enum AppleDeploymentTarget {
746746
TooLow { env_var: &'static str, version: String, os_min: String },
747747
}
748748

749-
#[derive(Diagnostic)]
750-
pub(crate) enum AppleSdkRootError<'a> {
751-
#[diag(codegen_ssa_apple_sdk_error_sdk_path)]
752-
SdkPath { sdk_name: &'a str, error: Error },
753-
}
754-
755749
#[derive(Diagnostic)]
756750
#[diag(codegen_ssa_read_file)]
757751
pub(crate) struct ReadFileError {
@@ -1327,3 +1321,26 @@ pub(crate) struct MixedExportNameAndNoMangle {
13271321
#[suggestion(style = "verbose", code = "", applicability = "machine-applicable")]
13281322
pub removal_span: Span,
13291323
}
1324+
1325+
#[derive(Diagnostic, Debug)]
1326+
pub(crate) enum XcrunError {
1327+
#[diag(codegen_ssa_xcrun_failed_invoking)]
1328+
FailedInvoking { sdk_name: &'static str, command_formatted: String, error: std::io::Error },
1329+
1330+
#[diag(codegen_ssa_xcrun_unsuccessful)]
1331+
#[note]
1332+
Unsuccessful {
1333+
sdk_name: &'static str,
1334+
command_formatted: String,
1335+
stdout: String,
1336+
stderr: String,
1337+
},
1338+
}
1339+
1340+
#[derive(Diagnostic, Debug)]
1341+
#[diag(codegen_ssa_xcrun_sdk_path_warning)]
1342+
#[note]
1343+
pub(crate) struct XcrunSdkPathWarning {
1344+
pub sdk_name: &'static str,
1345+
pub stderr: String,
1346+
}

compiler/rustc_codegen_ssa/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#![feature(let_chains)]
1414
#![feature(negative_impls)]
1515
#![feature(rustdoc_internals)]
16+
#![feature(string_from_utf8_lossy_owned)]
1617
#![feature(trait_alias)]
1718
#![feature(try_blocks)]
1819
// tidy-alphabetical-end

0 commit comments

Comments
 (0)