| 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 |