feat(class-dump): stable diff-friendly headers and per-path output for DSC#1196
feat(class-dump): stable diff-friendly headers and per-path output for DSC#1196Lessica wants to merge 6 commits into
Conversation
|
Related to #245 |
There was a problem hiding this comment.
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.
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.
…sc-output-dir-conflict'
There was a problem hiding this comment.
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.
…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.
There was a problem hiding this comment.
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.
…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.
There was a problem hiding this comment.
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
umbrellaBaseonly strips the trailing extension fromo.conf.Name, but dylibs can have additional dots in the stem (e.g.libobjc.A.dylib->libobjc.A). That dot survives intoumbrellaand then into theheaderInfo.Nameused for the#ifndef/#defineguard, 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.
|
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.
|
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. |
Summary
Two related improvements to
ipsw class-dump --headers, motivated by headers.82flex.com (a per-version archive of generated Apple framework headers):/System/Library/PrivateFrameworks/SpringBoard.framework/SpringBoardvs./System/Library/AccessibilityBundles/SpringBoard.axbundle/SpringBoard— previously they overwrote each other under<-o>/SpringBoard/.Changes
dedupeKeepLastByKey+ lexicographic sort on every class/protocol/category member array.<Name>-Umbrella.h(avoids colliding with a class named after the framework, e.g.SpringBoard.h); umbrella import list is sorted.<-o>/<framework-relative-path>(derived from the dylib id, hardened withfilepath.Clean+filepath.IsLocalagainst..traversal). Standalone Mach-O inputs keep the basename layout.XCFramework()modulemap updated toumbrella header "<Name>-Umbrella.h"and forces a flatHeaders/layout to match.o.conf.Nameis saved/restored perwriteHeaderscall so--depscannot leak a previous dylib's name into the next path derivation.Why no property-accessor filter is reintroduced
The previous code collected
@propertynames intoprops/settersand dropped ivars/methods that matched them. This PR removes that filter (rather than restoring it after the new sort/dedupe) because:getter=/setter=and can drop unrelated methods that happen to matchset<Cap>:(the original code even had aTODOacknowledging this)._fooivar vs.fooproperty, accessors moving between class and protocol.dedupeKeepLastByKey+ sort handle that uniformly.Testing
Both
SpringBoarddylibs land in distinct directories;--depsheaders are correctly routed per-dependency; the generated XCFramework's modulemap resolves against the emitted<Name>-Umbrella.h.AI Assistance