Rust · 27701 bytes Raw Blame History
1 use std::fs;
2 use std::path::PathBuf;
3 use std::process::Command;
4
5 use afs_ld::macho::constants::LC_UUID;
6 use afs_ld::macho::reader::{parse_commands, parse_header, LoadCommand};
7
8 const EXPECTED_HELP: &str = include_str!("snapshots/help.txt");
9
10 fn have_xcrun() -> bool {
11 Command::new("xcrun")
12 .arg("-f")
13 .arg("as")
14 .output()
15 .map(|o| o.status.success())
16 .unwrap_or(false)
17 }
18
19 fn sdk_path() -> Option<String> {
20 let out = Command::new("xcrun")
21 .args(["--sdk", "macosx", "--show-sdk-path"])
22 .output()
23 .ok()?;
24 if !out.status.success() {
25 return None;
26 }
27 Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
28 }
29
30 fn minimal_main_src() -> &'static str {
31 r#"
32 .section __TEXT,__text,regular,pure_instructions
33 .globl _main
34 _main:
35 mov w0, #0
36 ret
37 .subsections_via_symbols
38 "#
39 }
40
41 fn assemble(src: &str, out: &PathBuf) -> Result<(), String> {
42 let tmp = std::env::temp_dir().join(format!(
43 "afs-ld-cli-diag-{}-{}.s",
44 std::process::id(),
45 out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
46 ));
47 fs::write(&tmp, src).map_err(|e| format!("write: {e}"))?;
48 let output = Command::new("xcrun")
49 .args(["--sdk", "macosx", "as", "-arch", "arm64"])
50 .arg(&tmp)
51 .arg("-o")
52 .arg(out)
53 .output()
54 .map_err(|e| format!("spawn xcrun as: {e}"))?;
55 let _ = fs::remove_file(&tmp);
56 if !output.status.success() {
57 return Err(format!(
58 "xcrun as failed: {}",
59 String::from_utf8_lossy(&output.stderr)
60 ));
61 }
62 Ok(())
63 }
64
65 fn scratch(name: &str) -> PathBuf {
66 std::env::temp_dir().join(format!("afs-ld-cli-diag-{}-{name}", std::process::id()))
67 }
68
69 fn assemble_minimal_main(name: &str) -> Result<PathBuf, String> {
70 let obj = scratch(name);
71 assemble(minimal_main_src(), &obj)?;
72 Ok(obj)
73 }
74
75 fn assert_flag_errors(flag: &str, expected: &str, name: &str) {
76 if !have_xcrun() {
77 eprintln!("skipping: xcrun as unavailable");
78 return;
79 }
80 let exe = env!("CARGO_BIN_EXE_afs-ld");
81 let obj = match assemble_minimal_main(&format!("{name}.o")) {
82 Ok(obj) => obj,
83 Err(e) => {
84 eprintln!("skipping: assemble failed: {e}");
85 return;
86 }
87 };
88 let out_path = scratch(&format!("{name}.out"));
89 let out = Command::new(exe)
90 .arg(flag)
91 .arg("-o")
92 .arg(&out_path)
93 .arg(&obj)
94 .output()
95 .expect("afs-ld should run");
96 assert!(!out.status.success(), "{flag} should fail");
97 let stderr = String::from_utf8_lossy(&out.stderr);
98 assert!(
99 stderr.contains(expected),
100 "missing expected `{expected}` in stderr:\n{stderr}"
101 );
102 let _ = fs::remove_file(obj);
103 let _ = fs::remove_file(out_path);
104 }
105
106 fn archive(objects: &[&PathBuf], out: &PathBuf) -> Result<(), String> {
107 let output = Command::new("libtool")
108 .arg("-static")
109 .arg("-o")
110 .arg(out)
111 .args(objects)
112 .output()
113 .map_err(|e| format!("spawn libtool: {e}"))?;
114 if !output.status.success() {
115 return Err(format!(
116 "libtool failed: {}",
117 String::from_utf8_lossy(&output.stderr)
118 ));
119 }
120 Ok(())
121 }
122
123 fn nm_defined_names(path: &PathBuf) -> Result<Vec<String>, String> {
124 let output = Command::new("xcrun")
125 .args(["nm", "-gj"])
126 .arg(path)
127 .output()
128 .map_err(|e| format!("spawn xcrun nm: {e}"))?;
129 if !output.status.success() {
130 return Err(format!(
131 "xcrun nm failed: {}",
132 String::from_utf8_lossy(&output.stderr)
133 ));
134 }
135 Ok(String::from_utf8_lossy(&output.stdout)
136 .lines()
137 .map(str::trim)
138 .filter(|line| !line.is_empty())
139 .map(ToOwned::to_owned)
140 .collect())
141 }
142
143 #[test]
144 fn help_flag_prints_usage_and_exits_successfully() {
145 let exe = env!("CARGO_BIN_EXE_afs-ld");
146 let out = Command::new(exe)
147 .arg("--help")
148 .output()
149 .expect("afs-ld should run");
150 assert!(out.status.success(), "help should succeed");
151 let stdout = String::from_utf8_lossy(&out.stdout);
152 assert_eq!(stdout.as_ref(), EXPECTED_HELP);
153 assert!(
154 out.stderr.is_empty(),
155 "help should not write to stderr:\n{}",
156 String::from_utf8_lossy(&out.stderr)
157 );
158 }
159
160 #[test]
161 fn version_flag_prints_version_and_exits_successfully() {
162 let exe = env!("CARGO_BIN_EXE_afs-ld");
163 let out = Command::new(exe)
164 .arg("--version")
165 .output()
166 .expect("afs-ld should run");
167 assert!(out.status.success(), "version should succeed");
168 let stdout = String::from_utf8_lossy(&out.stdout);
169 assert_eq!(
170 stdout.trim(),
171 format!("afs-ld {}", env!("CARGO_PKG_VERSION"))
172 );
173 }
174
175 #[test]
176 fn no_uuid_flag_omits_uuid_load_command() {
177 if !have_xcrun() {
178 eprintln!("skipping: xcrun as unavailable");
179 return;
180 }
181
182 let exe = env!("CARGO_BIN_EXE_afs-ld");
183 let obj = match assemble_minimal_main("no-uuid-main.o") {
184 Ok(obj) => obj,
185 Err(e) => {
186 eprintln!("skipping: assemble failed: {e}");
187 return;
188 }
189 };
190 let out_path = scratch("no-uuid.out");
191 let out = Command::new(exe)
192 .arg("-no_uuid")
193 .arg("-o")
194 .arg(&out_path)
195 .arg(&obj)
196 .output()
197 .expect("afs-ld should run");
198 assert!(
199 out.status.success(),
200 "-no_uuid link should succeed:\nstderr:\n{}",
201 String::from_utf8_lossy(&out.stderr)
202 );
203
204 let bytes = fs::read(&out_path).expect("read linked output");
205 let header = parse_header(&bytes).expect("parse header");
206 let commands = parse_commands(&header, &bytes).expect("parse commands");
207 assert!(
208 commands.iter().all(|cmd| match cmd {
209 LoadCommand::Raw { cmd, .. } => *cmd != LC_UUID,
210 _ => true,
211 }),
212 "expected -no_uuid output to omit LC_UUID"
213 );
214
215 let _ = fs::remove_file(obj);
216 let _ = fs::remove_file(out_path);
217 }
218
219 #[test]
220 fn no_loh_flag_warns_but_links_successfully() {
221 if !have_xcrun() {
222 eprintln!("skipping: xcrun as unavailable");
223 return;
224 }
225
226 let exe = env!("CARGO_BIN_EXE_afs-ld");
227 let obj = match assemble_minimal_main("no-loh-main.o") {
228 Ok(obj) => obj,
229 Err(e) => {
230 eprintln!("skipping: assemble failed: {e}");
231 return;
232 }
233 };
234 let out_path = scratch("no-loh.out");
235 let out = Command::new(exe)
236 .arg("-no_loh")
237 .arg("-o")
238 .arg(&out_path)
239 .arg(&obj)
240 .output()
241 .expect("afs-ld should run");
242 assert!(
243 out.status.success(),
244 "-no_loh link should succeed:\nstderr:\n{}",
245 String::from_utf8_lossy(&out.stderr)
246 );
247 let stderr = String::from_utf8_lossy(&out.stderr);
248 assert!(
249 stderr.contains("afs-ld: warning: `-no_loh` requested"),
250 "expected -no_loh warning:\n{stderr}"
251 );
252 assert!(
253 out_path.is_file(),
254 "expected -no_loh link to produce {}",
255 out_path.display()
256 );
257
258 let _ = fs::remove_file(obj);
259 let _ = fs::remove_file(out_path);
260 }
261
262 #[test]
263 fn strip_debug_flag_warns_but_links_successfully() {
264 if !have_xcrun() {
265 eprintln!("skipping: xcrun as unavailable");
266 return;
267 }
268
269 let exe = env!("CARGO_BIN_EXE_afs-ld");
270 let obj = match assemble_minimal_main("strip-debug-main.o") {
271 Ok(obj) => obj,
272 Err(e) => {
273 eprintln!("skipping: assemble failed: {e}");
274 return;
275 }
276 };
277 let out_path = scratch("strip-debug.out");
278 let out = Command::new(exe)
279 .arg("-S")
280 .arg("-o")
281 .arg(&out_path)
282 .arg(&obj)
283 .output()
284 .expect("afs-ld should run");
285 assert!(
286 out.status.success(),
287 "-S link should succeed:\nstderr:\n{}",
288 String::from_utf8_lossy(&out.stderr)
289 );
290 let stderr = String::from_utf8_lossy(&out.stderr);
291 assert!(
292 stderr.contains("afs-ld: warning: `-S` requested"),
293 "expected -S warning:\n{stderr}"
294 );
295
296 let _ = fs::remove_file(obj);
297 let _ = fs::remove_file(out_path);
298 }
299
300 #[test]
301 fn objc_flag_warns_but_links_successfully() {
302 if !have_xcrun() {
303 eprintln!("skipping: xcrun as unavailable");
304 return;
305 }
306
307 let exe = env!("CARGO_BIN_EXE_afs-ld");
308 let obj = match assemble_minimal_main("objc-main.o") {
309 Ok(obj) => obj,
310 Err(e) => {
311 eprintln!("skipping: assemble failed: {e}");
312 return;
313 }
314 };
315 let out_path = scratch("objc.out");
316 let out = Command::new(exe)
317 .arg("-ObjC")
318 .arg("-o")
319 .arg(&out_path)
320 .arg(&obj)
321 .output()
322 .expect("afs-ld should run");
323 assert!(
324 out.status.success(),
325 "-ObjC link should succeed:\nstderr:\n{}",
326 String::from_utf8_lossy(&out.stderr)
327 );
328 let stderr = String::from_utf8_lossy(&out.stderr);
329 assert!(
330 stderr.contains("afs-ld: warning: `-ObjC` requested"),
331 "expected -ObjC warning:\n{stderr}"
332 );
333
334 let _ = fs::remove_file(obj);
335 let _ = fs::remove_file(out_path);
336 }
337
338 #[test]
339 fn relocatable_flag_errors_loudly() {
340 assert_flag_errors(
341 "-r",
342 "`-r` relocatable output is not yet supported",
343 "relocatable",
344 );
345 }
346
347 #[test]
348 fn bundle_flag_errors_loudly() {
349 assert_flag_errors("-bundle", "`-bundle` output is not yet supported", "bundle");
350 }
351
352 #[test]
353 fn dead_strip_removes_unreferenced_symbols_and_reports_why_live() {
354 if !have_xcrun() {
355 eprintln!("skipping: xcrun as unavailable");
356 return;
357 }
358
359 let exe = env!("CARGO_BIN_EXE_afs-ld");
360 let main_obj = scratch("dead-strip-main.o");
361 let helper_obj = scratch("dead-strip-helper.o");
362 let unused_obj = scratch("dead-strip-unused.o");
363 let out_path = scratch("dead-strip.out");
364 let main_src = r#"
365 .section __TEXT,__text,regular,pure_instructions
366 .globl _main
367 _main:
368 bl _helper
369 mov w0, #0
370 ret
371 .subsections_via_symbols
372 "#;
373 let helper_src = r#"
374 .section __TEXT,__text,regular,pure_instructions
375 .globl _helper
376 _helper:
377 ret
378 .subsections_via_symbols
379 "#;
380 let unused_src = r#"
381 .section __TEXT,__text,regular,pure_instructions
382 .globl _unused
383 _unused:
384 ret
385 .subsections_via_symbols
386 "#;
387 if let Err(e) = assemble(main_src, &main_obj) {
388 eprintln!("skipping: assemble failed: {e}");
389 return;
390 }
391 if let Err(e) = assemble(helper_src, &helper_obj) {
392 eprintln!("skipping: assemble failed: {e}");
393 let _ = fs::remove_file(main_obj);
394 return;
395 }
396 if let Err(e) = assemble(unused_src, &unused_obj) {
397 eprintln!("skipping: assemble failed: {e}");
398 let _ = fs::remove_file(main_obj);
399 let _ = fs::remove_file(helper_obj);
400 return;
401 }
402
403 let out = Command::new(exe)
404 .arg("-dead_strip")
405 .arg("-why_live")
406 .arg("_helper")
407 .arg("-why_live")
408 .arg("_unused")
409 .arg("-o")
410 .arg(&out_path)
411 .arg(&main_obj)
412 .arg(&helper_obj)
413 .arg(&unused_obj)
414 .output()
415 .expect("afs-ld should run");
416 assert!(
417 out.status.success(),
418 "-dead_strip link should succeed:\nstderr:\n{}",
419 String::from_utf8_lossy(&out.stderr)
420 );
421 let stdout = String::from_utf8_lossy(&out.stdout);
422 assert!(stdout.contains("_helper is live because:"));
423 assert!(stdout.contains("_helper is reachable from _main"));
424 assert!(stdout.contains("_main is in -e _main (GC root)"));
425 assert!(stdout.contains("_unused is not live (dead-stripped)"));
426
427 let symbols = match nm_defined_names(&out_path) {
428 Ok(symbols) => symbols,
429 Err(e) => {
430 panic!("nm failed: {e}");
431 }
432 };
433 assert!(symbols.contains(&"_main".to_string()));
434 assert!(symbols.contains(&"_helper".to_string()));
435 assert!(
436 !symbols.contains(&"_unused".to_string()),
437 "dead-stripped symbol still present:\n{}",
438 symbols.join("\n")
439 );
440
441 let _ = fs::remove_file(main_obj);
442 let _ = fs::remove_file(helper_obj);
443 let _ = fs::remove_file(unused_obj);
444 let _ = fs::remove_file(out_path);
445 }
446
447 #[test]
448 fn dead_strip_keeps_no_dead_strip_roots() {
449 if !have_xcrun() {
450 eprintln!("skipping: xcrun as unavailable");
451 return;
452 }
453
454 let exe = env!("CARGO_BIN_EXE_afs-ld");
455 let obj = scratch("dead-strip-no-dead-strip.o");
456 let out_path = scratch("dead-strip-no-dead-strip.out");
457 let src = r#"
458 .section __TEXT,__text,regular,pure_instructions
459 .globl _main
460 _main:
461 mov w0, #0
462 ret
463
464 .globl _keep
465 _keep:
466 ret
467 .desc _keep, 0x20
468
469 .globl _drop
470 _drop:
471 ret
472 .subsections_via_symbols
473 "#;
474 if let Err(e) = assemble(src, &obj) {
475 eprintln!("skipping: assemble failed: {e}");
476 return;
477 }
478
479 let out = Command::new(exe)
480 .arg("-dead_strip")
481 .arg("-why_live")
482 .arg("_keep")
483 .arg("-why_live")
484 .arg("_drop")
485 .arg("-o")
486 .arg(&out_path)
487 .arg(&obj)
488 .output()
489 .expect("afs-ld should run");
490 assert!(
491 out.status.success(),
492 "-dead_strip link should succeed:\nstderr:\n{}",
493 String::from_utf8_lossy(&out.stderr)
494 );
495 let stdout = String::from_utf8_lossy(&out.stdout);
496 assert!(stdout.contains("_keep is live because:"));
497 assert!(stdout.contains("_keep is marked N_NO_DEAD_STRIP (GC root)"));
498 assert!(stdout.contains("_drop is not live (dead-stripped)"));
499
500 let symbols = match nm_defined_names(&out_path) {
501 Ok(symbols) => symbols,
502 Err(e) => {
503 panic!("nm failed: {e}");
504 }
505 };
506 assert!(symbols.contains(&"_main".to_string()));
507 assert!(symbols.contains(&"_keep".to_string()));
508 assert!(
509 !symbols.contains(&"_drop".to_string()),
510 "dead-stripped symbol still present:\n{}",
511 symbols.join("\n")
512 );
513
514 let _ = fs::remove_file(obj);
515 let _ = fs::remove_file(out_path);
516 }
517
518 #[test]
519 fn icf_safe_flag_links_successfully() {
520 if !have_xcrun() {
521 eprintln!("skipping: xcrun as unavailable");
522 return;
523 }
524
525 let exe = env!("CARGO_BIN_EXE_afs-ld");
526 let obj = match assemble_minimal_main("icf-safe-main.o") {
527 Ok(obj) => obj,
528 Err(e) => {
529 eprintln!("skipping: assemble failed: {e}");
530 return;
531 }
532 };
533 let out_path = scratch("icf-safe.out");
534 let out = Command::new(exe)
535 .arg("-icf=safe")
536 .arg("-o")
537 .arg(&out_path)
538 .arg(&obj)
539 .output()
540 .expect("afs-ld should run");
541 assert!(
542 out.status.success(),
543 "-icf=safe link should succeed:\nstderr:\n{}",
544 String::from_utf8_lossy(&out.stderr)
545 );
546 assert!(
547 out_path.is_file(),
548 "expected -icf=safe link to produce {}",
549 out_path.display()
550 );
551
552 let _ = fs::remove_file(obj);
553 let _ = fs::remove_file(out_path);
554 }
555
556 #[test]
557 fn icf_all_flag_errors_loudly() {
558 assert_flag_errors(
559 "-icf=all",
560 "`-icf=all` is not yet supported; use `-icf=safe` or `-icf=none`",
561 "icf-all",
562 );
563 }
564
565 #[test]
566 fn fixup_chains_flag_errors_loudly() {
567 assert_flag_errors(
568 "-fixup_chains",
569 "`-fixup_chains` is not yet supported",
570 "fixup-chains",
571 );
572 }
573
574 #[test]
575 fn undefined_symbol_diagnostic_is_not_double_prefixed() {
576 if !have_xcrun() {
577 eprintln!("skipping: xcrun as unavailable");
578 return;
579 }
580
581 let exe = env!("CARGO_BIN_EXE_afs-ld");
582 let obj = scratch("missing.o");
583 let src = r#"
584 .section __TEXT,__text,regular,pure_instructions
585 .globl _main
586 _main:
587 bl _missing
588 ret
589 .subsections_via_symbols
590 "#;
591 if let Err(e) = assemble(src, &obj) {
592 eprintln!("skipping: assemble failed: {e}");
593 return;
594 }
595
596 let out = Command::new(exe)
597 .arg("-o")
598 .arg(scratch("missing.out"))
599 .arg(&obj)
600 .output()
601 .expect("afs-ld should run");
602 let stderr = String::from_utf8_lossy(&out.stderr);
603 assert!(
604 stderr.contains("afs-ld: error: undefined symbol: _missing"),
605 "missing expected undefined-symbol diagnostic:\n{stderr}"
606 );
607 assert!(
608 !stderr.contains("afs-ld: error: afs-ld: error:"),
609 "diagnostic was double-prefixed:\n{stderr}"
610 );
611
612 let _ = fs::remove_file(obj);
613 }
614
615 #[test]
616 fn undefined_warning_mode_links_and_warns_once() {
617 if !have_xcrun() {
618 eprintln!("skipping: xcrun as unavailable");
619 return;
620 }
621 let Some(sdk) = sdk_path() else {
622 eprintln!("skipping: xcrun --show-sdk-path unavailable");
623 return;
624 };
625
626 let exe = env!("CARGO_BIN_EXE_afs-ld");
627 let obj = scratch("missing-warning.o");
628 let src = r#"
629 .section __TEXT,__text,regular,pure_instructions
630 .globl _main
631 _main:
632 bl _missing
633 ret
634 .subsections_via_symbols
635 "#;
636 if let Err(e) = assemble(src, &obj) {
637 eprintln!("skipping: assemble failed: {e}");
638 return;
639 }
640
641 let out_path = scratch("missing-warning.out");
642 let out = Command::new(exe)
643 .arg("-undefined")
644 .arg("warning")
645 .arg("-syslibroot")
646 .arg(&sdk)
647 .arg("-lSystem")
648 .arg("-o")
649 .arg(&out_path)
650 .arg(&obj)
651 .output()
652 .expect("afs-ld should run");
653 assert!(
654 out.status.success(),
655 "-undefined warning should link successfully:\nstderr:\n{}",
656 String::from_utf8_lossy(&out.stderr)
657 );
658 let stderr = String::from_utf8_lossy(&out.stderr);
659 assert!(
660 stderr.contains("afs-ld: warning: undefined symbol: _missing"),
661 "missing expected undefined-symbol warning:\n{stderr}"
662 );
663 assert!(
664 !stderr.contains("afs-ld: warning: afs-ld: warning:"),
665 "warning diagnostic was double-prefixed:\n{stderr}"
666 );
667 assert!(out_path.exists(), "expected linked output to be written");
668
669 let _ = fs::remove_file(obj);
670 let _ = fs::remove_file(out_path);
671 }
672
673 #[test]
674 fn undefined_suppress_mode_links_silently() {
675 if !have_xcrun() {
676 eprintln!("skipping: xcrun as unavailable");
677 return;
678 }
679 let Some(sdk) = sdk_path() else {
680 eprintln!("skipping: xcrun --show-sdk-path unavailable");
681 return;
682 };
683
684 let exe = env!("CARGO_BIN_EXE_afs-ld");
685 let obj = scratch("missing-suppress.o");
686 let src = r#"
687 .section __TEXT,__text,regular,pure_instructions
688 .globl _main
689 _main:
690 bl _missing
691 ret
692 .subsections_via_symbols
693 "#;
694 if let Err(e) = assemble(src, &obj) {
695 eprintln!("skipping: assemble failed: {e}");
696 return;
697 }
698
699 let out_path = scratch("missing-suppress.out");
700 let out = Command::new(exe)
701 .arg("-undefined")
702 .arg("suppress")
703 .arg("-syslibroot")
704 .arg(&sdk)
705 .arg("-lSystem")
706 .arg("-o")
707 .arg(&out_path)
708 .arg(&obj)
709 .output()
710 .expect("afs-ld should run");
711 assert!(
712 out.status.success(),
713 "-undefined suppress should link successfully:\nstderr:\n{}",
714 String::from_utf8_lossy(&out.stderr)
715 );
716 let stderr = String::from_utf8_lossy(&out.stderr);
717 assert!(
718 !stderr.contains("undefined symbol: _missing"),
719 "expected -undefined suppress to omit undefined diagnostic:\n{stderr}"
720 );
721 assert!(out_path.exists(), "expected linked output to be written");
722
723 let _ = fs::remove_file(obj);
724 let _ = fs::remove_file(out_path);
725 }
726
727 #[test]
728 fn trace_flag_prints_loaded_inputs_and_archive_members() {
729 if !have_xcrun() {
730 eprintln!("skipping: xcrun as unavailable");
731 return;
732 }
733
734 let exe = env!("CARGO_BIN_EXE_afs-ld");
735 let main_obj = scratch("trace-main.o");
736 let helper_obj = scratch("trace-helper.o");
737 let archive_path = scratch("libtracehelpers.a");
738 let out_path = scratch("trace.out");
739 let main_src = r#"
740 .section __TEXT,__text,regular,pure_instructions
741 .globl _main
742 _main:
743 bl _helper
744 mov w0, #0
745 ret
746 .subsections_via_symbols
747 "#;
748 let helper_src = r#"
749 .section __TEXT,__text,regular,pure_instructions
750 .globl _helper
751 _helper:
752 ret
753 .subsections_via_symbols
754 "#;
755 if let Err(e) = assemble(main_src, &main_obj) {
756 eprintln!("skipping: assemble failed: {e}");
757 return;
758 }
759 if let Err(e) = assemble(helper_src, &helper_obj) {
760 eprintln!("skipping: assemble failed: {e}");
761 return;
762 }
763 if let Err(e) = archive(&[&helper_obj], &archive_path) {
764 eprintln!("skipping: archive failed: {e}");
765 let _ = fs::remove_file(main_obj);
766 let _ = fs::remove_file(helper_obj);
767 return;
768 }
769
770 let out = Command::new(exe)
771 .arg("-t")
772 .arg("-o")
773 .arg(&out_path)
774 .arg(&main_obj)
775 .arg(&archive_path)
776 .output()
777 .expect("afs-ld should run");
778 assert!(
779 out.status.success(),
780 "trace link should succeed:\nstderr:\n{}",
781 String::from_utf8_lossy(&out.stderr)
782 );
783 let stderr = String::from_utf8_lossy(&out.stderr);
784 assert!(
785 stderr.contains(&format!("afs-ld: loading {}", main_obj.display())),
786 "missing main object trace:\n{stderr}"
787 );
788 assert!(
789 stderr.contains(&format!("afs-ld: loading {}", archive_path.display())),
790 "missing archive trace:\n{stderr}"
791 );
792 assert!(
793 stderr.contains("libtracehelpers.a("),
794 "missing fetched archive member trace:\n{stderr}"
795 );
796
797 let _ = fs::remove_file(main_obj);
798 let _ = fs::remove_file(helper_obj);
799 let _ = fs::remove_file(archive_path);
800 let _ = fs::remove_file(out_path);
801 }
802
803 #[test]
804 fn why_live_reports_root_entry_symbol() {
805 if !have_xcrun() {
806 eprintln!("skipping: xcrun as unavailable");
807 return;
808 }
809
810 let exe = env!("CARGO_BIN_EXE_afs-ld");
811 let main_obj = scratch("why-live-root-main.o");
812 let out_path = scratch("why-live-root.out");
813 let src = r#"
814 .section __TEXT,__text,regular,pure_instructions
815 .globl _main
816 _main:
817 mov w0, #0
818 ret
819 .subsections_via_symbols
820 "#;
821 if let Err(e) = assemble(src, &main_obj) {
822 eprintln!("skipping: assemble failed: {e}");
823 return;
824 }
825
826 let out = Command::new(exe)
827 .arg("-why_live")
828 .arg("_main")
829 .arg("-o")
830 .arg(&out_path)
831 .arg(&main_obj)
832 .output()
833 .expect("afs-ld should run");
834 assert!(
835 out.status.success(),
836 "why_live link should succeed:\nstderr:\n{}",
837 String::from_utf8_lossy(&out.stderr)
838 );
839 let stdout = String::from_utf8_lossy(&out.stdout);
840 assert!(stdout.contains("_main is live because:"));
841 assert!(stdout.contains("-dead_strip was not requested"));
842 assert!(stdout.contains("_main is in -e _main (GC root)"));
843
844 let _ = fs::remove_file(main_obj);
845 let _ = fs::remove_file(out_path);
846 }
847
848 #[test]
849 fn why_live_reports_transitive_symbol_chain() {
850 if !have_xcrun() {
851 eprintln!("skipping: xcrun as unavailable");
852 return;
853 }
854
855 let exe = env!("CARGO_BIN_EXE_afs-ld");
856 let main_obj = scratch("why-live-main.o");
857 let helper_obj = scratch("why-live-helper.o");
858 let leaf_obj = scratch("why-live-leaf.o");
859 let out_path = scratch("why-live.out");
860 let main_src = r#"
861 .section __TEXT,__text,regular,pure_instructions
862 .globl _main
863 _main:
864 bl _helper
865 mov w0, #0
866 ret
867 .subsections_via_symbols
868 "#;
869 let helper_src = r#"
870 .section __TEXT,__text,regular,pure_instructions
871 .globl _helper
872 _helper:
873 bl _leaf
874 ret
875 .subsections_via_symbols
876 "#;
877 let leaf_src = r#"
878 .section __TEXT,__text,regular,pure_instructions
879 .globl _leaf
880 _leaf:
881 ret
882 .subsections_via_symbols
883 "#;
884 if let Err(e) = assemble(main_src, &main_obj) {
885 eprintln!("skipping: assemble failed: {e}");
886 return;
887 }
888 if let Err(e) = assemble(helper_src, &helper_obj) {
889 eprintln!("skipping: assemble failed: {e}");
890 let _ = fs::remove_file(main_obj);
891 return;
892 }
893 if let Err(e) = assemble(leaf_src, &leaf_obj) {
894 eprintln!("skipping: assemble failed: {e}");
895 let _ = fs::remove_file(main_obj);
896 let _ = fs::remove_file(helper_obj);
897 return;
898 }
899
900 let out = Command::new(exe)
901 .arg("-why_live")
902 .arg("_leaf")
903 .arg("-o")
904 .arg(&out_path)
905 .arg(&main_obj)
906 .arg(&helper_obj)
907 .arg(&leaf_obj)
908 .output()
909 .expect("afs-ld should run");
910 assert!(
911 out.status.success(),
912 "why_live link should succeed:\nstderr:\n{}",
913 String::from_utf8_lossy(&out.stderr)
914 );
915 let stdout = String::from_utf8_lossy(&out.stdout);
916 assert!(stdout.contains("_leaf is live because:"));
917 assert!(stdout.contains("-dead_strip was not requested"));
918 assert!(stdout.contains("_leaf is reachable from _helper"));
919 assert!(stdout.contains("_helper is reachable from _main"));
920 assert!(stdout.contains("_main is in -e _main (GC root)"));
921
922 let _ = fs::remove_file(main_obj);
923 let _ = fs::remove_file(helper_obj);
924 let _ = fs::remove_file(leaf_obj);
925 let _ = fs::remove_file(out_path);
926 }
927
928 #[test]
929 fn why_live_reports_folded_symbol_winner_chain() {
930 if !have_xcrun() {
931 eprintln!("skipping: xcrun as unavailable");
932 return;
933 }
934
935 let exe = env!("CARGO_BIN_EXE_afs-ld");
936 let obj = scratch("why-live-folded.o");
937 let out_path = scratch("why-live-folded.out");
938 let src = r#"
939 .section __TEXT,__text,regular,pure_instructions
940 .globl _main
941 _main:
942 stp x29, x30, [sp, #-16]!
943 mov x29, sp
944 bl _helper1
945 bl _helper2
946 mov w0, #0
947 ldp x29, x30, [sp], #16
948 ret
949
950 .private_extern _helper1
951 _helper1:
952 mov w0, #0
953 ret
954
955 .private_extern _helper2
956 _helper2:
957 mov w0, #0
958 ret
959 .subsections_via_symbols
960 "#;
961 if let Err(e) = assemble(src, &obj) {
962 eprintln!("skipping: assemble failed: {e}");
963 return;
964 }
965
966 let out = Command::new(exe)
967 .arg("-icf=safe")
968 .arg("-why_live")
969 .arg("_helper2")
970 .arg("-o")
971 .arg(&out_path)
972 .arg(&obj)
973 .output()
974 .expect("afs-ld should run");
975 assert!(
976 out.status.success(),
977 "why_live folded-symbol link should succeed:\nstderr:\n{}",
978 String::from_utf8_lossy(&out.stderr)
979 );
980 let stdout = String::from_utf8_lossy(&out.stdout);
981 assert!(stdout.contains("_helper2 was folded to _helper1 by -icf=safe"));
982 assert!(stdout.contains("_helper1 is live because:"));
983 assert!(stdout.contains("_helper1 is reachable from _main"));
984
985 let _ = fs::remove_file(obj);
986 let _ = fs::remove_file(out_path);
987 }
988