This is the macOS portion of my complete guide to Qt desktop releases found here(coming soon).
macOS is usually the most hands-on experience when releasing an app. The Mac operating system has some features unique to it, and you will need to take them into consideration.
Note: During this part you will see firstname.lastname@example.org used in the place of an AppleID email. Replace that with your own AppleID email to follow this guide.
Step 1 – Release
Start by building a release version of your application. This will spit out a macOS .app file somewhere in your project, depending on how you have set it up. By default, shadow-build will place the .app file in a folder next to your main project folder.
Inside the shadow-build folder should be an .app with your application’s name. This is a special folder macOS can execute. You can browse the structure by right-clicking it and clicking “Show package contents” if you are interested in how Mac apps are structured. I will explain this in more detail in an upcoming blog post.
Notice how the .app is tiny, this is because it doesn’t yet contain its dependencies and if we were to distribute it as-is to a client that does not have Qt installed, it would fail to run.
Step 2 – macdeployqt
The next step is to tell Qt to bundle all the libraries needed for our app to run. To do this, we open a terminal and cd to our shadow-build folder. Once there, we will execute the command “macdeployqt quicktest.app”. If you don’t have macdeployqt in your PATH, you can execute it directly from your Qt install directory.
If you are having issues getting your app to build or run and are using qml, try specifying –qmldir to your qml root as explained here.Troubleshooting
Now that macdeployqt has done its magic your .app should be a fair bit larger.
We now have an .app that can run directly on end-users machines. Hooray! One small problem, if we try to run it on another user’s machine, the following error will appear: ‘”quicktest” cannot be opened because the developer cannot be verified.’
All this means is we are not done yet.
Step 3 – Signing
The next step is to create an Apple Developer Account if you do not already have one. This requires a 99$/year fee.
You can read more about how to obtain a signing certificate here. Once you have the certificate on your local machine, you can sign your application. I do this using the “codesign” utility built into XCode. Newer versions of macdeployqt can handle signing, but since the oldest current LTS 5.12 at time of writing does not, this guide will cover how to do it manually.
First, we need to find the name of our certificate. Open Keychain Access and find your certificate.
Important Note about signing and macdeployqt
This tutorial will use the –deep command to sign our .app but this is officially discouraged by Apple. I have not had any bad experiences personally with “codesign –deep” even in quite intricate multi-binary Qt apps as they tend to be more uniformly structured.
If you are using a newer version of Qt you may have “macdeployqt -sign-for-notarization” available, which you can check for by running macdeployqt –help. It’s not in 5.12.10 so I won’t be using it for this tutorial.
Warning: The -codesign option by itself is insufficient as it doesn’t include a secure timestamp and runtime hardening. Without this you app will be rejected.
This is better practice than invoking codesign with –deep if you have a newer version of Qt:
macdeployqt quicktest.app -sign-for-notarization="Developer ID Application: YOUR_NAME_HERE"
The codesign command
codesign --deep --timestamp --options runtime -s "Developer ID Application: YOUR_NAME_HERE" quicktest.app
That’s quite a bit of gibberish, so let’s break it down:
- –deep Recursively signs all libraries and dependencies found in the .app folder.
- –timestamp Includes a secure timestamp with the signature. This requires an internet connection.
- –options runtime Enables Runtime Hardening, which is required for a later step, notarization
- -s “Developer ID Application: YOUR_NAME_HERE” The certificate name
If your app has entitlements now is the time to sign them in:
codesign --options runtime -s "Developer ID Application: Zan Vidrih (329CGDDU5U)" -f --timestamp --entitlements entitlements.plist quicktest.app
Let’s summarize the new parameters:
- -f force, replaces the previous top-level signature with the new one that contains the entitlements
- –entitlements the path to your entitlements file
Step 4 – Installer Creation
macOS apps distributed outside the Mac App Store are typically packed in .dmg files, the equivalent of an .iso on Windows. They most often contain two kinds of installers:
.dmg containing a .pkg
This format is less commonly used because of the added work of creating your own installer. Although if you have some sort of install-time logic that you must perform (license key validation, component selection, initial parameter configuration) this format is mandatory. This is the macOS equivalent of a .msi file on Windows.
WARNING: .pkg files MUST also be signed, but with a different kind of certificate, the “Developer ID Installer”, obtained via the same process as before.
The command for signing a .pkg is:
productsign --sign "Developer ID Installer: YOUR_NAME_HERE" unsigned.pkg signed.pkg
.dmg containing an .app
This format is very popular owing to its ease of use. It’s a very simple format that simply consists of a .dmg file containing an .app file. Usually, the .dmg also contains a symbolic link to the users ~/Applications directory, seen in the image on the right. This allows a user to drag the application to the Application folder, thus “installing” it.
What I chose in this guide
If you would like to follow along, I am going to create a very basic .app in a .dmg . No fancy styling here, we just want to check everything works.
First up, lets create an out directory in our shadow-build folder and copy our signed .app inside.
mkdir out cp -R quicktest.app out
Next, create a symlink to the system Applications folder
ln -s /Applications out
Finally, create our image
hdiutil create -volname "my-quick-test" -srcfolder out -ov -format UDZO quicktest.dmg
Let’s break that down:
- -volname “my-quick-test” The string the user will see in the title of the window that will appear
- -srcfolder out The folder used to create the image
- -ov Flag that tells hdiutil to overwrite any existing .dmg file with that same name
- -format UZDO The format of the image, UDZO means zlib-compressed image.
Now, let’s double click our .dmg to see if it worked.
After you choose
Your project requirements will influence your decision between these two options. Using an installer creator like Packages will mean your experience will be radically different from someone using macdeployqt’s -dmg parameter to generate a simple app-based .dmg. If you would like me to cover those in more detail, please let me know and I will write a follow-up article.
Regardless of what you choose, you will end up with a .dmg file. This dmg file must also be signed using the same command as earlier.
codesign --options runtime --timestamp -s "Developer ID Application: YOUR_NAME_HERE" quicktest.dmg
Now I know what you’re thinking, “Are we done? Can we now distribute this .dmg to our customers?”. Well, let’s take a look at what happens if we try to distribute it now:
The message ‘”quicktest” can’t be opened because Apple cannot check it for malicious software.’ appears! This is an indicator that our app is not notarized.
Step 5 – Notarization
Notarization is a new requirement for macOS applications. Basically, it means you upload your installer to Apple, they run some static analysis on it to verify its malware-free and then give you a certificate which you must staple to your installer. This process can take hours, but usually only takes a couple of minutes.
First we setup an App-Specific password. This is safer than just using your AppleID password because it can be revoked. Additionally, Apple rejects requests without App-Specific passwords.
Next we should setup your Keychain to store your new App-Specific password. Open the login keychain in Keychain Access, click on Passwords and add a new entry. Call it APPLE_DEV_PW, or something you will remember for the next step.
Now, we can notarize our .dmg . Simply plug in the values we have so far:
xcrun altool --notarize-app --primary-bundle-id "com.enatik.quicktest" --username "email@example.com" --password "@keychain:APPLE_DEV_PW" --file quicktest.dmg
If it all went to plan, this should appear in the terminal:
No errors uploading 'quicktest.dmg'. RequestUUID = XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
The requestUUID is what we are going to use to track the notarization progress.
You can get the status of the notarization using:
xcrun altool --notarization-info "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" --username "firstname.lastname@example.org" --password "@keychain:APPLE_DEV_PW"
Which will return something like this while it is still processing:
No errors getting notarization info. Date: 2021-04-05 18:39:19 +0000 RequestUUID: b5ca2dc9-1a05-4f19-8e5c-ca166d34bb0a Status: in progress
Or something like this when it’s done.
No errors getting notarization info. Date: 2021-04-05 18:28:20 +0000 Hash: a0010f5b87365fba8f45c00e976a6dacf02e55d1f78dc7db6f9b66ee42cbd533 LogFileURL: https://osxapps-ssl.itunes.apple.com/itunes-assets/... RequestUUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX Status: success Status Code: 0 Status Message: Package Approved
The automation of this system is left as an exercise for the reader 🙂
After notarization we need to staple the certificate to the .dmg
xcrun stapler staple quicktest.dmg
This is the final step, if this command is successful your .dmg is ready to be shipped to customers. Example of successful output:
Processing: ~/Downloads/build-quicktest-Desktop_Qt_5_12_10_clang_64bit-Release/quicktest.dmg Processing: ~/Downloads/build-quicktest-Desktop_Qt_5_12_10_clang_64bit-Release/quicktest.dmg The staple and validate action worked!
And you’re done! Congratulations, you successfully signed and notarized a macOS app.