| 1 | use std::env; |
| 2 | use std::ffi::OsString; |
| 3 | use std::fmt::{Display, from_fn}; |
| 4 | use std::num::ParseIntError; |
| 5 | use std::path::PathBuf; |
| 6 | use std::process::Command; |
| 7 | |
| 8 | use itertools::Itertools; |
| 9 | use rustc_middle::middle::exported_symbols::SymbolExportKind; |
| 10 | use rustc_session::Session; |
| 11 | use rustc_target::spec::Target; |
| 12 | use tracing::debug; |
| 13 | |
| 14 | use crate::errors::{AppleDeploymentTarget, XcrunError, XcrunSdkPathWarning}; |
| 15 | use crate::fluent_generated as fluent; |
| 16 | |
| 17 | #[cfg (test)] |
| 18 | mod tests; |
| 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: &str, abi: &str) => unreachable!("invalid os '{os}' / abi '{abi}' combination for Apple target" ), |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | pub(super) fn macho_platform(target: &Target) -> u32 { |
| 39 | match (&*target.os, &*target.abi) { |
| 40 | ("macos" , _) => object::macho::PLATFORM_MACOS, |
| 41 | ("ios" , "macabi" ) => object::macho::PLATFORM_MACCATALYST, |
| 42 | ("ios" , "sim" ) => object::macho::PLATFORM_IOSSIMULATOR, |
| 43 | ("ios" , _) => object::macho::PLATFORM_IOS, |
| 44 | ("watchos" , "sim" ) => object::macho::PLATFORM_WATCHOSSIMULATOR, |
| 45 | ("watchos" , _) => object::macho::PLATFORM_WATCHOS, |
| 46 | ("tvos" , "sim" ) => object::macho::PLATFORM_TVOSSIMULATOR, |
| 47 | ("tvos" , _) => object::macho::PLATFORM_TVOS, |
| 48 | ("visionos" , "sim" ) => object::macho::PLATFORM_XROSSIMULATOR, |
| 49 | ("visionos" , _) => object::macho::PLATFORM_XROS, |
| 50 | _ => unreachable!("tried to get Mach-O platform for non-Apple target" ), |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | /// Add relocation and section data needed for a symbol to be considered |
| 55 | /// undefined by ld64. |
| 56 | /// |
| 57 | /// The relocation must be valid, and hence must point to a valid piece of |
| 58 | /// machine code, and hence this is unfortunately very architecture-specific. |
| 59 | /// |
| 60 | /// |
| 61 | /// # New architectures |
| 62 | /// |
| 63 | /// The values here are basically the same as emitted by the following program: |
| 64 | /// |
| 65 | /// ```c |
| 66 | /// // clang -c foo.c -target $CLANG_TARGET |
| 67 | /// void foo(void); |
| 68 | /// |
| 69 | /// extern int bar; |
| 70 | /// |
| 71 | /// void* foobar[2] = { |
| 72 | /// (void*)foo, |
| 73 | /// (void*)&bar, |
| 74 | /// // ... |
| 75 | /// }; |
| 76 | /// ``` |
| 77 | /// |
| 78 | /// Can be inspected with: |
| 79 | /// ```console |
| 80 | /// objdump --macho --reloc foo.o |
| 81 | /// objdump --macho --full-contents foo.o |
| 82 | /// ``` |
| 83 | pub(super) fn add_data_and_relocation( |
| 84 | file: &mut object::write::Object<'_>, |
| 85 | section: object::write::SectionId, |
| 86 | symbol: object::write::SymbolId, |
| 87 | target: &Target, |
| 88 | kind: SymbolExportKind, |
| 89 | ) -> object::write::Result<()> { |
| 90 | let authenticated_pointer = |
| 91 | kind == SymbolExportKind::Text && target.llvm_target.starts_with("arm64e" ); |
| 92 | |
| 93 | let data: &[u8] = match target.pointer_width { |
| 94 | _ if authenticated_pointer => &[0, 0, 0, 0, 0, 0, 0, 0x80], |
| 95 | 32 => &[0; 4], |
| 96 | 64 => &[0; 8], |
| 97 | pointer_width => unimplemented!("unsupported Apple pointer width {pointer_width:?}" ), |
| 98 | }; |
| 99 | |
| 100 | if target.arch == "x86_64" { |
| 101 | // Force alignment for the entire section to be 16 on x86_64. |
| 102 | file.section_mut(section).append_data(&[], 16); |
| 103 | } else { |
| 104 | // Elsewhere, the section alignment is the same as the pointer width. |
| 105 | file.section_mut(section).append_data(&[], target.pointer_width as u64); |
| 106 | } |
| 107 | |
| 108 | let offset = file.section_mut(section).append_data(data, data.len() as u64); |
| 109 | |
| 110 | let flags = if authenticated_pointer { |
| 111 | object::write::RelocationFlags::MachO { |
| 112 | r_type: object::macho::ARM64_RELOC_AUTHENTICATED_POINTER, |
| 113 | r_pcrel: false, |
| 114 | r_length: 3, |
| 115 | } |
| 116 | } else if target.arch == "arm" { |
| 117 | // FIXME(madsmtm): Remove once `object` supports 32-bit ARM relocations: |
| 118 | // https://github.com/gimli-rs/object/pull/757 |
| 119 | object::write::RelocationFlags::MachO { |
| 120 | r_type: object::macho::ARM_RELOC_VANILLA, |
| 121 | r_pcrel: false, |
| 122 | r_length: 2, |
| 123 | } |
| 124 | } else { |
| 125 | object::write::RelocationFlags::Generic { |
| 126 | kind: object::RelocationKind::Absolute, |
| 127 | encoding: object::RelocationEncoding::Generic, |
| 128 | size: target.pointer_width as u8, |
| 129 | } |
| 130 | }; |
| 131 | |
| 132 | file.add_relocation(section, object::write::Relocation { offset, addend: 0, symbol, flags })?; |
| 133 | |
| 134 | Ok(()) |
| 135 | } |
| 136 | |
| 137 | /// Deployment target or SDK version. |
| 138 | /// |
| 139 | /// The size of the numbers in here are limited by Mach-O's `LC_BUILD_VERSION`. |
| 140 | type OSVersion = (u16, u8, u8); |
| 141 | |
| 142 | /// Parse an OS version triple (SDK version or deployment target). |
| 143 | fn parse_version(version: &str) -> Result<OSVersion, ParseIntError> { |
| 144 | if let Some((major, minor)) = version.split_once(delimiter:'.' ) { |
| 145 | let major: ! = major.parse()?; |
| 146 | if let Some((minor, patch)) = minor.split_once('.' ) { |
| 147 | Ok((major, minor.parse()?, patch.parse()?)) |
| 148 | } else { |
| 149 | Ok((major, minor.parse()?, 0)) |
| 150 | } |
| 151 | } else { |
| 152 | Ok((version.parse()?, 0, 0)) |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | pub fn pretty_version(version: OSVersion) -> impl Display { |
| 157 | let (major: u16, minor: u8, patch: u8) = version; |
| 158 | from_fn(move |f| { |
| 159 | write!(f, "{major}.{minor}" )?; |
| 160 | if patch != 0 { |
| 161 | write!(f, ".{patch}" )?; |
| 162 | } |
| 163 | Ok(()) |
| 164 | }) |
| 165 | } |
| 166 | |
| 167 | /// Minimum operating system versions currently supported by `rustc`. |
| 168 | fn os_minimum_deployment_target(os: &str) -> OSVersion { |
| 169 | // When bumping a version in here, remember to update the platform-support docs too. |
| 170 | // |
| 171 | // NOTE: The defaults may change in future `rustc` versions, so if you are looking for the |
| 172 | // default deployment target, prefer: |
| 173 | // ``` |
| 174 | // $ rustc --print deployment-target |
| 175 | // ``` |
| 176 | match os { |
| 177 | "macos" => (10, 12, 0), |
| 178 | "ios" => (10, 0, 0), |
| 179 | "tvos" => (10, 0, 0), |
| 180 | "watchos" => (5, 0, 0), |
| 181 | "visionos" => (1, 0, 0), |
| 182 | _ => unreachable!("tried to get deployment target for non-Apple platform" ), |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | /// The deployment target for the given target. |
| 187 | /// |
| 188 | /// This is similar to `os_minimum_deployment_target`, except that on certain targets it makes sense |
| 189 | /// to raise the minimum OS version. |
| 190 | /// |
| 191 | /// This matches what LLVM does, see in part: |
| 192 | /// <https://github.com/llvm/llvm-project/blob/llvmorg-18.1.8/llvm/lib/TargetParser/Triple.cpp#L1900-L1932> |
| 193 | fn minimum_deployment_target(target: &Target) -> OSVersion { |
| 194 | match (&*target.os, &*target.arch, &*target.abi) { |
| 195 | ("macos" , "aarch64" , _) => (11, 0, 0), |
| 196 | ("ios" , "aarch64" , "macabi" ) => (14, 0, 0), |
| 197 | ("ios" , "aarch64" , "sim" ) => (14, 0, 0), |
| 198 | ("ios" , _, _) if target.llvm_target.starts_with("arm64e" ) => (14, 0, 0), |
| 199 | // Mac Catalyst defaults to 13.1 in Clang. |
| 200 | ("ios" , _, "macabi" ) => (13, 1, 0), |
| 201 | ("tvos" , "aarch64" , "sim" ) => (14, 0, 0), |
| 202 | ("watchos" , "aarch64" , "sim" ) => (7, 0, 0), |
| 203 | (os: &str, _, _) => os_minimum_deployment_target(os), |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | /// Name of the environment variable used to fetch the deployment target on the given OS. |
| 208 | pub fn deployment_target_env_var(os: &str) -> &'static str { |
| 209 | match os { |
| 210 | "macos" => "MACOSX_DEPLOYMENT_TARGET" , |
| 211 | "ios" => "IPHONEOS_DEPLOYMENT_TARGET" , |
| 212 | "watchos" => "WATCHOS_DEPLOYMENT_TARGET" , |
| 213 | "tvos" => "TVOS_DEPLOYMENT_TARGET" , |
| 214 | "visionos" => "XROS_DEPLOYMENT_TARGET" , |
| 215 | _ => unreachable!("tried to get deployment target env var for non-Apple platform" ), |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | /// Get the deployment target based on the standard environment variables, or fall back to the |
| 220 | /// minimum version supported by `rustc`. |
| 221 | pub fn deployment_target(sess: &Session) -> OSVersion { |
| 222 | let min = minimum_deployment_target(&sess.target); |
| 223 | let env_var = deployment_target_env_var(&sess.target.os); |
| 224 | |
| 225 | if let Ok(deployment_target) = env::var(env_var) { |
| 226 | match parse_version(&deployment_target) { |
| 227 | Ok(version) => { |
| 228 | let os_min = os_minimum_deployment_target(&sess.target.os); |
| 229 | // It is common that the deployment target is set a bit too low, for example on |
| 230 | // macOS Aarch64 to also target older x86_64. So we only want to warn when variable |
| 231 | // is lower than the minimum OS supported by rustc, not when the variable is lower |
| 232 | // than the minimum for a specific target. |
| 233 | if version < os_min { |
| 234 | sess.dcx().emit_warn(AppleDeploymentTarget::TooLow { |
| 235 | env_var, |
| 236 | version: pretty_version(version).to_string(), |
| 237 | os_min: pretty_version(os_min).to_string(), |
| 238 | }); |
| 239 | } |
| 240 | |
| 241 | // Raise the deployment target to the minimum supported. |
| 242 | version.max(min) |
| 243 | } |
| 244 | Err(error) => { |
| 245 | sess.dcx().emit_err(AppleDeploymentTarget::Invalid { env_var, error }); |
| 246 | min |
| 247 | } |
| 248 | } |
| 249 | } else { |
| 250 | // If no deployment target variable is set, default to the minimum found above. |
| 251 | min |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | pub(super) fn add_version_to_llvm_target( |
| 256 | llvm_target: &str, |
| 257 | deployment_target: OSVersion, |
| 258 | ) -> String { |
| 259 | let mut components: Split<'_, &'static str> = llvm_target.split("-" ); |
| 260 | let arch = components.next().expect("apple target should have arch" ); |
| 261 | let vendor = components.next().expect("apple target should have vendor" ); |
| 262 | let os = components.next().expect("apple target should have os" ); |
| 263 | let environment = components.next(); |
| 264 | assert_eq!(components.next(), None, "too many LLVM triple components" ); |
| 265 | |
| 266 | let (major: u16, minor: u8, patch: u8) = deployment_target; |
| 267 | |
| 268 | assert!( |
| 269 | !os.contains(|c: char| c.is_ascii_digit()), |
| 270 | "LLVM target must not already be versioned" |
| 271 | ); |
| 272 | |
| 273 | if let Some(env) = environment { |
| 274 | // Insert version into OS, before environment |
| 275 | format!("{arch}-{vendor}-{os}{major}.{minor}.{patch}-{env}" ) |
| 276 | } else { |
| 277 | format!("{arch}-{vendor}-{os}{major}.{minor}.{patch}" ) |
| 278 | } |
| 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 = Command::new(program:"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 = <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 | } |
| 408 | |