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 Nov 3, 2024
1 parent 168951c commit 24e7da1
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 41 deletions.
60 changes: 59 additions & 1 deletion compiler/rustc_codegen_ssa/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,65 @@ codegen_ssa_apple_deployment_target_invalid =
codegen_ssa_apple_deployment_target_too_low =
deployment target in {$env_var} was set to {$version}, but the minimum supported by `rustc` is {$os_min}
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` to install the Command Line Tools 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 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 and for embedding the SDK version in the final object file.
The SDK can be downloaded and extracted from https://developer.apple.com/download/all/?q=xcode (requires an Apple ID).
The full Xcode bundle should contain the SDK in `Xcode.app/Contents/Developer/Platforms/{$sdk_name}.platform/Developer/SDKs/{$sdk_name}.sdk`{ $sdk_name ->
[MacOSX] , but downloading just the Command Line Tools for Xcode should also be sufficient to obtain the macOS SDK.
*[other] .
}
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 `rustc` is configured to use the bundled `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 directory `{$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 directory `{$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
134 changes: 133 additions & 1 deletion compiler/rustc_codegen_ssa/src/back/apple.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use std::env;
use std::fmt::{Display, from_fn};
use std::num::ParseIntError;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};

use rustc_session::Session;
use rustc_target::spec::Target;

use crate::errors::AppleDeploymentTarget;
use crate::errors::{AppleDeploymentTarget, AppleSdkError};

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -186,3 +189,132 @@ pub(super) fn add_version_to_llvm_target(
format!("{arch}-{vendor}-{os}{major}.{minor}.{patch}")
}
}

// 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 can change anytime anyhow in
// ways that are going to interfere much more with the compilation process.
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 the 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`.
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 = Path::new("/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: path.into(), error }),
}

// Next, fall back to reading from `/Applications/Xcode.app`.
let dir = Path::new("/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 = Path::new("/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 })
}
59 changes: 58 additions & 1 deletion compiler/rustc_codegen_ssa/src/back/apple/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use super::{add_version_to_llvm_target, parse_version};
use std::io;
use std::path::PathBuf;
use std::process::Command;

use super::{add_version_to_llvm_target, find_sdk_root, parse_version};

#[test]
fn test_add_version_to_llvm_target() {
Expand All @@ -19,3 +23,56 @@ fn test_parse_version() {
assert_eq!(parse_version("10.12.6"), Ok((10, 12, 6)));
assert_eq!(parse_version("9999.99.99"), Ok((9999, 99, 99)));
}

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 = std::str::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`");
}
}
}
}
38 changes: 10 additions & 28 deletions compiler/rustc_codegen_ssa/src/back/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3154,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 @@ -3202,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 }),
}
apple::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 @@ -541,10 +541,28 @@ pub(crate) enum AppleDeploymentTarget {
TooLow { env_var: &'static str, version: String, os_min: String },
}

#[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
3 changes: 2 additions & 1 deletion src/doc/rustc/src/platform-support/apple-darwin.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ the `-mmacosx-version-min=...`, `-miphoneos-version-min=...` or similar flags
to disambiguate.

The path to the SDK can be passed to `rustc` using the common `SDKROOT`
environment variable.
environment variable, or will be inferred when compiling on host macOS using
roughly the same logic as `xcrun -sdk macosx -show-sdk-path`.
3 changes: 2 additions & 1 deletion src/doc/rustc/src/platform-support/apple-ios-macabi.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ These targets are cross-compiled, and require the corresponding macOS SDK
iOS-specific headers, as provided by Xcode 11 or higher.

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

### OS version

Expand Down
Loading

0 comments on commit 24e7da1

Please sign in to comment.