| 
1 | 1 | use std::env;  | 
 | 2 | +use std::ffi::OsString;  | 
2 | 3 | use std::fmt::{Display, from_fn};  | 
3 | 4 | use std::num::ParseIntError;  | 
 | 5 | +use std::path::PathBuf;  | 
 | 6 | +use std::process::Command;  | 
4 | 7 | 
 
  | 
 | 8 | +use itertools::Itertools;  | 
5 | 9 | use rustc_middle::middle::exported_symbols::SymbolExportKind;  | 
6 | 10 | use rustc_session::Session;  | 
7 | 11 | use rustc_target::spec::Target;  | 
 | 12 | +use tracing::debug;  | 
8 | 13 | 
 
  | 
9 |  | -use crate::errors::AppleDeploymentTarget;  | 
 | 14 | +use crate::errors::{AppleDeploymentTarget, XcrunError, XcrunSdkPathWarning};  | 
 | 15 | +use crate::fluent_generated as fluent;  | 
10 | 16 | 
 
  | 
11 | 17 | #[cfg(test)]  | 
12 | 18 | mod tests;  | 
13 | 19 | 
 
  | 
 | 20 | +/// The canonical name of the desired SDK for a given target.  | 
 | 21 | +pub(super) fn sdk_name(target: &Target) -> &'static str {  | 
 | 22 | +    match (&*target.os, &*target.abi) {  | 
 | 23 | +        ("macos", "") => "MacOSX",  | 
 | 24 | +        ("ios", "") => "iPhoneOS",  | 
 | 25 | +        ("ios", "sim") => "iPhoneSimulator",  | 
 | 26 | +        // Mac Catalyst uses the macOS SDK  | 
 | 27 | +        ("ios", "macabi") => "MacOSX",  | 
 | 28 | +        ("tvos", "") => "AppleTVOS",  | 
 | 29 | +        ("tvos", "sim") => "AppleTVSimulator",  | 
 | 30 | +        ("visionos", "") => "XROS",  | 
 | 31 | +        ("visionos", "sim") => "XRSimulator",  | 
 | 32 | +        ("watchos", "") => "WatchOS",  | 
 | 33 | +        ("watchos", "sim") => "WatchSimulator",  | 
 | 34 | +        (os, abi) => unreachable!("invalid os '{os}' / abi '{abi}' combination for Apple target"),  | 
 | 35 | +    }  | 
 | 36 | +}  | 
 | 37 | + | 
14 | 38 | pub(super) fn macho_platform(target: &Target) -> u32 {  | 
15 | 39 |     match (&*target.os, &*target.abi) {  | 
16 | 40 |         ("macos", _) => object::macho::PLATFORM_MACOS,  | 
@@ -253,3 +277,131 @@ pub(super) fn add_version_to_llvm_target(  | 
253 | 277 |         format!("{arch}-{vendor}-{os}{major}.{minor}.{patch}")  | 
254 | 278 |     }  | 
255 | 279 | }  | 
 | 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 | +}  | 
0 commit comments