Generate private framework headers for iOS and macOS.
- iOS: dump from simulator runtimes and dyld shared caches.
- macOS: dump from host
/System/Library/{Frameworks,PrivateFrameworks}.
The PrivateHeaderKit CLI, installer, and internal helpers support Darwin/macOS hosts only. Non-Darwin hosts such as Linux are unsupported.
PrivateHeaderKit exposes one user-facing command:
privateheaderkitThe old privateheaderkit-dump, headerdump, and headerdump-sim names are not
installed as user-facing commands. Low-level raw dumping is handled by internal
helpers that are installed and updated with the public command as one cohort.
On an Apple Silicon Mac, install the latest validated release cohort with the version-baked installer:
curl -fsSLO https://github.com/lynnswap/PrivateHeaderKit/releases/latest/download/install.sh
sh install.shEach release has exactly three downloadable assets:
install.shSHA256SUMS.txtprivateheaderkit-darwin-arm64.tar.gz
The version-baked installer downloads the matching archive and checksum file,
verifies the archive checksum and exact archive contents, then validates
release.json and all three executables for SHA-256, architecture, platform,
executable permissions, and code signature. Only after the complete cohort has
passed validation does it publish the immutable cohort and switch the active
current link. By default, the stable user-facing command is
~/.local/bin/privateheaderkit.
If ~/.local/bin is not in your PATH, add it:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrcTo choose another prefix or command directory:
sh install.sh --prefix "$HOME/.local"
# or: this uses $HOME as the prefix and $HOME/bin for the stable command
sh install.sh --bindir "$HOME/bin"To build and install all artifacts from a checkout instead:
git clone https://github.com/lynnswap/PrivateHeaderKit.git
cd PrivateHeaderKit
swift run -c release privateheaderkit-installRelease and source installs use the same immutable layout:
~/.local/bin/privateheaderkit
-> ../libexec/privateheaderkit/current/privateheaderkit
~/.local/libexec/privateheaderkit/
current -> versions/<version>+<cohort-sha256>
versions/<version>+<cohort-sha256>/
privateheaderkit
privateheaderkit-raw-helper
privateheaderkit-sim-helper
release.json
Only privateheaderkit is public. Its two helpers are always resolved through
the same validated cohort. To update, download and run the latest install.sh
again, or update the source checkout and rerun swift run -c release privateheaderkit-install. A failed build, download, validation, or staging
step leaves the previous validated cohort active. If activation fails, the
installer attempts to restore the previous cohort and reports any rollback
failure instead of hiding it.
Untagged source checkouts use a commit-qualified
0.0.0-dev.<short-commit> version namespace.
An older direct install containing the three executable files is migrated under
the installer lock. If that migration is interrupted, the next install recovers
it from the recorded migration intent. A partial or ambiguous legacy layout is
rejected, and recovery refuses to guess if its files changed after the intent
was recorded. A retired helper whose contents changed is left untouched; once
the validated current and public command links establish the managed layout,
that leftover does not block a later managed install.
Maintainers prepare a release with the Prepare Draft Release workflow. It must
be dispatched with a version and the full SHA of the current default-branch
HEAD. The workflow creates or repairs a draft GitHub Release with exactly the
three assets listed above. Publishing remains a manual step after the draft and
release notes have been reviewed.
privateheaderkit
privateheaderkit --helpRunning privateheaderkit without arguments starts the interactive generation
wizard; its default output base is ~/PrivateHeaderKit. The wizard asks for an
explicit Continue or Restart decision when compatible unfinished state exists,
and asks for confirmation before migrating legacy state or output. If both are
present, the same confirmation lists both paths and their preservation or
backup effects before migration. For automation and CI, pass generation
options directly:
Generated headers are written to
~/PrivateHeaderKit/generated-headers/<source-storage-id>/. The command also
prints this concrete destination when a run starts.
privateheaderkit --platform iOS --version 27.0 --build 24A5355q --out "$HOME/PrivateHeaderKit" --target "SwiftUI,UIKit"
privateheaderkit --platform iOS --version 27.0 --build 24A5355q --system-root /path/to/RuntimeRoot --device "iPhone 17" --out "$HOME/PrivateHeaderKit" --target "SwiftUI,UIKit" --fresh
privateheaderkit --platform macOS --version 16.0 --system-root / --out "$HOME/PrivateHeaderKit" --target "AppKit,Foundation" --resumeFor iOS generation, privateheaderkit resolves an available simulator runtime
from --version and --build, selects and boots a simulator device, and uses
the internal simulator helper. --system-root is optional for iOS; when
supplied, it is used as the runtime root instead of being replaced by the
resolved runtime path. --device <name-or-udid> and --sim-helper <path> are
optional automation flags.
--target is a comma-separated target query, not a stable target ID list.
--resume and --fresh are mutually exclusive:
--resumecontinues the latest compatible plan and runs its unfinished or missing targets. A changed plan fingerprint or a smaller selected target set is rejected as incompatible.--freshstarts a new run for every selected target. It also permits the explicit migration of legacy JSON state and a legacy output directory.- With neither flag, a new run starts when no prior state exists. Compatible
state with no unfinished work may be reused, but unfinished state is rejected
until the caller explicitly chooses
--resumeor--fresh.
--fresh does not delete the currently published header tree before work
starts. A completed target updates the files it owns under
generated-headers/<source-storage-id>/ before the next target starts. A later
target or finalization failure does not remove those files, and --resume
continues without rerunning targets that were already published. Failed or
interrupted targets retain their last successfully published files. The
immutable generation finalized at the end of a run is an internal recovery
snapshot, not a gate that hides headers while generation is in progress. The
old <version> positional style is not part of the public surface.
On a TTY, the active target is one transient line animated as ., .., and
.... Successful lines are replaced by the next target instead of accumulating
in the terminal; only failed targets and concise diagnostics remain. Normal raw
helper output is not forwarded to the terminal. A bounded diagnostic tail for
a failed target is retained as its SQLite failureSummary.
The output base contains the incrementally visible header tree, internal immutable recovery generations, and one SQLite state database per source:
<output-base>/
generated-headers/
<source-storage-id>/
Frameworks/...
PrivateFrameworks/...
SystemLibrary/...
usr/lib/...
.privateheaderkit/
<source-storage-id>/
current -> generations/<generation-id>
generations/
<generation-id>/
.privateheaderkit-generation.json
Frameworks/...
PrivateFrameworks/...
legacy-backups/...
.state/
<source-storage-id>/
generation.sqlite
--out selects <output-base>. Consumers use
<output-base>/generated-headers/<source-storage-id>/. Completed target
artifacts and their SQLite published-target records become durable per target;
the internal snapshot under .privateheaderkit is reconciled at the end of a
run. The legacy-backups directory is created only when a pre-rewrite output
directory is migrated. Generation state, target attempts, publication intent,
and run logs are stored in generation.sqlite. The storage ID is versioned and
owned by PrivateHeaderKit; consumers must not construct it from the displayed
source label.
Old .state/<source-storage-id>/manifest.json and runs/ data is not imported as
resumable state. When those paths exist without generation.sqlite, a run
without --fresh stops. An explicit fresh migration creates
generation.sqlite and leaves the old JSON paths in place.
An existing real <output-base>/<source-storage-id>/ directory is not silently
adopted. With --fresh, PrivateHeaderKit inventories and copies its regular
files into the draft generation as opaque, unowned files. It then uses an
atomic directory/symlink swap to publish the managed stable path and retains
the original directory under
.privateheaderkit/<source-storage-id>/legacy-backups/. If the legacy tree cannot be
validated or the filesystem cannot perform the atomic swap, migration fails
without replacing the original output path.
- iOS runtime discovery and simulator execution require Xcode with
xcrun,simctl, and an installed iPhone Simulator SDK. - Building from source requires the Swift 6.3 toolchain. Source installation
additionally requires Xcode with
xcrunand an installed iPhone Simulator SDK because the installed cohort includes the simulator helper. - State, logs, and staging data are kept outside the published generated header tree.
swift test is expected to be deterministic. Regular tests should use fixed fixture trees, injected environments, and stub command runners only.
Compile the platform-neutral Core target and its test surface for iOS without launching a simulator:
PHK_IOS_SIMULATOR_SDK="$(xcrun --sdk iphonesimulator --show-sdk-path)"
PHK_IOS_SIMULATOR_TRIPLE="$(uname -m)-apple-ios17.0-simulator"
PHK_IOS_BUILD_SCRATCH="$PWD/.build/privateheaderkit-ios-compile/$PHK_IOS_SIMULATOR_TRIPLE"
swift build --scratch-path "$PHK_IOS_BUILD_SCRATCH" --sdk "$PHK_IOS_SIMULATOR_SDK" --triple "$PHK_IOS_SIMULATOR_TRIPLE" --target PrivateHeaderKitCore
swift build --scratch-path "$PHK_IOS_BUILD_SCRATCH" --sdk "$PHK_IOS_SIMULATOR_SDK" --triple "$PHK_IOS_SIMULATOR_TRIPLE" --target PrivateHeaderKitCoreTestsDo not add regular tests that depend on the host dyld shared cache, installed system apps, simulator availability, runtime boot state, wall-clock time, generated swiftc binaries, network access, or stress loops. If an integration smoke test needs one of those dependencies, guard it behind an explicit opt-in such as PHK_RUN_INTEGRATION_TESTS=1 and keep it out of the default acceptance path.
- MIT for this workspace: see
LICENSE.