Rust · 6438 bytes Raw Blame History
1 //! Sprint 6 real-world gate: parse the installed SDK's `libSystem.tbd`,
2 //! materialize it as a `DylibFile` for `arm64-macos`, and confirm:
3 //!
4 //! - every TBD document parses cleanly (`parse_tbd` returns multiple,
5 //! each with an `install-name`);
6 //! - the main document's DylibFile surfaces libSystem's direct
7 //! exports (small set — `_mach_init_routine`,
8 //! `_libSystem_init_after_boot_tasks_4launchd`, `___crashreporter_info__`);
9 //! - `reexported-libraries` surfaces as `DylibDependency` entries with
10 //! `Reexport` load kind, with monotonic 1-based ordinals;
11 //! - scanning every document's exports reveals _malloc / _free
12 //! somewhere in the re-export chain (libsystem_malloc / libsystem_c).
13 //!
14 //! Note: the SDK surfaces `dyld_stub_binder` in libSystem's re-export chain
15 //! (via the libdyld sub-document), not as a direct export of the main
16 //! libSystem umbrella document. Sprint 12 still handles it specially because
17 //! stub-helper synthesis needs to pin that import to libSystem's umbrella
18 //! load-command identity.
19 //!
20 //! Skipped if `xcrun` or `libSystem.tbd` aren't present.
21
22 use afs_ld::macho::dylib::{DylibFile, DylibLoadKind};
23 use afs_ld::macho::tbd::{parse_tbd, parse_tbd_for_target, Arch, Platform, Target};
24
25 fn sdk_path() -> Option<String> {
26 let out = std::process::Command::new("xcrun")
27 .args(["--sdk", "macosx", "--show-sdk-path"])
28 .output()
29 .ok()?;
30 if !out.status.success() {
31 return None;
32 }
33 Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
34 }
35
36 #[test]
37 fn libsystem_tbd_materializes_into_dylib_file() {
38 let Some(sdk) = sdk_path() else {
39 eprintln!("skipping: SDK path unavailable");
40 return;
41 };
42 let path = format!("{sdk}/usr/lib/libSystem.tbd");
43 let Ok(src) = std::fs::read_to_string(&path) else {
44 eprintln!("skipping: libSystem.tbd not found at {path}");
45 return;
46 };
47 let docs = parse_tbd(&src).unwrap_or_else(|e| panic!("libSystem.tbd failed to parse: {e}"));
48 assert!(docs.len() >= 2, "expected multi-doc TBD");
49
50 let main = &docs[0];
51 assert_eq!(main.install_name, "/usr/lib/libSystem.B.dylib");
52
53 let target = Target {
54 arch: Arch::Arm64,
55 platform: Platform::MacOs,
56 };
57 let fast_docs = parse_tbd_for_target(&src, &target)
58 .unwrap_or_else(|e| panic!("libSystem.tbd fast path failed to parse: {e}"));
59 assert!(
60 !fast_docs.is_empty(),
61 "fast path did not keep any arm64-compatible documents"
62 );
63 let dy = DylibFile::from_tbd(&path, main, &target);
64
65 assert_eq!(dy.install_name, "/usr/lib/libSystem.B.dylib");
66 assert!(dy.current_version >= (1 << 16));
67 assert_eq!(dy.compatibility_version, 1 << 16);
68
69 // libSystem's main TBD doc only exposes a short list of internal
70 // symbols directly; everything useful (malloc, printf, dyld binder)
71 // flows through its `reexported-libraries`. Confirm at least one of
72 // the direct exports made it through.
73 let exports = dy.exports.entries().unwrap();
74 let names: Vec<&str> = exports.iter().map(|e| e.name.as_str()).collect();
75 let direct_candidates = [
76 "_mach_init_routine",
77 "_libSystem_init_after_boot_tasks_4launchd",
78 "___crashreporter_info__",
79 ];
80 assert!(
81 direct_candidates.iter().any(|n| names.contains(n)),
82 "no libSystem direct export found; got {names:?}"
83 );
84
85 // Scan every document in the TBD — _malloc / _free / _printf surface
86 // through libsystem_malloc / libsystem_c / (implicit libstdc), which
87 // ship as their own --- !tapi-tbd documents inside libSystem.tbd.
88 let mut found = std::collections::HashSet::<&str>::new();
89 for doc in &docs {
90 let sub = DylibFile::from_tbd(&path, doc, &target);
91 for entry in sub.exports.entries().unwrap() {
92 match entry.name.as_str() {
93 "_malloc" => {
94 found.insert("_malloc");
95 }
96 "_free" => {
97 found.insert("_free");
98 }
99 "_printf" => {
100 found.insert("_printf");
101 }
102 _ => {}
103 }
104 }
105 }
106 assert!(
107 found.contains("_malloc"),
108 "_malloc not found anywhere in libSystem's TBD re-export chain"
109 );
110 assert!(
111 found.contains("_free"),
112 "_free not found anywhere in libSystem's TBD re-export chain"
113 );
114
115 let mut fast_found = std::collections::HashSet::<&str>::new();
116 for doc in &fast_docs {
117 let sub = DylibFile::from_tbd(&path, doc, &target);
118 for entry in sub.exports.entries().unwrap() {
119 match entry.name.as_str() {
120 "_atexit" => {
121 fast_found.insert("_atexit");
122 }
123 "_write" => {
124 fast_found.insert("_write");
125 }
126 "__Unwind_Backtrace" => {
127 fast_found.insert("__Unwind_Backtrace");
128 }
129 _ => {}
130 }
131 }
132 }
133 for expected in ["_atexit", "_write", "__Unwind_Backtrace"] {
134 assert!(
135 fast_found.contains(expected),
136 "{expected} not found by libSystem fast path; got {fast_found:?}"
137 );
138 }
139
140 // libSystem re-exports most actual libc symbols (malloc, free, etc.) from
141 // sub-dylibs. They come from the `reexported-libraries`, not from
142 // libSystem's own exports. Confirm we captured the chain.
143 assert!(
144 !dy.dependencies.is_empty(),
145 "libSystem.tbd has no reexported-libraries"
146 );
147 assert!(dy
148 .dependencies
149 .iter()
150 .all(|d| d.kind == DylibLoadKind::Reexport));
151
152 // Common expected sub-dylibs we re-export — matches what `otool -L
153 // libSystem.B.dylib` shows on a real macOS.
154 let install_names: Vec<&str> = dy
155 .dependencies
156 .iter()
157 .map(|d| d.install_name.as_str())
158 .collect();
159 for sub in [
160 "/usr/lib/system/libsystem_c.dylib",
161 "/usr/lib/system/libsystem_kernel.dylib",
162 ] {
163 assert!(
164 install_names.contains(&sub),
165 "expected {sub} in libSystem's reexported-libraries; got {install_names:?}"
166 );
167 }
168
169 // Ordinals are strictly monotonic and 1-based.
170 for (i, d) in dy.dependencies.iter().enumerate() {
171 assert_eq!(d.ordinal as usize, i + 1);
172 }
173 }
174