rustc_codegen_ssa/back/
apple.rs

1use std::env;
2use std::ffi::OsString;
3use std::fmt::{Display, from_fn};
4use std::num::ParseIntError;
5use std::path::PathBuf;
6use std::process::Command;
7
8use itertools::Itertools;
9use rustc_middle::middle::exported_symbols::SymbolExportKind;
10use rustc_session::Session;
11use rustc_target::spec::Target;
12use tracing::debug;
13
14use crate::errors::{AppleDeploymentTarget, XcrunError, XcrunSdkPathWarning};
15use crate::fluent_generated as fluent;
16
17#[cfg(test)]
18mod tests;
19
20/// The canonical name of the desired SDK for a given target.
21pub(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
38pub(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/// ```
83pub(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`.
140type OSVersion = (u16, u8, u8);
141
142/// Parse an OS version triple (SDK version or deployment target).
143fn parse_version(version: &str) -> Result<OSVersion, ParseIntError> {
144    if let Some((major, minor)) = version.split_once('.') {
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
156pub fn pretty_version(version: OSVersion) -> impl Display {
157    let (major, minor, patch) = 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`.
168fn 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>
193fn 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, _, _) => os_minimum_deployment_target(os),
204    }
205}
206
207/// Name of the environment variable used to fetch the deployment target on the given OS.
208pub 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`.
221pub 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
255pub(super) fn add_version_to_llvm_target(
256    llvm_target: &str,
257    deployment_target: OSVersion,
258) -> String {
259    let mut components = 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, minor, patch) = 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
281pub(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.
329fn 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).
387fn 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
397fn 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}