Rust · 9531 bytes Raw Blame History
1 use std::fs;
2 use std::path::{Path, PathBuf};
3 use std::process::Command;
4
5 use afs_ld::macho::constants::*;
6 use afs_ld::macho::reader::{parse_commands, parse_header, LoadCommand};
7
8 fn have_xcrun() -> bool {
9 Command::new("xcrun")
10 .arg("-f")
11 .arg("as")
12 .output()
13 .map(|o| o.status.success())
14 .unwrap_or(false)
15 }
16
17 fn have_clang() -> bool {
18 Command::new("xcrun")
19 .arg("-f")
20 .arg("clang")
21 .output()
22 .map(|o| o.status.success())
23 .unwrap_or(false)
24 }
25
26 fn have_ld() -> bool {
27 Command::new("xcrun")
28 .arg("-f")
29 .arg("ld")
30 .output()
31 .map(|o| o.status.success())
32 .unwrap_or(false)
33 }
34
35 fn sdk_path() -> Option<String> {
36 Command::new("xcrun")
37 .args(["--sdk", "macosx", "--show-sdk-path"])
38 .output()
39 .ok()
40 .filter(|out| out.status.success())
41 .and_then(|out| String::from_utf8(out.stdout).ok())
42 .map(|text| text.trim().to_string())
43 .filter(|text| !text.is_empty())
44 }
45
46 fn scratch(name: &str) -> PathBuf {
47 std::env::temp_dir().join(format!("afs-ld-load-order-{}-{name}", std::process::id()))
48 }
49
50 fn assemble(src_text: &str, out: &Path) -> Result<(), String> {
51 let tmp = std::env::temp_dir().join(format!(
52 "afs-ld-load-order-{}-{}.s",
53 std::process::id(),
54 out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
55 ));
56 fs::write(&tmp, src_text).map_err(|e| format!("write .s: {e}"))?;
57 let status = Command::new("xcrun")
58 .args(["--sdk", "macosx", "as", "-arch", "arm64"])
59 .arg(&tmp)
60 .arg("-o")
61 .arg(out)
62 .output()
63 .map_err(|e| format!("spawn xcrun as: {e}"))?;
64 let _ = fs::remove_file(&tmp);
65 if !status.status.success() {
66 return Err(format!(
67 "xcrun as failed: {}",
68 String::from_utf8_lossy(&status.stderr)
69 ));
70 }
71 Ok(())
72 }
73
74 fn build_test_dylib(src: &str, out: &Path, install_name: &str) -> Result<(), String> {
75 let mut child = Command::new("xcrun")
76 .args([
77 "--sdk", "macosx", "clang", "-x", "c", "-arch", "arm64", "-shared", "-o",
78 ])
79 .arg(out)
80 .arg("-install_name")
81 .arg(install_name)
82 .arg("-")
83 .stdin(std::process::Stdio::piped())
84 .stdout(std::process::Stdio::piped())
85 .stderr(std::process::Stdio::piped())
86 .spawn()
87 .map_err(|e| format!("spawn clang: {e}"))?;
88 use std::io::Write;
89 child
90 .stdin
91 .as_mut()
92 .unwrap()
93 .write_all(src.as_bytes())
94 .map_err(|e| format!("write clang stdin: {e}"))?;
95 let out = child.wait_with_output().map_err(|e| format!("wait: {e}"))?;
96 if !out.status.success() {
97 return Err(format!(
98 "clang failed: {}",
99 String::from_utf8_lossy(&out.stderr)
100 ));
101 }
102 Ok(())
103 }
104
105 fn link_with_afs_ld(args: &[&str]) -> Result<(), String> {
106 let exe = env!("CARGO_BIN_EXE_afs-ld");
107 let out = Command::new(exe)
108 .args(args)
109 .output()
110 .map_err(|e| format!("spawn afs-ld: {e}"))?;
111 if !out.status.success() {
112 return Err(format!(
113 "afs-ld failed: {}\n{}",
114 out.status,
115 String::from_utf8_lossy(&out.stderr)
116 ));
117 }
118 Ok(())
119 }
120
121 fn command_ids(path: &Path) -> Vec<u32> {
122 let bytes = fs::read(path).expect("read mach-o");
123 let hdr = parse_header(&bytes).expect("parse header");
124 parse_commands(&hdr, &bytes)
125 .expect("parse commands")
126 .into_iter()
127 .map(|cmd| match cmd {
128 LoadCommand::Raw { cmd, .. } => cmd,
129 other => other.cmd(),
130 })
131 .collect()
132 }
133
134 fn normalize(ids: &[u32]) -> Vec<&'static str> {
135 let mut out = Vec::new();
136 for token in ids.iter().filter_map(|cmd| match *cmd {
137 LC_SEGMENT_64 => Some("SEGMENT"),
138 LC_DYLD_INFO_ONLY | LC_DYLD_CHAINED_FIXUPS | LC_DYLD_EXPORTS_TRIE => Some("FIXUPS"),
139 LC_SYMTAB => Some("SYMTAB"),
140 LC_DYSYMTAB => Some("DYSYMTAB"),
141 LC_LOAD_DYLINKER => Some("LOAD_DYLINKER"),
142 LC_UUID => Some("UUID"),
143 LC_BUILD_VERSION => Some("BUILD_VERSION"),
144 LC_SOURCE_VERSION => Some("SOURCE_VERSION"),
145 LC_MAIN => Some("MAIN"),
146 LC_ID_DYLIB => Some("ID_DYLIB"),
147 LC_LOAD_DYLIB => Some("LOAD_DYLIB"),
148 LC_RPATH => Some("RPATH"),
149 LC_FUNCTION_STARTS => Some("FUNCTION_STARTS"),
150 LC_DATA_IN_CODE => Some("DATA_IN_CODE"),
151 LC_CODE_SIGNATURE => Some("CODE_SIGNATURE"),
152 _ => None,
153 }) {
154 if out.last().copied() == Some(token) && token == "FIXUPS" {
155 continue;
156 }
157 out.push(token);
158 }
159 out
160 }
161
162 #[test]
163 fn executable_load_command_order_matches_apple_for_common_surface() {
164 if !have_xcrun() || !have_ld() {
165 eprintln!("skipping: xcrun as / ld unavailable");
166 return;
167 }
168 let Some(sdk) = sdk_path() else {
169 eprintln!("skipping: xcrun --show-sdk-path unavailable");
170 return;
171 };
172
173 let obj = scratch("main.o");
174 let ours = scratch("ours-exec");
175 let theirs = scratch("apple-exec");
176 assemble(
177 r#"
178 .section __TEXT,__text,regular,pure_instructions
179 .globl _main
180 _main:
181 ret
182 "#,
183 &obj,
184 )
185 .expect("assemble");
186 link_with_afs_ld(&[
187 "-syslibroot",
188 &sdk,
189 "-lSystem",
190 obj.to_str().unwrap(),
191 "-o",
192 ours.to_str().unwrap(),
193 ])
194 .expect("afs-ld");
195 let status = Command::new("xcrun")
196 .args([
197 "ld",
198 "-arch",
199 "arm64",
200 "-syslibroot",
201 &sdk,
202 "-lSystem",
203 "-e",
204 "_main",
205 "-o",
206 ])
207 .arg(&theirs)
208 .arg(&obj)
209 .status()
210 .expect("spawn ld");
211 assert!(status.success(), "ld link failed");
212
213 assert_eq!(
214 normalize(&command_ids(&ours)),
215 normalize(&command_ids(&theirs))
216 );
217
218 let _ = fs::remove_file(&obj);
219 let _ = fs::remove_file(&ours);
220 let _ = fs::remove_file(&theirs);
221 }
222
223 #[test]
224 fn dylib_load_command_order_matches_apple_for_common_surface() {
225 if !have_xcrun() || !have_ld() {
226 eprintln!("skipping: xcrun as / ld unavailable");
227 return;
228 }
229 let Some(sdk) = sdk_path() else {
230 eprintln!("skipping: xcrun --show-sdk-path unavailable");
231 return;
232 };
233
234 let obj = scratch("lib.o");
235 let ours = scratch("ours.dylib");
236 let theirs = scratch("apple.dylib");
237 assemble(
238 r#"
239 .section __TEXT,__text,regular,pure_instructions
240 .globl _answer
241 _answer:
242 ret
243 "#,
244 &obj,
245 )
246 .expect("assemble");
247 link_with_afs_ld(&[
248 "-dylib",
249 "-syslibroot",
250 &sdk,
251 "-lSystem",
252 obj.to_str().unwrap(),
253 "-o",
254 ours.to_str().unwrap(),
255 ])
256 .expect("afs-ld dylib");
257 let status = Command::new("xcrun")
258 .args([
259 "ld",
260 "-dylib",
261 "-arch",
262 "arm64",
263 "-syslibroot",
264 &sdk,
265 "-lSystem",
266 "-install_name",
267 "@rpath/libparity.dylib",
268 "-o",
269 ])
270 .arg(&theirs)
271 .arg(&obj)
272 .status()
273 .expect("spawn ld");
274 assert!(status.success(), "ld dylib link failed");
275
276 assert_eq!(
277 normalize(&command_ids(&ours)),
278 normalize(&command_ids(&theirs))
279 );
280
281 let _ = fs::remove_file(&obj);
282 let _ = fs::remove_file(&ours);
283 let _ = fs::remove_file(&theirs);
284 }
285
286 #[test]
287 fn executable_load_command_order_with_dependency_and_rpath_matches_common_surface() {
288 if !have_xcrun() || !have_clang() || !have_ld() {
289 eprintln!("skipping: xcrun as / clang / ld unavailable");
290 return;
291 }
292 let Some(sdk) = sdk_path() else {
293 eprintln!("skipping: xcrun --show-sdk-path unavailable");
294 return;
295 };
296
297 let obj = scratch("dep-main.o");
298 let dep = scratch("dep.dylib");
299 let ours = scratch("ours-dep");
300 let theirs = scratch("apple-dep");
301 assemble(
302 r#"
303 .section __TEXT,__text,regular,pure_instructions
304 .globl _main
305 _main:
306 ret
307 "#,
308 &obj,
309 )
310 .expect("assemble");
311 build_test_dylib("int dep(void) { return 1; }\n", &dep, "@rpath/libdep.dylib")
312 .expect("build dylib");
313 link_with_afs_ld(&[
314 "-syslibroot",
315 &sdk,
316 "-lSystem",
317 obj.to_str().unwrap(),
318 dep.to_str().unwrap(),
319 "-rpath",
320 "@executable_path/../lib",
321 "-o",
322 ours.to_str().unwrap(),
323 ])
324 .expect("afs-ld with dep");
325
326 let status = Command::new("xcrun")
327 .args([
328 "ld",
329 "-arch",
330 "arm64",
331 "-syslibroot",
332 &sdk,
333 "-lSystem",
334 "-e",
335 "_main",
336 "-o",
337 ])
338 .arg(&theirs)
339 .arg(&obj)
340 .arg(&dep)
341 .arg("-rpath")
342 .arg("@executable_path/../lib")
343 .status()
344 .expect("spawn ld");
345 assert!(status.success(), "ld dep link failed");
346
347 assert_eq!(
348 normalize(&command_ids(&ours)),
349 normalize(&command_ids(&theirs))
350 );
351
352 let _ = fs::remove_file(&obj);
353 let _ = fs::remove_file(&dep);
354 let _ = fs::remove_file(&ours);
355 let _ = fs::remove_file(&theirs);
356 }
357