rustc_codegen_ssa/back/
apple.rs

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