Introduction

Code signing is one of the few operations that takes just as long to do for an incremental build as for a clean build. It also takes more time the larger an app grows in size. As a result, it can become a bottleneck for incremental builds. Here are some tricks to reduce that time. They’re all technically undocumented and may break in the future, but they’re also used by large companies with no apparent downsides.

Note: these tricks are for debug builds only.

Note: see how much time code signing takes during builds, i.e. how much time you can actually save, and decide if that amount matters to you.

Speeding up code signing

Faster hashing

Changing the hash algorithm that code signing uses is the easiest, lowest-risk change of these. We can have it use SHA-1, which is faster than the default SHA-256, by adding --digest-algorithm=sha1 to “Other Code Signing Flags” in the Xcode build settings. Isn’t SHA-1 insecure, you say? In some situations, yes, but here you probably wouldn’t care since it’s a personal debug build.

Resource rules

Now that we’ve made hashing faster, we’re going to reduce the amount of stuff we have to hash in the first place. Resource rules are rules, given in a plist, telling codesign what to sign and what not to. Since so much time of code signing is taken up by hashing files in the bundle besides the binary, and since Apple APIs seem only to care if the binary has been hashed, we can use resource rules to have Apple exclude everything but the binary. Here’s the plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
       <key>rules</key>
       <dict>
               <key>.*</key>
               <false/>
       </dict>
</dict>
</plist>

Put that somewhere in your project and add --resource-rules=<path to the plist> to “Other Code Signing Flags” in the Xcode build settings. Although this flag was deprecated in iOS 7, it still seems to work on both Xcode 11 and 12. There’s also an alternative way to do this. It’s less recommended because it’s more work, but the one benefit is that it doesn’t require this old flag. For this, create an empty directory MyApp.app somewhere temporary move the binary, MyApp, to it. I.e., you have your normal app bundle now but with everything besides the binary removed. Now, codesign that bundle and then move the binary afterwards back to where it was. This will have only signed the binary and no other resources, since the bundle consisted of nothing but the binary.

Optional hack for the simulator

The above changes will reduce code signing time but still preserve functionality. However, for the simulator, there’s another shortcut. The only downside is that it will break app groups and possibly other entitlements (though most entitlements, such as keychain and sign in with Apple, will still work). If you do this change, undo the previous changes for simulator signing, as they won’t offer any benefits on top of this. Some background: the code signing process consists of both computing hashes and writing their results somewhere, as well as adding the entitlements plist to the binary. Since most APIs will only check if their entitlement is in the entitlements plist in the binary (if they check at all), and since the simulator otherwise doesn’t care if the binary is signed, we can just inject the plist into the binary ourselves.

How to inject entitlements like the code signing step does:

  • Use xcrun segedit <path to simulator binary> -extract __TEXT __entitlements - to see the actual entitlements plist that codesign puts in the binary.
  • Compare those entitlements to the entitlements file you supplied to Xcode, and you should see how to go between the two (possibly just variable substitution with Xcode build environment variables).
  • In a post-build phase, run a script doing that conversion.
  • Add -Wl,-sectcreate,__TEXT,__entitlements,<the processed entitlements file path> to “Other Linker Flags”. This will instruct the linker to inject that entitlements file into a section called __entitlements, just like the code signing step would do.
  • In an .xcconfig file, set CODE_SIGNING_ALLOWED = NO to disable Xcode’s code signing step.

Conclusion

By following these steps, you can improve the incremental build time for all debug builds. Since these are still hacks though, feel free to let me know on Twitter if anything didn’t work. Note that Apple’s code signing tools are woefully under-optimized, e.g. because they’re single-threaded, but hopefully in the future they or some open-source project will make a faster version. For a really deep dive into code signing, consider reading MacOS and iOS Internals, Volume III.

Special thanks to Milen Dzhumerov and Keith Smiley for their help with this post.