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.py and cli/main.py are already written against the SigningProvider interface, not against LocalIdentityProvider directly, 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::ProfilePool matches each profile-bearing target (app, app extension, watch app) to its own .mobileprovision by its own bundle id; a single profile still signs everything, exactly as before (see signing/pipeline.py::sign_bundle).
  • plist_edit: a PatchOperation (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.

On this page