Command line Watch Kit builds
Update: Please read this newer blog post before following the steps here. While these steps were valid at the time of writing, the new post outlines a better alternative without using any custom steps that are likely to fail in the future.
Overview
The Apple Watch is here! and Apple is now accepting Watch Kit enabled Apps. Now if you happen to do your builds outside the comfort of the Xcode application, chances are you will run into some issues. Namely a binary rejection from iTunes Connect after what you may have thought was a successful upload with the following message:
Invalid WatchKit Support - The bundle contains an invalid implementation of WatchKit.
Here are the steps I followed to get a build successfully submitted (till review stage at least). Depening on your project configuration, some steps may or may not apply, for dealing with the issue above only you can jump straight to step 6
Full disclosure, I did not come up with the solutions myself, I am just collating my findings into a single location in hope it will help as I found most of the information scattered around the interwebs. A fair bit of these steps were adapted from WDUK’s gist.
Steps
Let’s assume we have an app called MyApp, with a Watch Kit Extension called WatchKitExtension and Watch Kit App called WatchKitApp.
1) Update Plists
First, you’ll need to update the Info.plist for the App & the Extension.
I usually use python for this, as it has a built in plistlib which is semantically easier to deal with than the command line alternative.
import plistlib
# Read Plists
myAppInfoPlist = plistlib.readPlist(“MyApp/Info.plist”)
watchKitExtensionInfoPlist = plistlib.readPlist(“WatchKitExtension/Info.plist”)
watchKitAppInfoPlist = plistlib.readPlist(“WatchKitApp/Info.plist”)
# Update Watch Kit Extension Plist
watchKitExtensionInfoPlist[“NSExtension”][“NSExtensionAttributes”][“WKAppBundleIdentifier”] = watchKitAppInfoPlist[“CFBundleIdentifier”]
watchKitExtensionInfoPlist[“CFBundleVersion”] = myAppInfoPlist[“CFBundleVersion”]
WatchKitExtensionInfoPlist[“CFBundleShortVersionString”] = myAppInfoPlist[“CFBundleShortVersionString”]
# Update Watch Kit App Plist
watchKitAppInfoPlist[“WKCompanionAppBundleIdentifier”] = myAppInfoPlist[“CFBundleIdentifier”]
watchKitAppInfoPlist[“CFBundleVersion”] = myAppInfoPlist[“CFBundleVersion”]
watchKitAppInfoPlist[“CFBundleShortVersionString”] = myAppInfoPlist[“CFBundleShortVersionString”]
# Save the Plists
plistlib.writePlist(myAppInfoPlist, “MyApp/Info.plist”)
plistlib.writePlist(watchKitExtensionInfoPlist, “WatchKitExtension/Info.plist”)
plistlib.writePlist(watchKitAppInfoPlist, “WatchKitApp/Info.plist”)
2) Build
Issue your usual xcodebuild
command
for example:
xcrun xcodebuild -sdk iphoneos -config "Release"
This should spit out MyApp.app in build/Release-iphoneos/
3) Embed Provisioning Profiles
You’ll need to get a hold of provisioning profiles for the Watch App & Extension if you are not using a wildcard profile.
cp watchkitapp.mobileprovision MyApp.app/WatchKitExtension.appex/WatchKitApp.app/embedded.mobileprovision
cp watchkitextension.mobileprovision MyApp.app/WatchKitExtension.appex/embedded.mobileprovision
cp myapp.mobileprovision MyApp.app/embedded.mobileprovision
4) Get a hold of the expanded entitlements
The archive expanded entitlements will be needed for the signing step next, to get a copy of them ready you can run:
xcrun codesign -d --entitlements :- MyApp.app/WatchKitExtension.appex/WatchKitApp.app 2>/dev/null > watchkitapp.xcent
xcrun codesign -d --entitlements :- MyApp.app/WatchKitExtension.appex 2>/dev/null > watchkitextension.xcent
xcrun codesign -d --entitlements :- MyApp.app 2>/dev/null > myapp.xcent
- Edit:Thanks to Norod for the clarification on this step*.
5) Sign
As we now have modified the contents of the packages by embedding the missing provisioning profiles, we will need to re-sign them all. Note that the order of the following steps matter! This is due to the nature of the hierarchy these packages are in.
- MyApp.app
- Plugins
- WatchKitExtension.appex
- WatchKitApp.app
- …
- WatchKitExtension.appex
- Plugins
# Order is important!!
# WatchKit App
rm -rf MyApp.app/Plugins/MyWatchKitExtension.appex/MyWatchKitApp.app/_CodeSignature
xcrun codesign -f -s "Certificate Name" -- entitlements watchkitapp.xcent MyApp.app/Plugins/MyWatchKitExtension.appex/MyWatchKitApp.app
# WatchKit Extension
rm -rf MyApp.app/Plugins/MyWatchKitExtension.appex/_CodeSignature
xcrun codesign -f -s "Certificate Name" -- entitlements watchkitextension.xcent MyApp.app/Plugins/MyWatchKitExtension.appex
# Main App
rm -rf MyApp.app/_CodeSignature
xcrun codesign -f -s "Certificate Name" -- entitlements myapp.xcent MyApp.app
6) Package
The usual way involves running
xcrun -sdk iphoneos PackageApplication MyApp.app -o MyApp.ipa
What this does is make an .ipa (which if unzipped) will have the following structure
- Payload
- MyApp.app
However upon uploading to iTunes Connect the binary will end up getting rejected due to invalid Watch Kit Support. Turns out the command line tools don’t take care of all the necessary steps required for Watch Kit enabled apps. The missing step in this case is the requirement for a top level folder called WatchKitSupport with a Watch Kit Binary WK
. See this devfroums post for details.
- Payload
- MyApp.app
- WatchKitSupport
- WK
To do this you need to run the following few lines
# Extract the contents of the ipa
unzip MyApp.ipa
# Include WatchKitSupport
mkdir WatchKitSupport
cp "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/Library/Application\ Support/WatchKit/WK" WatchKitSupport/WK
# Repackage
zip -qr MyAppWithWatchKit.ipa Payload WatchKitSupport
That will create your new .ipa with WatchKitSupport
Streamlining the packaging
Now here is my contribution, the last step was a little too messy for my liking and was harder to integrate into our existing scripts and as such I wrote a simple packager class in python.
# https://github.com/mxpr/ios-build-utils
from utils.packager import Packager
def package():
ipa = Packager("MyAppWithWatchKit.ipa")
ipa.add("MyApp.app","Payload/MyApp.app")
ipa.add("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/Library/Application\ Support/WatchKit/WK", "WatchKitSupport/WK")
ipa.package()
Final Thoughts
I really do hope the command line tools get some more attention in upcoming releases as the tips and tricks only work around the issues and generally get outdated or invalid fairly soon.
In any case, hope this helps and happy building!