Nov 24, 2018

Distributing macOS apps via App Center

I’ve been a happy customer of the HockeyApp since their introduction back in 2012. Almost immediately, in 2014, bitstadium was acquired by Microsoft to become a part of the Visual Studio App Center. Fortunately, Microsoft did not shut down the service. With time, the HockeyApp got even better, and recently they announced a full transition to the App Center during 2019. Which is great news, because the App Center has improved analytics and crash reporting. In this post I will demonstrate these capabilities by releasing a new Mac app via the command line.

You will find solid documentation for getting started with the App Center SDK for macOS, but information about uploading local archives is iOS-specific and misses some important details. So this blog post should be useful for Mac developers in the first place.

We will start by creating a simple Mac app. Then we will integrate the App Center SDK into our Xcode project. Next we will build a binary archive and upload it to the App Center. In the end we will publish a download and play with App Center Analytics and Diagnostics, by submitting a crash report from the Mac app.

The sample project is built with Xcode 10.1 on macOS Mojave 10.14.1 and these instructions will get outdated eventually. Please let me know if something breaks or does not work in the future.

Create a new Mac app in Xcode

In this tutorial I will work with a simple Mac app. This app will have just one button. And this button will crash the app.

An Xcode dialog when you create a new Mac app for this tutorial

Options for the NewMacApp in Xcode

  1. Launch Xcode or open its Welcome screen by pressing Shift-Command-1.
  2. Click the button Create a new Xcode project in the Welcome window.
  3. Choose the template Cocoa App in the macOS section and click Next.
  4. Fill the details about our new project, I will call mine the NewMacApp.
  5. Make sure that you have selected a Team in the popup, and click Next again.
  6. Choose an appropriate location for the project folder and click Create.
Location dialog when you create a new Xcode project

Location for the new Xcode project

Once Xcode launches a new workspace, press Command-R or choose ProductRun to build and launch our NewMacApp.

A new Mac app in Xcode 10.1 built and run from the Cocoa template

The NewMacApp created from the Xcode template

Now quit the app and Xcode, we will open it later, when CocoaPods will have built a separate workspace.

Add an App Center SDK using CocoaPods

In order to use the App Center, we should add their SDK into our Mac app. I prefer Carthage but Microsoft’s repo is hard-coded for CocoaPods.

So let’s open the Terminal.app and go to our Xcode project. In the root folder, we create a Podfile to download the official App Center SDK from the GitHub:

$ sudo gem install cocoapods
$ cd ~/Desktop/NewMacApp
$ touch Podfile && open Podfile

Paste the following lines into our Podfile:

platform :osx, '10.14'
workspace 'NewMacApp.xcworkspace'
use_frameworks!
target 'NewMacApp' do
 pod 'AppCenter'
 pod 'AppCenter/Analytics'
 pod 'AppCenter/Crashes'
end

To fetch dependencies and create a new Xcode workspace named NewMacApp.xcworkspace, we should run these commands:

$ pod install
$ echo -e "\nPods\n" >> .gitignore
$ git add . && git commit -m "Add App Center SDK"
$ open NewMacApp.xcworkspace

We have just built-in all necessary App Center SDK frameworks. And now they can be used in the Mac app. So open the AppDelegate.swift in Xcode and change it to something like this:

import AppKit
import AppCenter
import AppCenterAnalytics
import AppCenterCrashes

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
  func applicationDidFinishLaunching(_ notification: Notification) {
    // TBD
  }
}

Our app has been added to the new workspace. But it doesn’t have code signing set up. Also we should add a sandbox exception necessary for sending crash reports.

Enable the development signing in Xcode General project settings

Development signing already enabled for the NewMacApp

  1. Press Command-1 or choose ViewNavigatorsShow Project Navigator in the main menu.
  2. Select the root project NewMacApp in the Sidebar.
  3. Go to the General tab for the target NewMacApp.
  4. Press the button Enable Development Signing
  5. Choose your Team.
  6. Go to the Capabilities tab for the target NewMacApp.
  7. Expand a section App Sandbox.
  8. Select the checkbox Outgoing Connections.
Enable the outgoing connections checkbox required by the App Center SDK in the Xcode Capabilities panel

Outgoing Connections in the App Sandbox Capabilities panel

Why the App Center SDK needs write access to the Keychain?

If you build and run the app from Xcode now, it will ask for a permission to write some data into the login Keychain. I contacted App Center Support, and they recognized this is a bug, which should be fixed soon. Until then just allow access and type your password. The App Center SDK creates a keychain password item named like app.nativeconnect.NewMacApp.AppCenter. It’s just a token, an encoded private string. And when you remove it, the app should never ask for writing permissions again, even after Xcode project cleanup.

Create a new app in the App Center

An application using the App Center SDK needs only a so called API Key. Unlike the HockeyApp, there is no need to register a bundle identifier. You can just create an account at appcenter.ms and add a new Mac app. Very conveniently, you can use your HockeyApp credentials for the App Center login.

A dialog for creating a new Mac app in the App Center

Add the NewMacApp to the App Center

  1. Go to your account in the App Center.
  2. Click the button Add new app in the top right corner.
  3. Type the App name e.g. NewMacApp.
  4. Select macOS in the OS radio group.
  5. Press the button Add new app in the bottom.
  6. Find and copy the API Key for the NewMacApp from the Overview screen.
An API key for the Mac app in the App Center

An API Key for the NewMacApp

So now we have the API Key ready for using in the Xcode project.

Use an API Key from the App Center in our Swift code

We are not going to commit the API Key into the git repo however. Instead, we will keep it in the root file named Secrets.xcconfig and ignored by git. Please understand, modify appropriately and execute the following lines in the Terminal:

$ echo APP_CENTER = f1a43f78-d123-2b6f-b321-913cf1944bab > Secrets.xcconfig
$ echo -e "\nSecrets.xcconfig\n" >> .gitignore
$ echo -e "\n#include? \"../../../Secrets.xcconfig\"" >> Pods/Target\ Support\ Files/Pods-NewMacApp/Pods-NewMacApp.release.xcconfig
$ echo -e "\nGCC_PREPROCESSOR_DEFINITIONS = \$(inherited) COCOAPODS=1 APP_CENTER=\$(APP_CENTER)" >> Pods/Target\ Support\ Files/Pods-NewMacApp/Pods-NewMacApp.release.xcconfig

After this script, the API Key will be defined as APP_CENTER. This symbol is available in the Release mode, but only in Objective-C. To make it accessible from Swift, we will use the following trick:

  1. Press Command-1 or choose ViewNavigatorsShow Project Navigator in the main menu.
  2. Expand and select the folder NewMacAppNewMacApp in the Sidebar.
  3. Press Command-N or choose FileNewFile in the main menu.
  4. Choose a template named Objective-C File in the macOS panel and click Next.
  5. Give a File the name Secrets, make sure that File Type is Empty File.
  6. Click Next, make sure that the target NewMacApp is selected and click Create.
  7. You will see the alert Would you like to configure an Objective-C bridging header?
  8. Click the button Don’t Create, because our Secrets.m is exceptional, and because a bridging header is evil.

Paste the following code into the new Secrets.m. It creates an Objective-C class with a short API +[Secrets appCenter]. And this API is accessible by Swift from the Objective-C runtime:

#define STRINGIZE(macro) #macro
#define STRINGIFY(macro) STRINGIZE(macro)

#ifdef APP_CENTER
#import <Foundation/Foundation.h>

@interface Secrets: NSObject @end
@implementation Secrets
+ (NSString *)appCenter {
  #define AppCenter @ STRINGIFY(APP_CENTER)
  return AppCenter;
}
@end

#endif

Now we can replace AppDelegate.applicationDidFinishLaunching(_:) by something like this:

let appCenterSelector = NSSelectorFromString("appCenter")
guard
  let secrets = NSClassFromString("Secrets") as AnyObject as? NSObjectProtocol,
  secrets.responds(to: appCenterSelector),
  let appCenter = secrets.perform(appCenterSelector)?.takeRetainedValue() as? String,
  appCenter.count > 0
else {
  return
}
MSAppCenter.start(appCenter, withServices: [
  MSAnalytics.self,
  MSCrashes.self,
])

We are done with the API Key, the MSAppCenter will start its services only in Release mode and only if the APP_CENTER symbol is available!

Feel free to commit the latest changes using a command like git commit -am "Use App Center API key" in Terminal.

Add crashing code to the Mac app

In order to test the crash reporting feature, let’s add the following action to the AppDelegate.swift:

@IBAction
func crash(_ sender: Any?) {
  MSCrashes.generateTestCrash()
}

Then we need to trigger it e.g. by adding a button to the main window.

A button Crash in the Mac app main window

A button Crash in the main window

  1. Open the file MainMenu.xib in Xcode and select Window in the list of top-level objects.
  2. Press Shift-Command-L or select ViewLibrariesShow Library in the main menu.
  3. Drag & drop a button from the library panel into the main window and name it Crash.
  4. Add missing Auto Layout constraints to remove any warnings.
  5. Control-Click the button Crash to show the list of its actions.
  6. Connect the action item to the top-level Delegate’s action crash.
  7. Save the changes and close the MainMenu.xib.
Connecting a Crash button to the crash(_:) action

Connecting the button Crash to the AppDelegate

Now if you launch the NewMacApp and press the button Crash, the Xcode console would show you a crash, but the app would keep running in Debug mode.

And we are done with the Mac app. Feel free to commit the changes using a command like git commit -am "Add Crash button".

Build an Xcode archive from the command line

We are not going to notarize or rebuild the NewMacApp using a Developer ID, sorry. Instead, let’s go the short path and make an unsafe yet signed binary, still suitable for uploading to the App Center:

$ echo -e "\n*.zip\n" >> .gitignore && git commit -am "Ignore ZIP archives"
$ xcodebuild -workspace NewMacApp.xcworkspace -scheme NewMacApp -configuration Release -archivePath NewMacApp archive
$ cd NewMacApp.xcarchive/Products/Applications/ && zip -r NewMacApp.app.zip NewMacApp.app && cd ../../..
$ mv NewMacApp.xcarchive/Products/Applications/NewMacApp.app.zip ./
$ cd NewMacApp.xcarchive/dSYMs && zip -r NewMacApp.app.dSYM.zip NewMacApp.app.dSYM && cd ../..
$ mv NewMacApp.xcarchive/dSYMs/NewMacApp.app.dSYM.zip ./
$ rm -rf NewMacApp.xcarchive

Now we have 2 archives in the root folder: NewMacApp.app.zip and NewMacApp.app.dSYM.zip. And they are ready for uploading to the App Center.

Upload binaries to the App Center from the command line

The good old HockeyApp provided a CLI named puck. This utility allowed to upload Xcode archives to the Hockey web service from Terminal. The App Center built something similar. They provide an advanced command line tool named appcenter-cli. However, at the moment of writing, this utility can be used only for token generation. For the rest of the job we should use a public API.

$ npm install -g appcenter-cli && brew install jq
$ appcenter login
$ appcenter apps list
$ appcenter tokens create -d "Token for NewMacApp"

Please notice a symbolic app name having a suffix NewMacApp in the output, for example vadim.shpakovski/NewMacApp. There is also your token string, for example fa93920b0044fc15f657526e3f494798d59fa36c. We will use these parameters for direct API requests, because the appcenter-cli is still in development.

An official App Center documentation suggests curl to upload the app and its associated dSYM, both exported from the Xcode archive as ZIP files. We should use the symbolic app name and the API token copied before to make a POST request. So modify the following command using your own credentials and run it in Terminal:

$ curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' --header 'X-API-Token: fa93920b0044fc15f657526e3f494798d59fa36c' 'https://api.appcenter.ms/v0.1/apps/vadim.shpakovski/NewMacApp/release_uploads' | jq

In case of success, the App Center will return a temporary URL and an ID for uploading a single binary for our NewMacApp:

{
  "upload_id": "cd032271-d0b9-0136-1028-0afc7492f81e",
  "upload_url": "https://rink.hockeyapp.net/api/sonoma/apps/13df3731-ebf2-86f3-afc4-d394614b3231/app_versions/upload?upload_id=cd032271-d0b9-0136-1028-0afc7492f81e"
}

Now we can use the given URL for an upload request, also secured by the API token provided before. Uploading of the dSYM and the app archives is done separately.

Let’s start with symbols:

$ curl -F 'ipa=@NewMacApp.app.dSYM.zip' "https://rink.hockeyapp.net/api/sonoma/apps/13df3731-ebf2-86f3-afc4-d394614b3231/app_versions/upload?upload_id=cd032271-d0b9-0136-1028-0afc7492f81e"

Next, let’s upload the application binary. In order to do this, we should create another upload, using the same URL as before:

$ curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' --header 'X-API-Token: fa93920b0044fc15f657526e3f494798d59fa36c' 'https://api.appcenter.ms/v0.1/apps/vadim.shpakovski/NewMacApp/release_uploads' | jq

Please notice different ID and URL parameters in the returned JSON:

{
  "upload_id": "238ffb40-d0bc-0106-c2fb-0add454b23e6",
  "upload_url": "https://rink.hockeyapp.net/api/sonoma/apps/12df3731-ebf2-42f3-afc4-d394614b3230/app_versions/upload?upload_id=238ffb40-d0bc-0106-c2fb-0add454b23e6"
}

Uploading of the app is similar to dSYM. We just need to provide another filename and the new URL:

$ curl -F 'ipa=@NewMacApp.app.zip' "https://rink.hockeyapp.net/api/sonoma/apps/12df3731-ebf2-42f3-afc4-d394614b3230/app_versions/upload?upload_id=238ffb40-d0bc-0106-c2fb-0add454b23e6"

Next, we are committing artefacts using the upload ID containing our Mac app:

$ curl -X PATCH --header 'Content-Type: application/json' --header 'Accept: application/json' --header 'X-API-Token: fa93920b0044fc15f657526e3f494798d59fa36c' -d '{ "status": "committed"  }' 'https://api.appcenter.ms/v0.1/apps/vadim.shpakovski/NewMacApp/release_uploads/238ffb40-d0bc-0106-c2fb-0add454b23e6' | jq

The following JSON reply is a pre-last piece of our curl puzzle:

{
  "release_id": "1",
  "release_url": "v0.1/apps/vadim.shpakovski/NewMacApp/releases/1"
}

Publish a Mac app from the command line

Now, if you go to the App Center and check the screen NewMacAppDistributeReleases, you will find out that it’s still empty. In order to make a release available for downloading, we will use a release_url retrieved in the previous section.

And this is one more API request. For needs of this tutorial, as we are testing the Mac app privately, the standard distribution group named Collaborators fits well:

curl -X PATCH --header 'Content-Type: application/json' --header 'Accept: application/json' --header 'X-API-Token: fa93920b0044fc15f657526e3f494798d59fa36c' -d '{ "destination_name": "Collaborators", "release_notes": "NewMacApp released via command line" }' 'https://api.appcenter.ms/v0.1/apps/vadim.shpakovski/NewMacApp/releases/1'

If this command is successful, go to the App Center again and refresh the screen NewMacAppDistributeReleases. Something like this should pop up:

A Mac app published from the command line

Mac app released from the command line

Moreover, a beautiful notification email should arrive into your Inbox:

An email from the App Center about just published Mac app released from the command line

An email from the App Center

Let’s check what’s behind the Install button:

A public page for downloading a new build of the Mac App in the App Center

A public Mac app page in the App Center

Looks neat. Click the Download button? Congratulations, eventually you should have the NewMacApp in the ~/Downloads folder.

Post-downloading page for the Mac app in the App Center

This is a Mac, on the Microsoft website

It’s about time to catch and submit our first crash report.

Submit crash reports to the App Center

As we did not provision the binary with a Developer ID, the NewMacApp cannot be launched by double-click out of the box. So please try the following:

  1. Open Finder and go to the Downloads folder.
  2. Control-click the NewMacApp to show its contextual menu.
  3. Press and hold an Option key, then click the Open item in the top.
  4. After a tiny delay, you will see a system alert, where you click the button Open.

After our app is up and running, but before you click the button Crash, go to the App Center and open a panel NewMacAppAnalyticsOverview. You should see the first launch in a chart:

App Center panel with Analytics Overview for the Mac app

Analytics Overview in the App Center

Finally, let’s generate a crash report. Just click the button Crash and relaunch the NewMacApp. Under the hood, the App Center SDK will send all the details about a crash.

Now open a screen NewMacAppDiagnostics in the App Center and wait for a minute or so, then refresh. Isn’t it nice?

A crash report page for a Mac app in the App Center

Crash report page in the App Center

Conclusion

In this post we built and published a completely new Mac app to the App Center. We started with a fresh Cocoa project and installed the App Center SDK using CocoaPods. Then we imported an API Key into Swift code and added a button to crash the app. Finally, we built an archive and uploaded it to the App Center, all from the command line. Which means that distribution can be automated for Xcode bots, Jenkins etc.

Microsoft has built a fantastic replacement for the HockeyApp. Not just a cool and modern UI, it seems like the service itself got much better. Let’s hope that next year we will get all promised features including ‘Sparkle support for macOS beta distribution’. Thanks for reading!