Extensibility
How to extend ipa-forge with new patch operation types and signing providers.
Adding a new patch operation type
Implement the protocol. PatchOperation (ipa_forge/patch/base.py):
dry_run(ctx) -> PatchResult and apply(ctx) -> PatchResult. Look at
patch/resource.py for the simplest example.
Add the schema. A pydantic spec class in patch/schema.py with a
type: Literal[...] discriminator field, added to the PatchSpec union.
Wire it up. In patch/loader.py::build_operations.
Place it in the apply order. If it mutates a Mach-O file, decide where it
belongs in patch/engine.py::_apply_order_rank — resource-only ops run
first, binary-offset patches next, Mach-O-load-command edits (which can
shift binary layout) last. A completely new mutation category should get
its own rank rather than reusing an existing one if its ordering constraints
differ.
Add tests. Exercise both the dry-run failure paths and a real apply,
following tests/unit/test_resources.py or tests/unit/test_binary_patch.py.
The core engine (pipeline.py, patch/engine.py, patch/resolver.py) never
needs to change for a new operation type — that's the point of the
PatchOperation protocol.
Implementing AltStoreCredentialProvider
signing/provider.py::AltStoreCredentialProvider is currently a stub. A real
implementation would need to:
- Obtain a signing identity + provisioning profile through AltServer's own
Apple-account pairing flow, rather than assuming one is already sitting in
the local Keychain (
LocalIdentityProvider's assumption). - Implement the same four abstract methods (
list_identities,sign,verify,dump_entitlements) —pipeline.pyandcli/main.pyare already written against theSigningProviderinterface, not againstLocalIdentityProviderdirectly, so no other code should need to change. - Handle the account-level constraints AltStore Classic itself enforces (7-day expiry, active-app count, App-ID-per-week limits) — see Architecture's AltStore flow diagram.
Linux-only analysis mode
Everything except ipa_forge/signing/ runs without macOS: extraction,
repacking, plist inspection, patch resolution, resource patching, and binary
pattern analysis/modification (bundle/, patch/, machO/arch.py,
machO/injector.py). This means forge inspect, forge validate, and
forge patch --dry-run (up through the dry-run gate) all work on Linux
today without any code changes — only the signing stages (load_profile_pool
onward) require codesign/security and therefore fail on non-macOS.
There is no runtime OS check gating this; it falls out naturally from which modules a given code path imports.
Worked examples of the extension seams above
Two features shipped by following exactly the steps described above, concrete enough to copy from:
- Per-extension provisioning profiles:
signing/profile.py::ProfilePoolmatches each profile-bearing target (app, app extension, watch app) to its own.mobileprovisionby its own bundle id; a single profile still signs everything, exactly as before (seesigning/pipeline.py::sign_bundle). plist_edit: aPatchOperation(patch/plist.py) for set/remove on bundle-relative plists — a compact worked example of the steps above.
The hooks: block (declare runtime hook targets that pipeline.py verifies
during the dry-run gate — see Patch Reference) is
not a new operation type; it's a verification surface layered on top of the
same definition file, which is why it needed no changes to the extension
seams above.
For the full history of what shipped in each release, see
CHANGELOG.md.