Packaging iOS apps from the command line

by Dan Cutting

The blessed way to submit an iOS app to the App Store is to use Xcode. This works well for independent developers, but is a pain when working as part of a larger team with a continuous integration system.

In these situations we need to build and package apps via command line scripts to produce build artefacts ready for submission. The command line tools that ship with Xcode enable this:

xcodebuild -workspace "MyApp.xcworkspace" -scheme MyApp clean build
xcrun -sdk iphoneos PackageApplication -v MyApp.app \
  -o MyApp.ipa --sign "iPhone Distribution: My Company"

This (ostensibly) produces an IPA we can upload through Application Loader to iTunes Connect.

Code signing bug

The second step above invokes the PackageApplication Perl script supplied as part of the command line tools with Xcode (found at /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/PackageApplication).

Since Xcode 6 there’s been a bug in this script that prevents successful signing and packaging of App Store builds. At the point of code signing the application, we get the following error:

error: /usr/bin/codesign --force --preserve-metadata=identifier,entitlements,resource-rules \
  --sign iPhone Distribution: My Company \
  --resource-rules=/var/folders/.../Payload/MyApp.app/ResourceRules.plist \
  --entitlements /var/folders/.../entitlements_plistGGqmBvdU \
  /var/folders/.../Payload/MyApp.app failed with error 1.
Output: Warning: usage of --preserve-metadata with option "resource-rules" (deprecated in Mac OS X >= 10.10)!

As of Mavericks, the resource-rules option should not be included when signing apps, but for some reason the script still passes this option along to the code signing tool.

The workaround is to alter the PackageApplication script so it no longer does this (which will require sudo access). Line 155 of the script constructs the parameters to pass through to the code signer, so we can simply remove the references to resource-rules:

- my @codesign_args = ("/usr/bin/codesign", "--force", "--preserve-metadata=identifier,entitlements,resource-rules",
-   "--sign", $opt{sign},
-   "--resource-rules=$destApp/ResourceRules.plist");
+ my @codesign_args = ("/usr/bin/codesign", "--force", "--preserve-metadata=identifier,entitlements",
+   "--sign", $opt{sign});

At this point, we can again use the command line tools to generate a signed App Store IPA.

TestFlight

However, if we want to use Apple’s new TestFlight Beta Testing service, there is another wrinkle.

After submitting our IPA, we may see an error that our build does not have the beta-reports-active entitlement. Without this, we cannot distribute the build to testers.

According to Apple, this is caused by signing the build with an old distribution provisioning profile which does not contain the entitlement. This may be true, but it’s only half the story.

Even if we create and use a brand new distribution provisioning profile, the build may be rejected by TestFlight as not having the entitlement. What’s going on?

If our project contains an explicit Entitlements.plist file, then we need to make sure it also includes the new entitlement. Unfortunately, this entitlement should only be present for App Store builds, not Ad Hoc builds, so we may need to create separate plists for different configurations.

However, it’s more usual these days for a project not to contain an explicit Entitlements.plist. Xcode automatically ensures the appropriate entitlements are embedded at build and sign time. But an oversight in the tools means the beta-reports-active entitlement does not get added if building from the command line.

We can modify the PackageApplication script a little more to ensure this entitlement is included in App Store builds.

+ runCmd('/usr/libexec/PlistBuddy', '-c', 'Add :beta-reports-active bool', $entitlements_plist);
+ runCmd('/usr/libexec/PlistBuddy', '-c', 'Set :beta-reports-active YES', $entitlements_plist);

This workaround, plus the use of a recent distribution provisioning profile, will result in an IPA that contains the necessary entitlement and can be uploaded to TestFlight and distributed to testers.

Conclusion

The Xcode IDE continues to improve with each iteration. However, Apple sometimes seems to neglect the command line tools that accompany it. Since automated builds are becoming a fixture of the iOS landscape, it is vital that Apple properly supports this alternate toolchain.

In the meantime, here’s the complete patch for the PackageApplication script:

--- PackageApplication.bak	2015-04-09 12:14:29.000000000 +0100
+++ PackageApplication	2015-04-09 14:35:35.000000000 +0100
@@ -146,15 +146,16 @@
             close($ofh) or fatal("Cannot close file handle for '$entitlements_plist': $!");
 
             runCmd('/usr/libexec/PlistBuddy', '-c', 'Set :get-task-allow NO', $entitlements_plist);
+            runCmd('/usr/libexec/PlistBuddy', '-c', 'Add :beta-reports-active bool', $entitlements_plist);
+            runCmd('/usr/libexec/PlistBuddy', '-c', 'Set :beta-reports-active YES', $entitlements_plist);
             # PlistBuddy will fail if get-task-allow doesn't exist. That's okay.
             runCmd('/usr/bin/plutil', '-lint', $entitlements_plist);
             $? == 0 or fatal("Invalid plist at '$entitlements_plist'");
         }
     }
 
-    my @codesign_args = ("/usr/bin/codesign", "--force", "--preserve-metadata=identifier,entitlements,resource-rules",
-                         "--sign", $opt{sign},
-                         "--resource-rules=$destApp/ResourceRules.plist");
+    my @codesign_args = ("/usr/bin/codesign", "--force", "--preserve-metadata=identifier,entitlements",
+                         "--sign", $opt{sign});
 
     if ( -e $entitlements_plist ) {
         push(@codesign_args, '--entitlements');

Hi, I'm Dan Cutting. I mainly write Swift for iOS but love working on Mac apps, web APIs, and programming languages too.

Follow me on Twitter, Micro.blog, or GitHub or subscribe to the feed.