Sprint 6: TAPI TBD Text Stubs
Prerequisites
Sprint 5 — binary dylib reader works.
Goals
Read .tbd files (TAPI text dylib stubs). On modern SDKs libSystem, libc++, and CoreFoundation ship only as .tbd — linking without this sprint means no system libraries, full stop.
Deliverables
1. Minimal YAML subset
TBD is YAML with a well-defined schema. We implement only the subset TAPI emits, not a general YAML parser:
- Flow scalars (plain, single-quoted, double-quoted).
- Flow sequences:
[ a, b, c ]. - Block sequences:
- item. - Block mappings:
key: value. - Multi-document files with
---/.... - Tags:
!tapi-tbd. - Version directives:
%YAML 1.2.
No anchors, no aliases, no complex types, no folded scalars. If a real .tbd in the wild uses features outside the subset, the parser fails loudly with line/column.
2. TBD schema
afs-ld/src/macho/tbd.rs:
pub struct Tbd {
pub tbd_version: u32, // 3 or 4
pub targets: Vec<Target>, // arch + platform
pub install_name: String,
pub current_version: Option<String>,
pub compatibility_version: Option<String>,
pub parent_umbrella: Vec<Scoped<String>>,
pub allowable_clients: Vec<Scoped<String>>,
pub reexported_libraries: Vec<Scoped<String>>,
pub exports: Vec<Scoped<Exports>>,
pub reexports: Vec<Scoped<Exports>>,
}
pub struct Target { pub arch: Arch, pub platform: Platform }
pub struct Scoped<T> { pub targets: Vec<Target>, pub value: T }
pub struct Exports {
pub symbols: Vec<String>,
pub weak_symbols: Vec<String>,
pub thread_local_symbols: Vec<String>,
pub objc_classes: Vec<String>,
pub objc_eh_types: Vec<String>,
pub objc_ivars: Vec<String>,
}
v3 and v4 both supported; v4 is what modern Xcode ships.
3. Materialize into DylibFile
Tbd::into_dylib_file(tbd: Tbd, for_target: Target) -> DylibFile. Filters scoped entries to only those matching arm64 / macos. Produces the same DylibFile surface Sprint 5 produces, so downstream code doesn't care about source format.
4. SDK search implementation
Integrate with -syslibroot. Search order for -l<name>:
${SDK}/usr/lib/lib<name>.tbd${SDK}/usr/lib/lib<name>.dylib${SDK}/usr/local/lib/lib<name>.tbd${SDK}/usr/local/lib/lib<name>.dylib-L<dir>entries in order, same four suffixes.
For frameworks (-framework Foo): ${SDK}/System/Library/Frameworks/Foo.framework/Foo.{tbd,dylib}.
5. Platform/arch filtering
Target { arch: Arm64, platform: MacOS } is what armfortas cares about. If the TBD has no matching target, produce a clear diagnostic: "<path> does not export for arm64-macos".
Testing Strategy
- Fixtures: copies of
${SDK}/usr/lib/libSystem.tbd,libc++.tbd,libobjc.tbdchecked intotests/corpus/tbd/(small, just headers of exported symbols — confirm they're not under a license that forbids redistribution; if so, generate equivalent fixtures). - Parse
libSystem.tbd, assert that_dyld_stub_binder,_malloc,_free,_printfare all in exports. - Verify
DylibFileproduced is byte-level equivalent (in the fields we populate) to one produced by loading an actuallibSystem.dylibfrom an older SDK. - Malformed YAML: missing
install_name, tabs in indentation, unterminated quoted scalar — each with a precise diagnostic.
Definition of Done
- Can read a modern Xcode
libSystem.tbdand enumerate its exports. - SDK +
-l+-frameworkresolution picks the right file on a real toolchain. - Differential test: hello-world link with
libSystem.tbdproduces the same bind entries as with a binary dylib (on older SDKs where both exist).
View source
| 1 | # Sprint 6: TAPI TBD Text Stubs |
| 2 | |
| 3 | ## Prerequisites |
| 4 | Sprint 5 — binary dylib reader works. |
| 5 | |
| 6 | ## Goals |
| 7 | Read `.tbd` files (TAPI text dylib stubs). On modern SDKs `libSystem`, `libc++`, and `CoreFoundation` ship only as `.tbd` — linking without this sprint means no system libraries, full stop. |
| 8 | |
| 9 | ## Deliverables |
| 10 | |
| 11 | ### 1. Minimal YAML subset |
| 12 | TBD is YAML with a well-defined schema. We implement only the subset TAPI emits, not a general YAML parser: |
| 13 | |
| 14 | - Flow scalars (plain, single-quoted, double-quoted). |
| 15 | - Flow sequences: `[ a, b, c ]`. |
| 16 | - Block sequences: `- item`. |
| 17 | - Block mappings: `key: value`. |
| 18 | - Multi-document files with `---` / `...`. |
| 19 | - Tags: `!tapi-tbd`. |
| 20 | - Version directives: `%YAML 1.2`. |
| 21 | |
| 22 | No anchors, no aliases, no complex types, no folded scalars. If a real `.tbd` in the wild uses features outside the subset, the parser fails loudly with line/column. |
| 23 | |
| 24 | ### 2. TBD schema |
| 25 | `afs-ld/src/macho/tbd.rs`: |
| 26 | |
| 27 | ```rust |
| 28 | pub struct Tbd { |
| 29 | pub tbd_version: u32, // 3 or 4 |
| 30 | pub targets: Vec<Target>, // arch + platform |
| 31 | pub install_name: String, |
| 32 | pub current_version: Option<String>, |
| 33 | pub compatibility_version: Option<String>, |
| 34 | pub parent_umbrella: Vec<Scoped<String>>, |
| 35 | pub allowable_clients: Vec<Scoped<String>>, |
| 36 | pub reexported_libraries: Vec<Scoped<String>>, |
| 37 | pub exports: Vec<Scoped<Exports>>, |
| 38 | pub reexports: Vec<Scoped<Exports>>, |
| 39 | } |
| 40 | |
| 41 | pub struct Target { pub arch: Arch, pub platform: Platform } |
| 42 | pub struct Scoped<T> { pub targets: Vec<Target>, pub value: T } |
| 43 | pub struct Exports { |
| 44 | pub symbols: Vec<String>, |
| 45 | pub weak_symbols: Vec<String>, |
| 46 | pub thread_local_symbols: Vec<String>, |
| 47 | pub objc_classes: Vec<String>, |
| 48 | pub objc_eh_types: Vec<String>, |
| 49 | pub objc_ivars: Vec<String>, |
| 50 | } |
| 51 | ``` |
| 52 | |
| 53 | v3 and v4 both supported; v4 is what modern Xcode ships. |
| 54 | |
| 55 | ### 3. Materialize into DylibFile |
| 56 | `Tbd::into_dylib_file(tbd: Tbd, for_target: Target) -> DylibFile`. Filters scoped entries to only those matching `arm64 / macos`. Produces the same `DylibFile` surface Sprint 5 produces, so downstream code doesn't care about source format. |
| 57 | |
| 58 | ### 4. SDK search implementation |
| 59 | Integrate with `-syslibroot`. Search order for `-l<name>`: |
| 60 | 1. `${SDK}/usr/lib/lib<name>.tbd` |
| 61 | 2. `${SDK}/usr/lib/lib<name>.dylib` |
| 62 | 3. `${SDK}/usr/local/lib/lib<name>.tbd` |
| 63 | 4. `${SDK}/usr/local/lib/lib<name>.dylib` |
| 64 | 5. `-L<dir>` entries in order, same four suffixes. |
| 65 | |
| 66 | For frameworks (`-framework Foo`): `${SDK}/System/Library/Frameworks/Foo.framework/Foo.{tbd,dylib}`. |
| 67 | |
| 68 | ### 5. Platform/arch filtering |
| 69 | `Target { arch: Arm64, platform: MacOS }` is what armfortas cares about. If the TBD has no matching target, produce a clear diagnostic: "`<path>` does not export for arm64-macos". |
| 70 | |
| 71 | ## Testing Strategy |
| 72 | - Fixtures: copies of `${SDK}/usr/lib/libSystem.tbd`, `libc++.tbd`, `libobjc.tbd` checked into `tests/corpus/tbd/` (small, just headers of exported symbols — confirm they're not under a license that forbids redistribution; if so, generate equivalent fixtures). |
| 73 | - Parse `libSystem.tbd`, assert that `_dyld_stub_binder`, `_malloc`, `_free`, `_printf` are all in exports. |
| 74 | - Verify `DylibFile` produced is byte-level equivalent (in the fields we populate) to one produced by loading an actual `libSystem.dylib` from an older SDK. |
| 75 | - Malformed YAML: missing `install_name`, tabs in indentation, unterminated quoted scalar — each with a precise diagnostic. |
| 76 | |
| 77 | ## Definition of Done |
| 78 | - Can read a modern Xcode `libSystem.tbd` and enumerate its exports. |
| 79 | - SDK + `-l` + `-framework` resolution picks the right file on a real toolchain. |
| 80 | - Differential test: hello-world link with `libSystem.tbd` produces the same bind entries as with a binary dylib (on older SDKs where both exist). |