rustc_codegen_ssa/back/
apple.rs

1use std::ffi::OsString;
2use std::path::PathBuf;
3use std::process::Command;
4
5use itertools::Itertools;
6use rustc_middle::middle::exported_symbols::SymbolExportKind;
7use rustc_session::Session;
8use rustc_target::spec::Target;
9pub(super) use rustc_target::spec::apple::OSVersion;
10use tracing::debug;
11
12use crate::errors::{XcrunError, XcrunSdkPathWarning};
13use crate::fluent_generated as fluent;
14
15#[cfg(test)]
16mod tests;
17
18/// The canonical name of the desired SDK for a given target.
19pub(super) fn sdk_name(target: &Target) -> &'static str {
20    match (&*target.os, &*target.abi) {
21        ("macos", "") => "MacOSX",
22        ("ios", "") => "iPhoneOS",
23        ("ios", "sim") => "iPhoneSimulator",
24        // Mac Catalyst uses the macOS SDK
25        ("ios", "macabi") => "MacOSX",
26        ("tvos", "") => "AppleTVOS",
27        ("tvos", "sim") => "AppleTVSimulator",
28        ("visionos", "") => "XROS",
29        ("visionos", "sim") => "XRSimulator",
30        ("watchos", "") => "WatchOS",
31        ("watchos", "sim") => "WatchSimulator",
32        (os, abi) => unreachable!("invalid os '{os}' / abi '{abi}' combination for Apple target"),
33    }
34}
35
36pub(super) fn macho_platform(target: &Target) -> u32 {
37    match (&*target.os, &*target.abi) {
38        ("macos", _) => object::macho::PLATFORM_MACOS,
39        ("ios", "macabi") => object::macho::PLATFORM_MACCATALYST,
40        ("ios", "sim") => object::macho::PLATFORM_IOSSIMULATOR,
41        ("ios", _) => object::macho::PLATFORM_IOS,
42        ("watchos", "sim") => object::macho::PLATFORM_WATCHOSSIMULATOR,
43        ("watchos", _) => object::macho::PLATFORM_WATCHOS,
44        ("tvos", "sim") => object::macho::PLATFORM_TVOSSIMULATOR,
45        ("tvos", _) => object::macho::PLATFORM_TVOS,
46        ("visionos", "sim") => object::macho::PLATFORM_XROSSIMULATOR,
47        ("visionos", _) => object::macho::PLATFORM_XROS,
48        _ => unreachable!("tried to get Mach-O platform for non-Apple target"),
49    }
50}
51
52/// Add relocation and section data needed for a symbol to be considered
53/// undefined by ld64.
54///
55/// The relocation must be valid, and hence must point to a valid piece of
56/// machine code, and hence this is unfortunately very architecture-specific.
57///
58///
59/// # New architectures
60///
61/// The values here are basically the same as emitted by the following program:
62///
63/// ```c
64/// // clang -c foo.c -target $CLANG_TARGET
65/// void foo(void);
66///
67/// extern int bar;
68///
69/// void* foobar[2] = {
70///     (void*)foo,
71///     (void*)&bar,
72///     // ...
73/// };
74/// ```
75///
76/// Can be inspected with:
77/// ```console
78/// objdump --macho --reloc foo.o
79/// objdump --macho --full-contents foo.o
80/// ```
81pub(super) fn add_data_and_relocation(
82    file: &mut object::write::Object<'_>,
83    section: object::write::SectionId,
84    symbol: object::write::SymbolId,
85    target: &Target,
86    kind: SymbolExportKind,
87) -> object::write::Result<()> {
88    let authenticated_pointer =
89        kind == SymbolExportKind::Text && target.llvm_target.starts_with("arm64e");
90
91    let data: &[u8] = match target.pointer_width {
92        _ if authenticated_pointer => &[0, 0, 0, 0, 0, 0, 0, 0x80],
93        32 => &[0; 4],
94        64 => &[0; 8],
95        pointer_width => unimplemented!("unsupported Apple pointer width {pointer_width:?}"),
96    };
97
98    if target.arch == "x86_64" {
99        // Force alignment for the entire section to be 16 on x86_64.
100        file.section_mut(section).append_data(&[], 16);
101    } else {
102        // Elsewhere, the section alignment is the same as the pointer width.
103        file.section_mut(section).append_data(&[], target.pointer_width as u64);
104    }
105
106    let offset = file.section_mut(section).append_data(data, data.len() as u64);
107
108    let flags = if authenticated_pointer {
109        object::write::RelocationFlags::MachO {
110            r_type: object::macho::ARM64_RELOC_AUTHENTICATED_POINTER,
111            r_pcrel: false,
112            r_length: 3,
113        }
114    } else if target.arch == "arm" {
115        // FIXME(madsmtm): Remove once `object` supports 32-bit ARM relocations:
116        // https://github.com/gimli-rs/object/pull/757
117        object::write::RelocationFlags::MachO {
118            r_type: object::macho::ARM_RELOC_VANILLA,
119            r_pcrel: false,
120            r_length: 2,
121        }
122    } else {
123        object::write::RelocationFlags::Generic {
124            kind: object::RelocationKind::Absolute,
125            encoding: object::RelocationEncoding::Generic,
126            size: target.pointer_width as u8,
127        }
128    };
129
130    file.add_relocation(section, object::write::Relocation { offset, addend: 0, symbol, flags })?;
131
132    Ok(())
133}
134
135pub(super) fn add_version_to_llvm_target(
136    llvm_target: &str,
137    deployment_target: OSVersion,
138) -> String {
139    let mut components = llvm_target.split("-");
140    let arch = components.next().expect("apple target should have arch");
141    let vendor = components.next().expect("apple target should have vendor");
142    let os = components.next().expect("apple target should have os");
143    let environment = components.next();
144    assert_eq!(components.next(), None, "too many LLVM triple components");
145
146    assert!(
147        !os.contains(|c: char| c.is_ascii_digit()),
148        "LLVM target must not already be versioned"
149    );
150
151    let version = deployment_target.fmt_full();
152    if let Some(env) = environment {
153        // Insert version into OS, before environment
154        format!("{arch}-{vendor}-{os}{version}-{env}")
155    } else {
156        format!("{arch}-{vendor}-{os}{version}")
157    }
158}
159
160pub(super) fn get_sdk_root(sess: &Session) -> Option<PathBuf> {
161    let sdk_name = sdk_name(&sess.target);
162
163    match xcrun_show_sdk_path(sdk_name, sess.verbose_internals()) {
164        Ok((path, stderr)) => {
165            // Emit extra stderr, such as if `-verbose` was passed, or if `xcrun` emitted a warning.
166            if !stderr.is_empty() {
167                sess.dcx().emit_warn(XcrunSdkPathWarning { sdk_name, stderr });
168            }
169            Some(path)
170        }
171        Err(err) => {
172            let mut diag = sess.dcx().create_err(err);
173
174            // Recognize common error cases, and give more Rust-specific error messages for those.
175            if let Some(developer_dir) = xcode_select_developer_dir() {
176                diag.arg("developer_dir", &developer_dir);
177                diag.note(fluent::codegen_ssa_xcrun_found_developer_dir);
178                if developer_dir.as_os_str().to_string_lossy().contains("CommandLineTools") {
179                    if sdk_name != "MacOSX" {
180                        diag.help(fluent::codegen_ssa_xcrun_command_line_tools_insufficient);
181                    }
182                }
183            } else {
184                diag.help(fluent::codegen_ssa_xcrun_no_developer_dir);
185            }
186
187            diag.emit();
188            None
189        }
190    }
191}
192
193/// Invoke `xcrun --sdk $sdk_name --show-sdk-path` to get the SDK path.
194///
195/// The exact logic that `xcrun` uses is unspecified (see `man xcrun` for a few details), and may
196/// change between macOS and Xcode versions, but it roughly boils down to finding the active
197/// developer directory, and then invoking `xcodebuild -sdk $sdk_name -version` to get the SDK
198/// details.
199///
200/// Finding the developer directory is roughly done by looking at, in order:
201/// - The `DEVELOPER_DIR` environment variable.
202/// - The `/var/db/xcode_select_link` symlink (set by `xcode-select --switch`).
203/// - `/Applications/Xcode.app` (hardcoded fallback path).
204/// - `/Library/Developer/CommandLineTools` (hardcoded fallback path).
205///
206/// Note that `xcrun` caches its result, but with a cold cache this whole operation can be quite
207/// slow, especially so the first time it's run after a reboot.
208fn xcrun_show_sdk_path(
209    sdk_name: &'static str,
210    verbose: bool,
211) -> Result<(PathBuf, String), XcrunError> {
212    let mut cmd = Command::new("xcrun");
213    if verbose {
214        cmd.arg("--verbose");
215    }
216    // The `--sdk` parameter is the same as in xcodebuild, namely either an absolute path to an SDK,
217    // or the (lowercase) canonical name of an SDK.
218    cmd.arg("--sdk");
219    cmd.arg(&sdk_name.to_lowercase());
220    cmd.arg("--show-sdk-path");
221
222    // We do not stream stdout/stderr lines directly to the user, since whether they are warnings or
223    // errors depends on the status code at the end.
224    let output = cmd.output().map_err(|error| XcrunError::FailedInvoking {
225        sdk_name,
226        command_formatted: format!("{cmd:?}"),
227        error,
228    })?;
229
230    // It is fine to do lossy conversion here, non-UTF-8 paths are quite rare on macOS nowadays
231    // (only possible with the HFS+ file system), and we only use it for error messages.
232    let stderr = String::from_utf8_lossy_owned(output.stderr);
233    if !stderr.is_empty() {
234        debug!(stderr, "original xcrun stderr");
235    }
236
237    // Some versions of `xcodebuild` output beefy errors when invoked via `xcrun`,
238    // but these are usually red herrings.
239    let stderr = stderr
240        .lines()
241        .filter(|line| {
242            !line.contains("Writing error result bundle")
243                && !line.contains("Requested but did not find extension point with identifier")
244        })
245        .join("\n");
246
247    if output.status.success() {
248        Ok((stdout_to_path(output.stdout), stderr))
249    } else {
250        // Output both stdout and stderr, since shims of `xcrun` (such as the one provided by
251        // nixpkgs), do not always use stderr for errors.
252        let stdout = String::from_utf8_lossy_owned(output.stdout).trim().to_string();
253        Err(XcrunError::Unsuccessful {
254            sdk_name,
255            command_formatted: format!("{cmd:?}"),
256            stdout,
257            stderr,
258        })
259    }
260}
261
262/// Invoke `xcode-select --print-path`, and return the current developer directory.
263///
264/// NOTE: We don't do any error handling here, this is only used as a canary in diagnostics (`xcrun`
265/// will have already emitted the relevant error information).
266fn xcode_select_developer_dir() -> Option<PathBuf> {
267    let mut cmd = Command::new("xcode-select");
268    cmd.arg("--print-path");
269    let output = cmd.output().ok()?;
270    if !output.status.success() {
271        return None;
272    }
273    Some(stdout_to_path(output.stdout))
274}
275
276fn stdout_to_path(mut stdout: Vec<u8>) -> PathBuf {
277    // Remove trailing newline.
278    if let Some(b'\n') = stdout.last() {
279        let _ = stdout.pop().unwrap();
280    }
281    #[cfg(unix)]
282    let path = <OsString as std::os::unix::ffi::OsStringExt>::from_vec(stdout);
283    #[cfg(not(unix))] // Unimportant, this is only used on macOS
284    let path = OsString::from(String::from_utf8(stdout).unwrap());
285    PathBuf::from(path)
286}