From 24e7da12d92a9c178319d7a824d5318ae9a30feb Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 9 Oct 2024 04:57:56 +0200 Subject: [PATCH] Re-implement SDK discovery instead of using xcrun --- compiler/rustc_codegen_ssa/messages.ftl | 60 +++++++- compiler/rustc_codegen_ssa/src/back/apple.rs | 134 +++++++++++++++++- .../rustc_codegen_ssa/src/back/apple/tests.rs | 59 +++++++- compiler/rustc_codegen_ssa/src/back/link.rs | 38 ++--- compiler/rustc_codegen_ssa/src/errors.rs | 26 +++- .../src/platform-support/apple-darwin.md | 3 +- .../src/platform-support/apple-ios-macabi.md | 3 +- .../rustc/src/platform-support/apple-ios.md | 3 +- .../rustc/src/platform-support/apple-tvos.md | 3 +- .../src/platform-support/apple-visionos.md | 3 +- .../src/platform-support/apple-watchos.md | 3 +- 11 files changed, 294 insertions(+), 41 deletions(-) diff --git a/compiler/rustc_codegen_ssa/messages.ftl b/compiler/rustc_codegen_ssa/messages.ftl index 9751d043c9867..4924b41242fcd 100644 --- a/compiler/rustc_codegen_ssa/messages.ftl +++ b/compiler/rustc_codegen_ssa/messages.ftl @@ -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} diff --git a/compiler/rustc_codegen_ssa/src/back/apple.rs b/compiler/rustc_codegen_ssa/src/back/apple.rs index 48f5b7e662047..e990928967a10 100644 --- a/compiler/rustc_codegen_ssa/src/back/apple.rs +++ b/compiler/rustc_codegen_ssa/src/back/apple.rs @@ -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; @@ -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 { + 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, 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, 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 { + // 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 }) +} diff --git a/compiler/rustc_codegen_ssa/src/back/apple/tests.rs b/compiler/rustc_codegen_ssa/src/back/apple/tests.rs index 7ccda5a8190c7..6ebc01d376830 100644 --- a/compiler/rustc_codegen_ssa/src/back/apple/tests.rs +++ b/compiler/rustc_codegen_ssa/src/back/apple/tests.rs @@ -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() { @@ -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 { + 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`"); + } + } + } +} diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs index e644a864a4854..9cf0c7e392d3c 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -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> { +fn get_apple_sdk_root(sdk_name: &'static str) -> Result { // 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" @@ -3202,29 +3202,11 @@ fn get_apple_sdk_root(sdk_name: &str) -> Result {} // 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 diff --git a/compiler/rustc_codegen_ssa/src/errors.rs b/compiler/rustc_codegen_ssa/src/errors.rs index 44c4fab573beb..6c3eedbec91d4 100644 --- a/compiler/rustc_codegen_ssa/src/errors.rs +++ b/compiler/rustc_codegen_ssa/src/errors.rs @@ -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)] diff --git a/src/doc/rustc/src/platform-support/apple-darwin.md b/src/doc/rustc/src/platform-support/apple-darwin.md index 17ea225805b5c..31ae642fc93fd 100644 --- a/src/doc/rustc/src/platform-support/apple-darwin.md +++ b/src/doc/rustc/src/platform-support/apple-darwin.md @@ -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`. diff --git a/src/doc/rustc/src/platform-support/apple-ios-macabi.md b/src/doc/rustc/src/platform-support/apple-ios-macabi.md index a54656190d1f3..2d54e15f16314 100644 --- a/src/doc/rustc/src/platform-support/apple-ios-macabi.md +++ b/src/doc/rustc/src/platform-support/apple-ios-macabi.md @@ -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 diff --git a/src/doc/rustc/src/platform-support/apple-ios.md b/src/doc/rustc/src/platform-support/apple-ios.md index 5045f810400f5..c1c34118ad986 100644 --- a/src/doc/rustc/src/platform-support/apple-ios.md +++ b/src/doc/rustc/src/platform-support/apple-ios.md @@ -26,7 +26,8 @@ These targets are cross-compiled, and require the corresponding iOS SDK ARM64 targets, Xcode 12 or higher is required. 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 iphoneos -show-sdk-path`. ### OS version diff --git a/src/doc/rustc/src/platform-support/apple-tvos.md b/src/doc/rustc/src/platform-support/apple-tvos.md index 7a3b601579a01..761f7a7ffb903 100644 --- a/src/doc/rustc/src/platform-support/apple-tvos.md +++ b/src/doc/rustc/src/platform-support/apple-tvos.md @@ -20,7 +20,8 @@ These targets are cross-compiled, and require the corresponding tvOS SDK ARM64 targets, Xcode 12 or higher is required. 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 appletvos -show-sdk-path`. ### OS version diff --git a/src/doc/rustc/src/platform-support/apple-visionos.md b/src/doc/rustc/src/platform-support/apple-visionos.md index 56224d7e20d4e..df9a173b8b414 100644 --- a/src/doc/rustc/src/platform-support/apple-visionos.md +++ b/src/doc/rustc/src/platform-support/apple-visionos.md @@ -18,7 +18,8 @@ These targets are cross-compiled, and require the corresponding visionOS SDK (`XROS.sdk` or `XRSimulator.sdk`), as provided by Xcode 15 or newer. 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 xros -show-sdk-path`. ### OS version diff --git a/src/doc/rustc/src/platform-support/apple-watchos.md b/src/doc/rustc/src/platform-support/apple-watchos.md index 8ba35f70b8578..f6f9bc5cac337 100644 --- a/src/doc/rustc/src/platform-support/apple-watchos.md +++ b/src/doc/rustc/src/platform-support/apple-watchos.md @@ -24,7 +24,8 @@ These targets are cross-compiled, and require the corresponding watchOS SDK ARM64 targets, Xcode 12 or higher is required. 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 watchos -show-sdk-path`. ### OS version