Xcode Project Setup to Build, Embed, Codesign, Notarize, and Use the Sparkle XPC Services
In the previous post, I talked about how to download the “modern” Sparkle updater branch called ui-separation-and-xpc
that allows you to update sandboxed apps and migrate your code to use the new types.
This time, we’re going to use the actual XPC services that do the grunt work.
Remember: Sparkle does not require the XPC services to work. If you embed them, they are used. This is necessary for Sandboxed apps. All the other kinds of app can skip using the XPC services if they want.
This is the third post in a series:
- How to migrate to the new Sparkle XPC branch
- How to toggle use of XPC service (Spoiler: you don’t have to do anything but bundle the XPC services in your app)
- How to build, embed, and notarize the XPC services in your app (this post)
Prepare App Sandbox Entitlements
I assume your app is sandboxed. Otherwise, you could still use the traditional Sparkle updater version.
Your app will download the update feed from the web. Make sure you add the com.apple.security.network.client
entitlement. Add this manually or head to the “Capabilities” tab of your app target, expand the “App Sandbox” settings, then enable “Outgoing Connections (Client)”.
Alternatively, you can use the SparkleDownloader
XPC service (its filename is org.sparkle-project.Downloader.xpc
). But I didn’t test this, because I need the networking entitlement to display the About.html
file in my app anyway for some reason. When we embed the XPC services in the next steps, keep in mind to include this extra service.
Build the XPC services
When you build your app with the new Sparkle framework, the XPC services will not build automatically.
Let’s have a look at TableFlip’s $(BUILT_PRODUCTS_DIR)
, where you will find a flat list of all build products for the app project:
$ l /Users/ctm/Library/Developer/Xcode/DerivedData/TableFlip-fggcerpwydmlxkbphymfxiredpny/Build/Products/Debug
total 1032
-rwxr-xr-x 1 ctm staff 441K Jun 21 09:57 Autoupdate
drwxr-xr-x 7 ctm staff 224B Jun 21 09:57 Export.framework
drwxr-xr-x 7 ctm staff 224B Jun 21 09:57 FastSpringStore.framework
drwxr-xr-x 7 ctm staff 224B Jun 21 09:57 LatexExport.framework
drwxr-xr-x 7 ctm staff 224B Jun 21 09:57 Sparkle.framework
drwxr-xr-x 7 ctm staff 224B Jun 21 09:57 State.framework
drwxr-xr-x 4 ctm staff 128B Jun 21 09:57 StateTests.swiftmodule
drwxr-xr-x 3 ctm staff 96B Jun 21 09:57 TableFlip.app
drwxr-xr-x 4 ctm staff 128B Jun 21 14:21 TableFlip.swiftmodule
drwxr-xr-x 3 ctm staff 96B Jun 21 09:57 TableFlipHelp.help
drwxr-xr-x 4 ctm staff 128B Jun 21 09:57 TableFlipTests.swiftmodule
drwxr-xr-x 7 ctm staff 224B Jun 21 09:57 TableModel.framework
drwxr-xr-x 3 ctm staff 96B Jun 21 09:57 Updater.app
drwxr-xr-x 3 ctm staff 96B Jun 21 09:57 include
-rw-r--r-- 1 ctm staff 70K Jun 21 09:57 libbsdiff.a
Next to all the TableFlip-related stuff, it contains Sparkle.framework
and its dependencies Updater.app
and Autoupdate
. It does not include the XPC services.
That’s because only Sparkle.framework
is a “Target Dependency” of the app; and if you look at the build phases of the Sparkle.framework
itself, the “Autoupdate” and “Installer Progress” targets of the Sparkle project are in turn its dependencies.
The current version of the install instructions assumes you have built Sparkle from the command line using make release
. That would build the XPC services for you, too. If I cannot use a dependency manager like Carthage, I prefer to build the framework and XPC services as part of the app build process; the previous post prepared the project for just that, and that’s why the XPC services haven’t been built, yet.
The fix is simple: add more target dependencies to build the XPC services.
Heads up for the lazy ones: Do not just use the “Distribution” target. Like the make
command, it builds the framework, the services, and all example apps. When you try to export your own app afterwards from Xcode, you will end up with a generic Xcode archive, but not the usual archive that you can export as an app. That’s because Xcode will try to bundle your own app and the Sparkle test app together, and that doesn’t work out, except with a generic archive that’s of no use.
- Show your app project settings
- Select the app target
- Select the “Build Phases” tab
- Expand “Target Dependencies”
Remove(Update 2020-03-24: This is not necessary and I do in fact use it later on and in my apps. Thanks to Helge Heß for pointing this out!)Sparkle.framework
from the dependencies list- Add (“+”) new dependencies and select the XPC services from the “Sparkle” sub-project: do include
org.sparkle-project.InstallerConnection.xpc
,org.sparkle-project.InstallerLauncher.xpc
,org.sparkle-project.InstallerStatus.xpc
, and considerorg.sparkle-project.Downloader.xpc
if you need it.
Build your app, then check the directory again:
$ l /Users/ctm/Library/Developer/Xcode/DerivedData/TableFlip-fggcerpwydmlxkbphymfxiredpny/Build/Products/Debug
-rwxr-xr-x 1 ctm staff 441K Jun 22 11:29 Autoupdate
drwxr-xr-x 7 ctm staff 224B Jun 22 11:29 Export.framework
drwxr-xr-x 7 ctm staff 224B Jun 22 11:29 FastSpringStore.framework
drwxr-xr-x 7 ctm staff 224B Jun 22 11:29 LatexExport.framework
drwxr-xr-x 7 ctm staff 224B Jun 22 11:29 Sparkle.framework
drwxr-xr-x 7 ctm staff 224B Jun 22 11:29 State.framework
drwxr-xr-x 3 ctm staff 96B Jun 22 11:30 TableFlip.app
drwxr-xr-x 4 ctm staff 128B Jun 22 11:29 TableFlip.swiftmodule
drwxr-xr-x 3 ctm staff 96B Jun 22 11:29 TableFlipHelp.help
drwxr-xr-x 7 ctm staff 224B Jun 22 11:29 TableModel.framework
drwxr-xr-x 3 ctm staff 96B Jun 22 11:29 Updater.app
drwxr-xr-x 3 ctm staff 96B Jun 22 11:29 include
-rw-r--r-- 1 ctm staff 70K Jun 22 11:29 libbsdiff.a
drwxr-xr-x 3 ctm staff 96B Jun 22 12:29 org.sparkle-project.Downloader.xpc
drwxr-xr-x 3 ctm staff 96B Jun 22 11:29 org.sparkle-project.InstallerConnection.xpc
drwxr-xr-x 3 ctm staff 96B Jun 22 11:29 org.sparkle-project.InstallerLauncher.xpc
drwxr-xr-x 3 ctm staff 96B Jun 22 11:29 org.sparkle-project.InstallerStatus.xpc
This means it worked!
Embed the XPC Services
As for embedding, the Sparkle.framework
is already embedded in your app (we did that in the previous post); the XPC services aren’t, though.
Still in your app’s build phases, add another build phase and select “New Copy Files Phase” from the dropdown.
- Rename the new build phase to “Embed XPC Services”
- Select “XPC Services” as its destination
- Add files (“+”) and select the services with “Installer” in their name:
org.sparkle-project.InstallerConnection.xpc
,org.sparkle-project.InstallerLauncher.xpc
,org.sparkle-project.InstallerStatus.xpc
If you want to embed the SparkleDownloader as well, this is the place and time to do so.
Codesigning and Hardened Runtimes
Update 2021-11-18: Check out the v2.x Sandboxing docs: You usually don’t need to manually sign anything anymore. So that’s good. This won’t work with Carthage builds, though, but it works fine if you use SwiftPM or CocoaPods out of the box.
Sparkle comes with a script that does code-signing and runtime hardening for you. It is located at bin/codesign_embedded_executable
inside the Sparkle framework project directory.
For reference, these are the original instructions, assuming you sign the build products once and then simply embed them in your app:
./bin/codesign_embedded_executable "Developer ID Application: XXX" XPCServices/*.xpc
./bin/codesign_embedded_executable "Developer ID Application: XXX" ./Sparkle.framework/Versions/A/Resources/Autoupdate
./bin/codesign_embedded_executable "Developer ID Application: XXX" ./Sparkle.framework/Versions/A/Resources/Updater.app/
As announced at the beginning, here’s my take on the instructions to embed and sign everything as part of your build:
- Figure out the script location
- Automate the signing identity selection
- Figure out the framework and XPC bundle paths
1. Location of the script
Assuming that you put Sparkle in Extern/Sparkle/
relative to your project directory, then the script will be located at
$(PROJECT_DIR)/Extern/Sparkle/bin/codesign_embedded_executable
2. Codesign identity
In practice, this part worked great for me since hardening the runtime of my apps:
IDENTITY="${CODE_SIGN_IDENTITY}"
if [ "$IDENTITY" == "" ]
then
# If a code signing identity is not specified, use ad hoc signing
IDENTITY="-"
fi
codesign --verbose --force --deep -o runtime --sign "$IDENTITY" <TARGET_BUNDLE>
We’ll be using this approach to getting to the IDENTITY
variable.
3. Paths of the framework and XPC services
You can either code-sign and harden the runtimes before the bundles are embedded in your app, or afterwards. I prefer to sign after the bundles are embedded because other scripts on the web did the same, but it doesn’t seem to matter.
When you want to sign before embedding, use $(BUILT_PRODUCTS_DIR)/Sparkle.framework/
and $(BUILT_PRODUCTS_DIR)/*.xpc
. The base location of build products, including your app, is $(BUILT_PRODUCTS_DIR)
. We looked at the contents of this before. Xcode assembles the app from the parts in here.
When you want to sign after embedding, you can find the framework and XPC service inside your app bundle. For example, CoolApp.app/Contents/Frameworks/Sparkle.framework
exists, and CoolApp.app/Contents/XPCServices/
contains the XPC services. You can use the following variable path combinations: $(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Sparkle.framework/
and $(BUILT_PRODUCTS_DIR)/$(XPCSERVICES_FOLDER_PATH)/*.xpc
. For reference, in case of TableFlip XPCSERVICES_FOLDER_PATH
is TableFlip.app/Contents/XPCServices
.
With the paths figured out, let’s add the script!
- Go to your app target’s settings if you aren’t there anymore
- Select the “Build Phases” tab again
- Add (“+”) a build phase and select “New Run Script Phase” this time
- Rename the build phase to “Code-sign Sparkle” or similar
- Drag and drop this build phase after both the “Embed XPC Services” build phase we created above, and the default “Embed Frameworks” build phase
Then paste the following into the script text view; we’ll be signing after the stuff is embedded, remember:
# Shorthand for the script
alias dosign="${PROJECT_DIR}/Extern/Sparkle/bin/codesign_embedded_executable"
# Code Signing identity
IDENTITY="${CODE_SIGN_IDENTITY}"
if [ "$IDENTITY" == "" ]
then
# If a code signing identity is not specified, use ad hoc signing
IDENTITY="-"
fi
# Shorthand for the Sparkle.framework
SPARKLE_PATH=${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework
dosign "$IDENTITY" "${BUILT_PRODUCTS_DIR}/${XPCSERVICES_FOLDER_PATH}"/org.sparkle-project.*.xpc
dosign "$IDENTITY" "${SPARKLE_PATH}/Versions/A/Resources/Autoupdate"
dosign "$IDENTITY" "${SPARKLE_PATH}/Versions/A/Resources/Updater.app/"
dosign "$IDENTITY" "${SPARKLE_PATH}"
I found I needed to add a re-signing step for the Sparkle framework itself since I sign after the framework was embedded and already signed during that step, but then its contents were modified by the script. You will probably not need the last line if you signed before embedding the framework and services.
Building and testing
Build your app. There should be no errors.
When you build your app in release mode for distribution using the “Build > Archive” command and export the result, verify that the hardening of runtimes and the signing of bundles worked as expected:
$ spctl --assess -vv TableFlip.app
/path/to/TableFlip.app: accepted
source=Developer ID
origin=Developer ID Application: Christian Tietze (FRMDA3XRGC)
That’s good.
Note that spctl
will reject your debug-mode build products, so you never know until you export the final app.
Also note that you can verify signing all you like: if an embedded (!) helper app or XPC service is not signed or notarized properly, only a test run on a clean system (or a Guest user on your own computer) will reveal problems when you download the file from the internet. Gatekeeper will not show some notarization errors for files that weren’t downloaded from the web.
This bit me before, so please make sure to test the download for a Guest user before you make it public.
So far, the updates seem to work in release mode. I have to archive the app first; when I run it from Xcode in Debug mode, the feed will not be downloaded and no update dialog presented. That’s a bummer. It’s most likely an error on my side: other people had great success with testing updates when they run their app from Xcode.
I will have to test if running make release
outside of Xcode solves the problem.