Skip to content

Commit

Permalink
Re-implement SDK discovery instead of using xcrun
Browse files Browse the repository at this point in the history
  • Loading branch information
madsmtm committed Oct 9, 2024
1 parent 8055191 commit edf397b
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 33 deletions.
55 changes: 54 additions & 1 deletion compiler/rustc_codegen_ssa/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,60 @@ codegen_ssa_L4Bender_exporting_symbols_unimplemented = exporting symbols not imp
codegen_ssa_add_native_library = failed to add native library {$library_path}: {$error}
codegen_ssa_apple_sdk_error_sdk_path = failed to get {$sdk_name} SDK path: {$error}
codegen_ssa_apple_sdk_error_failed_reading =
failed reading `{$path}` while looking for SDK root: {$error}
codegen_ssa_apple_sdk_error_missing =
failed finding SDK for platform `{$sdk_name}`. It looks like you have not installed Xcode?
{ $sdk_name ->
[MacOSX] You should install Xcode via the App Store, or run `xcode-select --install` if you only intend on developing for macOS.
*[other] You should install Xcode via the App Store.
}
codegen_ssa_apple_sdk_error_missing_commandline_tools =
failed finding SDK at `{$sdkroot}` in Command Line Tools installation.
{ $sdk_name ->
[MacOSX] Perhaps you need to reinstall it with `xcode-select --install`?
*[other] When cross-compiling for iOS, tvOS, visionOS or watchOS, you will need a full installation of Xcode.
}
codegen_ssa_apple_sdk_error_missing_cross_compile_non_macos =
failed finding Apple SDK with name `{$sdk_name}`.
The SDK is needed by the linker to know where to find symbols in system libraries.
The SDK can be downloaded and extracted from https://developer.apple.com/download/all/?q=xcode (requires an Apple ID).
You will then need to tell `rustc` about it using the `SDKROOT` environment variables.
Furthermore, you might need to install a linker capable of linking Mach-O files, or at least ensure that your linker is configured as `lld`.
{ $sdk_name ->
[MacOSX] {""}
*[other] Beware that cross-compilation to iOS, tvOS, visionOS or watchOS is generally ill supported on non-macOS hosts.
}
codegen_ssa_apple_sdk_error_missing_developer_dir =
failed finding SDK inside active developer path `{$dir}` set by the DEVELOPER_DIR environment variable. Looked in:
- `{$sdkroot}`
- `{$sdkroot_bare}`
codegen_ssa_apple_sdk_error_missing_xcode =
failed finding SDK at `{$sdkroot}` in Xcode installation.
{ $sdk_name ->
[MacOSX] {""}
*[other] Perhaps you need a newer version of Xcode?
}
codegen_ssa_apple_sdk_error_missing_xcode_select =
failed finding SDK inside active developer path `{$dir}` set by `xcode-select`. Looked in:
- `{$sdkroot}`
- `{$sdkroot_bare}`
Consider using `sudo xcode-select --switch path/to/Xcode.app` or `sudo xcode-select --reset` to select a valid path.
codegen_ssa_archive_build_failure = failed to build archive at `{$path}`: {$error}
Expand Down
136 changes: 136 additions & 0 deletions compiler/rustc_codegen_ssa/src/apple.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};

use crate::errors::AppleSdkError;

#[cfg(test)]
mod tests;

// TOCTOU is not _really_ an issue with our use of `try_exists` in here, we mostly use it for
// diagnostics, and these directories are global state that the user may change anytime anyhow.
fn try_exists(path: &Path) -> Result<bool, AppleSdkError> {
path.try_exists().map_err(|error| AppleSdkError::FailedReading { path: path.to_owned(), error })
}

/// Get the SDK path for an SDK under `/Library/Developer/CommandLineTools`.
fn sdk_root_in_sdks_dir(sdks_dir: impl Into<PathBuf>, sdk_name: &str) -> PathBuf {
let mut path = sdks_dir.into();
path.push("SDKs");
path.push(sdk_name);
path.set_extension("sdk");
path
}

/// Get the SDK path for an SDK under `/Applications/Xcode.app/Contents/Developer`.
fn sdk_root_in_developer_dir(developer_dir: impl Into<PathBuf>, sdk_name: &str) -> PathBuf {
let mut path = developer_dir.into();
path.push("Platforms");
path.push(sdk_name);
path.set_extension("platform");
path.push("Developer");
path.push("SDKs");
path.push(sdk_name);
path.set_extension("sdk");
path
}

/// Find a SDK root from the user's environment for the given SDK name.
///
/// We do this by searching (purely by names in the filesystem, without reading SDKSettings.json)
/// for a matching SDK in the following places:
/// - `DEVELOPER_DIR`
/// - `/var/db/xcode_select_link`
/// - `/Applications/Xcode.app`
/// - `/Library/Developer/CommandLineTools`
///
/// This does roughly the same thing as `xcrun -sdk $sdk_name -show-sdk-path` (see `man xcrun` for
/// a few details on the search algorithm).
///
/// The reason why we implement this logic ourselves is:
/// - Reading these directly is faster than spawning a new process.
/// - `xcrun` can be fairly slow to start up after a reboot.
/// - In the future, we will be able to integrate this better with the compiler's change tracking
/// mechanisms, allowing rebuilds when the involved env vars and paths here change. See #118204.
/// - It's easier for us to emit better error messages.
///
/// Though a downside is that `xcrun` might be expanded in the future to check more places, and then
/// `rustc` would have to be changed to keep up. Furthermore, `xcrun`'s exact algorithm is
/// undocumented, so it might be doing more things than we do here.
pub(crate) fn find_sdk_root(sdk_name: &'static str) -> Result<PathBuf, AppleSdkError> {
// Only try this if host OS is macOS.
if !cfg!(target_os = "macos") {
return Err(AppleSdkError::MissingCrossCompileNonMacOS { sdk_name });
}

// NOTE: We could consider walking upwards in `SDKROOT` assuming Xcode directory structure, but
// that isn't what `xcrun` does, and might still not yield the desired result (e.g. if using an
// old SDK to compile for an old ARM iOS arch, we don't want `rustc` to pick a macOS SDK from
// the old Xcode).

// Try reading from `DEVELOPER_DIR` on all hosts.
if let Some(dir) = std::env::var_os("DEVELOPER_DIR") {
let dir = PathBuf::from(dir);
let sdkroot = sdk_root_in_developer_dir(&dir, sdk_name);

if try_exists(&sdkroot)? {
return Ok(sdkroot);
} else {
let sdkroot_bare = sdk_root_in_sdks_dir(&dir, sdk_name);
if try_exists(&sdkroot_bare)? {
return Ok(sdkroot_bare);
} else {
return Err(AppleSdkError::MissingDeveloperDir { dir, sdkroot, sdkroot_bare });
}
}
}

// Next, try to read the link that `xcode-select` sets.
//
// FIXME(madsmtm): Support cases where `/var/db/xcode_select_link` contains a relative path?
let path = PathBuf::from("/var/db/xcode_select_link");
match fs::read_link(&path) {
Ok(dir) => {
let sdkroot = sdk_root_in_developer_dir(&dir, sdk_name);
if try_exists(&sdkroot)? {
return Ok(sdkroot);
} else {
let sdkroot_bare = sdk_root_in_sdks_dir(&dir, sdk_name);
if try_exists(&sdkroot_bare)? {
return Ok(sdkroot_bare);
} else {
return Err(AppleSdkError::MissingXcodeSelect { dir, sdkroot, sdkroot_bare });
}
}
}
Err(err) if err.kind() == ErrorKind::NotFound => {
// Intentionally ignore not found errors, if `xcode-select --reset` is called the
// link will not exist.
}
Err(error) => return Err(AppleSdkError::FailedReading { path, error }),
}

// Next, fall back to reading from `/Applications/Xcode.app`.
let dir = PathBuf::from("/Applications/Xcode.app/Contents/Developer");
if try_exists(&dir)? {
let sdkroot = sdk_root_in_developer_dir(&dir, sdk_name);
if try_exists(&sdkroot)? {
return Ok(sdkroot);
} else {
return Err(AppleSdkError::MissingXcode { sdkroot, sdk_name });
}
}

// Finally, fall back to reading from `/Library/Developer/CommandLineTools`.
let dir = PathBuf::from("/Library/Developer/CommandLineTools");
if try_exists(&dir)? {
let sdkroot = sdk_root_in_sdks_dir(&dir, sdk_name);
if try_exists(&sdkroot)? {
return Ok(sdkroot);
} else {
return Err(AppleSdkError::MissingCommandlineTools { sdkroot, sdk_name });
}
}

Err(AppleSdkError::Missing { sdk_name })
}
58 changes: 58 additions & 0 deletions compiler/rustc_codegen_ssa/src/apple/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use std::io;
use std::path::PathBuf;
use std::process::Command;

use super::find_sdk_root;

fn find_sdk_root_xcrun(sdk_name: &str) -> io::Result<PathBuf> {
let output = Command::new("xcrun")
.arg("-sdk")
.arg(sdk_name.to_lowercase())
.arg("-show-sdk-path")
.output()?;
if output.status.success() {
// FIXME(madsmtm): If using this for real, we should not error on non-UTF-8 paths.
let output = String::from_utf8(output.stdout).unwrap();
Ok(PathBuf::from(output.trim()))
} else {
let error = String::from_utf8(output.stderr);
let error = format!("process exit with error: {}", error.unwrap());
Err(io::Error::new(io::ErrorKind::Other, &error[..]))
}
}

/// Ensure that our `find_sdk_root` matches `xcrun`'s behaviour.
///
/// `xcrun` is quite slow the first time it's run after a reboot, so this test may take some time.
#[test]
#[cfg_attr(not(target_os = "macos"), ignore = "xcrun is only available on macOS")]
fn test_find_sdk_root() {
let sdks = [
"MacOSX",
"AppleTVOS",
"AppleTVSimulator",
"iPhoneOS",
"iPhoneSimulator",
"WatchOS",
"WatchSimulator",
"XROS",
"XRSimulator",
];
for sdk_name in sdks {
if let Ok(expected) = find_sdk_root_xcrun(sdk_name) {
// `xcrun` prefers `MacOSX14.0.sdk` over `MacOSX.sdk`, so let's compare canonical paths.
let expected = std::fs::canonicalize(expected).unwrap();
let actual = find_sdk_root(sdk_name).unwrap();
let actual = std::fs::canonicalize(actual).unwrap();
assert_eq!(expected, actual);
} else {
// The macOS SDK must always be findable in Rust's CI.
//
// The other SDKs are allowed to not be found in the current developer directory when
// running this test.
if sdk_name == "MacOSX" {
panic!("Could not find macOS SDK with `xcrun -sdk macosx -show-sdk-path`");
}
}
}
}
39 changes: 11 additions & 28 deletions compiler/rustc_codegen_ssa/src/back/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ use super::command::Command;
use super::linker::{self, Linker};
use super::metadata::{MetadataPosition, create_wrapper_file};
use super::rpath::{self, RPathConfig};
use crate::apple::find_sdk_root;
use crate::{
CodegenResults, CompiledModule, CrateInfo, NativeLib, common, errors,
looks_like_rust_object_file,
Expand Down Expand Up @@ -3153,26 +3154,26 @@ fn add_apple_sdk(cmd: &mut dyn Linker, sess: &Session, flavor: LinkerFlavor) ->
// This is admittedly a bit strange, as on most targets
// `-isysroot` only applies to include header files, but on Apple
// targets this also applies to libraries and frameworks.
cmd.cc_args(&["-isysroot", &sdk_root]);
cmd.cc_arg("-isysroot");
cmd.cc_arg(&sdk_root);
}
LinkerFlavor::Darwin(Cc::No, _) => {
cmd.link_args(&["-syslibroot", &sdk_root]);
cmd.link_arg("-syslibroot");
cmd.link_arg(&sdk_root);
}
_ => unreachable!(),
}

Some(sdk_root.into())
Some(sdk_root)
}

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

let res = Command::new("xcrun")
.arg("--show-sdk-path")
.arg("-sdk")
.arg(sdk_name.to_lowercase())
.output()
.and_then(|output| {
if output.status.success() {
Ok(String::from_utf8(output.stdout).unwrap())
} else {
let error = String::from_utf8(output.stderr);
let error = format!("process exit with error: {}", error.unwrap());
Err(io::Error::new(io::ErrorKind::Other, &error[..]))
}
});

match res {
Ok(output) => Ok(output.trim().to_string()),
Err(error) => Err(errors::AppleSdkRootError::SdkPath { sdk_name, error }),
}
find_sdk_root(sdk_name)
}

/// When using the linker flavors opting in to `lld`, add the necessary paths and arguments to
Expand Down
26 changes: 22 additions & 4 deletions compiler/rustc_codegen_ssa/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,28 @@ pub enum ExtractBundledLibsError<'a> {
ExtractSection { rlib: &'a Path, error: Box<dyn std::error::Error> },
}

#[derive(Diagnostic)]
pub(crate) enum AppleSdkRootError<'a> {
#[diag(codegen_ssa_apple_sdk_error_sdk_path)]
SdkPath { sdk_name: &'a str, error: Error },
#[derive(Diagnostic, Debug)]
pub(crate) enum AppleSdkError {
#[diag(codegen_ssa_apple_sdk_error_failed_reading)]
FailedReading { path: PathBuf, error: std::io::Error },

#[diag(codegen_ssa_apple_sdk_error_missing)]
Missing { sdk_name: &'static str },

#[diag(codegen_ssa_apple_sdk_error_missing_commandline_tools)]
MissingCommandlineTools { sdkroot: PathBuf, sdk_name: &'static str },

#[diag(codegen_ssa_apple_sdk_error_missing_cross_compile_non_macos)]
MissingCrossCompileNonMacOS { sdk_name: &'static str },

#[diag(codegen_ssa_apple_sdk_error_missing_developer_dir)]
MissingDeveloperDir { dir: PathBuf, sdkroot: PathBuf, sdkroot_bare: PathBuf },

#[diag(codegen_ssa_apple_sdk_error_missing_xcode)]
MissingXcode { sdkroot: PathBuf, sdk_name: &'static str },

#[diag(codegen_ssa_apple_sdk_error_missing_xcode_select)]
MissingXcodeSelect { dir: PathBuf, sdkroot: PathBuf, sdkroot_bare: PathBuf },
}

#[derive(Diagnostic)]
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_codegen_ssa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use rustc_session::cstore::{self, CrateSource};
use rustc_session::utils::NativeLibKind;
use rustc_span::symbol::Symbol;

pub mod apple;
pub mod assert_module_sources;
pub mod back;
pub mod base;
Expand Down

0 comments on commit edf397b

Please sign in to comment.