Skip to content

feat(class-dump): stable diff-friendly headers and per-path output for DSC#1196

Closed
Lessica wants to merge 6 commits into
blacktop:masterfrom
Lessica:main
Closed

feat(class-dump): stable diff-friendly headers and per-path output for DSC#1196
Lessica wants to merge 6 commits into
blacktop:masterfrom
Lessica:main

Conversation

@Lessica

@Lessica Lessica commented Apr 28, 2026

Copy link
Copy Markdown

Summary

Two related improvements to ipsw class-dump --headers, motivated by headers.82flex.com (a per-version archive of generated Apple framework headers):

  1. Stable, diff-friendly output. Member order is now deterministic across runs, so cross-version diffs surface real ABI changes instead of churn.
  2. No output collisions for DSC dylibs sharing a basename. e.g. /System/Library/PrivateFrameworks/SpringBoard.framework/SpringBoard vs. /System/Library/AccessibilityBundles/SpringBoard.axbundle/SpringBoard — previously they overwrote each other under <-o>/SpringBoard/.

Changes

  • Generic dedupeKeepLastByKey + lexicographic sort on every class/protocol/category member array.
  • Umbrella header is always <Name>-Umbrella.h (avoids colliding with a class named after the framework, e.g. SpringBoard.h); umbrella import list is sorted.
  • For DSC inputs, headers are routed under <-o>/<framework-relative-path> (derived from the dylib id, hardened with filepath.Clean + filepath.IsLocal against .. traversal). Standalone Mach-O inputs keep the basename layout.
  • XCFramework() modulemap updated to umbrella header "<Name>-Umbrella.h" and forces a flat Headers/ layout to match.
  • o.conf.Name is saved/restored per writeHeaders call so --deps cannot leak a previous dylib's name into the next path derivation.

Why no property-accessor filter is reintroduced

The previous code collected @property names into props/setters and dropped ivars/methods that matched them. This PR removes that filter (rather than restoring it after the new sort/dedupe) because:

  • It's a name-based heuristic that doesn't honor explicit getter= / setter= and can drop unrelated methods that happen to match set<Cap>: (the original code even had a TODO acknowledging this).
  • It hides the exact signals the headers archive exists to surface — methods promoted to properties, _foo ivar vs. foo property, accessors moving between class and protocol.
  • It is not needed for output stability; dedupeKeepLastByKey + sort handle that uniformly.

Testing

DSC=22A3351__iPhone17,1/.../dyld_shared_cache_arm64e
./ipsw class-dump --headers -o out "$DSC" /System/Library/PrivateFrameworks/SpringBoard.framework/SpringBoard
./ipsw class-dump --headers -o out "$DSC" /System/Library/AccessibilityBundles/SpringBoard.axbundle/SpringBoard
./ipsw class-dump --headers --deps -o out-deps "$DSC" /System/Library/PrivateFrameworks/SpringBoard.framework/SpringBoard
./ipsw class-dump --xcfw    -o xcfw "$DSC" /usr/lib/libMobileGestalt.dylib
./ipsw class-dump --all --headers -o all "$DSC"

Both SpringBoard dylibs land in distinct directories; --deps headers are correctly routed per-dependency; the generated XCFramework's modulemap resolves against the emitted <Name>-Umbrella.h.

AI Assistance

  • Claude Opus 4.7

Copilot AI review requested due to automatic review settings April 28, 2026 17:10
@Lessica

Lessica commented Apr 28, 2026

Copy link
Copy Markdown
Author

Related to #245

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves ipsw class-dump --headers determinism (stable, diff-friendly header generation) and fixes output directory collisions when dumping multiple DSC dylibs that share the same basename.

Changes:

  • Add deterministic member handling for ObjC classes/protocols/categories via de-duping and lexicographic sorting, plus stable umbrella header generation.
  • For DSC inputs, route header output under an on-disk directory derived from the dylib’s framework-relative path rather than <output>/<basename>/ to avoid collisions.
  • Minor header formatting tweak in writeHeader.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
internal/commands/macho/objc.go Implements stable ordering/de-duping and introduces per-path output routing for DSC header dumps.
cmd/ipsw/cmd/class_dump.go Passes full DSC image path into ObjC config when --headers is enabled to support per-path output routing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/commands/macho/objc.go
Comment thread internal/commands/macho/objc.go Outdated
Comment thread internal/commands/macho/objc.go Outdated
Lessica added 3 commits April 29, 2026 01:26
Make 'ipsw class-dump --headers' output deterministic so generated
headers can be diffed across builds:

- Add a generic dedupeKeepLastByKey helper and run it on every
  class/protocol/category member array (ivars, props, methods,
  protocols, properties) so duplicate definitions emitted by the
  compiler do not produce flapping output.
- Replace the previous best-effort filtering of property-backed
  ivars/methods with stable lexicographic sorting on every member
  collection, ensuring the output order does not depend on traversal
  order of the binary.
- Sort the umbrella header import list and unconditionally suffix the
  umbrella header with '-Umbrella' so the umbrella file name never
  collides with a generated class header.
- Insert a blank line after the metadata header block in writeHeader
  for consistent separation between metadata and includes.
…sename

When dumping headers from a dyld shared cache, two dylibs with the same
file name (for example a public 'Foo.framework/Foo' and a private
'/usr/lib/Foo') would write to the same '<output>/Foo' directory and
clobber each other's headers and umbrella file.

Resolve by routing the in-cache dylib path through to the writer:

- cmd/ipsw/cmd/class_dump.go: when --headers is set, pass the full
  in-cache image name to ObjcConfig.Name instead of just its basename
  so the writer can recover a unique relative path.
- internal/commands/macho/objc.go: split the writer's notion of
  identity into a 'binaryName' (file basename, used for the umbrella
  name and base-framework checks) and a 'frameworkRelPath' (the
  relative directory under -o, derived from the dylib id when present
  or from the supplied path otherwise). All generated header files now
  resolve under '<output>/<frameworkRelPath>/' instead of
  '<output>/<basename>/', so dylibs with colliding basenames land in
  separate directories. Standalone (non-DSC) MachO inputs continue to
  use the basename, preserving previous behavior.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/commands/macho/objc.go Outdated
Comment thread internal/commands/macho/objc.go
…across deps

- XCFramework(): switch generated modulemap to a real `umbrella header`
  directive pointing at `<Name>-Umbrella.h` (matches the new umbrella
  filename emitted by Headers()), and force Headers() to lay headers
  directly under <fw>.framework/Headers/ via a new flatOutput flag so
  the modulemap reference resolves.
- Headers(): save/restore o.conf.Name with a deferred restore so a
  --deps iteration cannot leak the previous dylib's binaryName into
  the next frameworkRelPath derivation.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/commands/macho/objc.go
…ule name

For dylib inputs, the umbrella header was named 'libFoo.dylib-Umbrella.h'
and the XCFramework modulemap declared 'module libFoo.dylib', neither of
which is desirable: the file gets a redundant '.dylib' segment, and
modulemap module identifiers can't legally contain '.'. Strip a single
trailing extension via filepath.Ext so the umbrella becomes
'libFoo-Umbrella.h' and the module is just 'libFoo'. Framework binaries
(no extension) are unaffected.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

internal/commands/macho/objc.go:702

  • umbrellaBase only strips the trailing extension from o.conf.Name, but dylibs can have additional dots in the stem (e.g. libobjc.A.dylib -> libobjc.A). That dot survives into umbrella and then into the headerInfo.Name used for the #ifndef/#define guard, producing an invalid preprocessor identifier and headers that won’t compile. Consider deriving a separate sanitized identifier for header guards (e.g. map any non [A-Za-z0-9_] to _ and ensure it doesn’t start with a digit) rather than only replacing -.
			// Strip a trailing extension (e.g. ".dylib") so the umbrella file is
			// named "<Stem>-Umbrella.h" rather than "<Foo>.dylib-Umbrella.h".
			umbrellaBase := strings.TrimSuffix(o.conf.Name, filepath.Ext(o.conf.Name))
			var umbrella = umbrellaBase + "-Umbrella"
			slices.SortStableFunc(headers, func(a, b string) int {
				return cmp.Compare(a, b)
			})

			for i, header := range headers {
				headers[i] = "#import \"" + header + "\""
			}

			fname := filepath.Join(frameworkDir, umbrella+".h")
			if err := writeHeader(&headerInfo{
				FileName:      fname,
				IpswVersion:   o.conf.IpswVersion,
				BuildVersions: buildVersions,
				SourceVersion: sourceVersion,
				IsUmbrella:    true,
				Name:          strings.ReplaceAll(umbrella, "-", "_"),
				Object:        strings.Join(headers, "\n") + "\n",

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/commands/macho/objc.go Outdated
Comment thread internal/commands/macho/objc.go Outdated
@blacktop

Copy link
Copy Markdown
Owner

Dup of #1106

…mework names

- Sanitize all strings used as C/Clang preprocessor identifiers (#ifndef
  guards, modulemap module names) so umbrella stems like 'libobjc.A' or
  ObjC categories with '+' produce headers that compile.
- Drop --deps emission while writing an XCFramework: in flat-headers
  mode deps would otherwise share Headers/ and not be covered by the
  umbrella.
- Use the extension-stripped stem for every on-disk XCFramework name
  (.xcframework, .framework, .tbd) and Info.plist field, so dylibs no
  longer leave a '.dylib' segment in the bundle tree.
@Lessica

Lessica commented Apr 28, 2026

Copy link
Copy Markdown
Author

Dup of #1106

No. It's not a duplicate.

This PR is trying to implement part of #245 Create diff-able versions of most output

See: https://headers.82flex.com/view/26.2_23C55/System/Library/Frameworks/CFNetwork.framework/CFNetwork/NSHTTPCookie.h

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants