While my previous blog post show you how to create a proper installer for windows reliably, let’s see how to do it for Mac OS.

Peculiarities to Mac OS

There is several issues with Mac OS. The main problem lie in the fact that the majority of tutorials rely on XCode.

However XCode is very broken tool which change at every release with new bugs. Not only this, but you don’t know to have to do manual steps to distribute your app. You want the process to be integrated through CI. A simple git tag should be enough to create a new release.

Distributing for Mac has been quite frustrating, but now it is done and scripted, it works reliably and I hope you will just use what I did.

The other peculiarities: You can’t do everything via Docker. At one point, you will need to use Mac tools, which is very time consuming to debug. If you don’t have a Mac, you have to configure your travis, delete/create tag/force push a lot to test it.

In any case, you need a mac for two things:

Generating .DS_Store, and .fseventsd for your project. (more on that later) Getting an apple Developer ID. This requires a “trusted device” to complete. Note that the trusted device can be a phone, or the mac of a friend.

You will need a developer certificate which cost 100 USD per year. Thanks god, the process is not complicated. (More on that later)

Preparing the dmg

Here is the folder for mac deployment that I created:

├───osx-x64

│ │ Dockerfile

│ │ entitlements.plist

│ │ Info.plist

│ │

│ └───Metadata

│ │ .DS_Store

│ │

│ ├───.background

│ │ Logo_with_text_small.png

│ │

│ └───.fseventsd

│ 00000000009a0cb3

│ 00000000009a0cb4

│ fseventsd-uuid

The Dockerfile will create a tar file which have the same structure as the dmg file we will create later.

entitlements.plist is a list of right you want mac to grant to your app. Take note of the rights, they are necessary to run a .NET Core app.

Info.plist is other kind of metadata specific to your app.

The dockerfile will replace some information (like version of your app taken from your csproj) in those files before creating the dmg during the docker build process.

The Metadata folder is here to provide a nice and familiar drag and drop install to Mac users when they double click on your app.

Logo_with_text_small.png is just drawing the arrow and the B below it.

How to create .DS_Store and .fseventsd

The metadata folder is very tricky to generate, you have to do it only once, here has been my process. You need a Mac, any attempt to automate it will fails. (I spent lot’s of time on it)

Get a Mac Install XCode From the tar file generated by the dockerfile, extract .VolumeIcon.icns and .background/Logo_with_text_small.png Run the following code

brew install create-dmg

mkdir temp create-dmg --volname "BTCPayServer Vault" --volicon .VolumeIcon.icns --background Logo_with_text_small.png --window-pos 200 120 --window-size 600 440 --app-drop-link 500 150 --icon "BTCPayServer Vault" 110 150 --hdiutil-verbose "temp.dmg" temp

5. Mount the created temp.dmg and copy .background, .fseventsd and .DS_Store, and put that in the Metadata folder and commit that to your repo so you don’t have to repeat the manual steps.

Note that this it is impossible to use create-dmg in shell script, because we can’t run Apple scripts without a UI since Mac OS 10.13.

Preparing the dmg content via dockerfile

I build our app via a dockerfile so everybody can reproduce and debug easily.

The process is not so different from what we did for windows.

Here is the structure of the generated tar file.

C:\USERS\NICOLASDORIER\TESTTEST\BTCPAYSERVER VAULT

│ .DS_Store

│ .VolumeIcon.icns

│ Applications

│

├───.background

│ Logo_with_text_small.png

│

├───.fseventsd

│ 00000000009a0cb3

│ 00000000009a0cb4

│ fseventsd-uuid

│

└───BTCPayServer Vault.app

└───Contents

│ entitlements.plist

│ Info.plist

│

├───MacOS

│ Avalonia.Animation.dll

│ .......................

│ System.Threading.Channels.dll

│ System.Threading.dll

│ System.Threading.Tasks.Parallel.dll

│ System.Windows.Extensions.dll

│ Tmds.DBus.dll

│

├───Resources

│ BTCPayServerVault.icns

The file .VolumeIcon.icns and BTCPayServerVault.icns are the same.

They are generated thanks to imagemagick, in a way similar to what I did to generate the .ico file on windows.

You can see this here.

The Info.plist file is edited to file out the application name and version.

Creating, signing and notarizing a dmg file

Our Dockerfile is creating a tar file with the right structure. But Apple users installs apps via dmg files.

While creating a dmg file can be done in the Dockerfile with some tools, we need Mac OS to sign and notarize our app.

Luckily, most CI services allow you to use any OS you want as part of your release process. This is painful to test, but it works.

I will talk in a later blog post of the travis configuration to tie all the releases together, but let’s see the whole process of signing and notarizing via a script!

Creating a Application Developer certificate

This step has to be done only once, here is the process to do it on linux/wsl.

You need an apple trusted device, because enrolling to apple developer program require two factor authentication, (SMS is not enough) with a trusted device confirmation. Once you enrolled to the apple developer program, you don’t need it anymore. So you can ask a friend to help you for this, his device does not need to be registered with your Apple ID. Enroll in the apple developer program (100USD per year). Get the apple certificate in a .p12 file (this is what the rest of this page is about)

Go on apple developer program website, create a Developer ID Application certificate. It will ask you to upload a certificate request file. (CSR)

Fill in your own parameters here:

email="nicolas.dorier@gmail.com"

common_name="Nicolas Dorier"

country_code="JP"

Run

rsa_key_file="temp.key"

csr_file_name="request.csr"

openssl req -new -key "$rsa_key_file" -out "$csr_file_name" -subj "/emailAddress=$email, CN=$common_name, C=$country_code"

This will create a request file as request.csr in your current folder.

Upload the csr back to apple, this will give you back a .cer file.

Save this file as developerID_application.cer in the current folder.

Now you need to export this in the .p12 format, this will bundle the .cer and the private key together in the same file.

cer_file="developerID_application.cer"

pem_file="developerID_application.pem"

cert_output_file="developerID_application.p12"

openssl x509 -in "$cer_file" -inform DER -out "$pem_file" -outform PEM

openssl pkcs12 -export -inkey "$rsa_key_file" -in "$pem_file" -out "$cert_output_file"

Now enter a password, don’t pick an empty one as the rest would fail.

# Cleanup what we don't need anymore

rm $csr_file_name $cer_file $pem_file $rsa_key_file

Now you should have a file called developerID_application.p12. This is your certificate that you can easily upload where you need to sign binaries.

For travis to sign BTCPayServer.Vault dmg file, you need to convert the certificate to base64 with this command line:

cat $cert_output_file | base64 -w0

Then setup your travis environment variable APPLE_DEV_ID_CERT to this value SURROUNDED BY DOUBLE QUOTE ("").

Additionally setup the travis environment variable APPLE_DEV_ID_CERT_PASSWORD , SURROUNDED BY DOUBLE QUOTE ("").

We will come back in the travis parts in later blog posts.

Signing the app

This is achieve by our script applesign.sh.

We will not go through all the script but here are the main points

This will take our base64 encoded certificate and save it in a file.

echo "$APPLE_DEV_ID_CERT" | base64 --decode > dev.p12

This will import it in a “keychain” (source)

key_chain="build.keychain"

key_chain_pass="mysecretpassword"

security create-keychain -p "$key_chain_pass" "$key_chain"

security default-keychain -s "$key_chain"

security unlock-keychain -p "$key_chain_pass" "$key_chain"

security import dev.p12 -k "$key_chain" -P "$APPLE_DEV_ID_CERT_PASSWORD" -A

CERT_IDENTITY=$(security find-identity -v -p codesigning "$key_chain" | head -1 | grep '"' | sed -e 's/[^"]*"//' -e 's/".*//')

echo "Signing with identity $CERT_IDENTITY"

security set-key-partition-list -S apple-tool:,apple: -s -k "$key_chain_pass" "$key_chain"

Then we can sign our app (source)

# codesign don't like that the entitlements file path have spaces, so we move it to local folder.

sudo cp "$app_path/Contents/entitlements.plist" "./"

echo "Signing $app_path..."code_sign_args="--deep --force --options runtime --timestamp --entitlements entitlements.plist"

sudo codesign $code_sign_args --sign "$CERT_IDENTITY" "$app_path"

Note that we are actually signing the “BTCPayServer Vault.app” folder.

Then we generate the dmg file (source)

dmg_file="BTCPayServerVault-${RUNTIME}-$version.dmg"

echo "Create $dmg_file with signature"

dmg_file_writable="$dmg_file.writable.dmg"

sudo hdiutil create "$dmg_file_writable" -ov -volname "$title" -fs HFS+ -srcfolder "$dmg_folder"

sudo hdiutil convert "$dmg_file_writable" -format UDZO -o "$dmg_file"

sudo rm -rf "$dmg_file_writable"

rm -rf "$dmg_folder"

We also need to sign the dmg itself

echo "Signing $dmg_file..."

sudo codesign $code_sign_args --sign "$CERT_IDENTITY" "$dmg_file"

echo "DMG signed"

Notarize the app

All would be nice if apple did not added another step for us to distribute our app.

It turns out that from the Mojave version of Mac OS, dmg files will be blocked by gatekeeper if you do not upload your app to apple before distributing. This is what they call “notarizing”.

You can see the code here, the trick is that notarization is “asynchronous”. After uploading the dmg file, you need to wait the process to succeed, which take between 1 minute and… unbounded. Luckily for me it always worked in less than 5 minutes.

Then finally, you need to staple you dmg file. A staple is not really required. A staple let the user’s mac know that your app has been notarized by apple without having to connect to internet.

Conclusion

Distributing your app with apple has been quite difficult and time consuming.

It requires you to have a mac, at least for setting things up the first time. Then you can rely on Travis to run the rest of your release automation.

We will see the travis setup in a later blog post.

Note that the user will still see a popup “Warning this application has been downloaded from the internet” when running your dmg file.

If you don’t sign and notarize your application, gatekeeper will plainly prevent the user from opening it, not showing a warning, but an error.

Note that .NET Core 3.1 is supporting only MacOS 10.13 and above. Your app will fail to open before that version.

A user on 10.12.6 (Sierra) reported to me that it is still possible to run your app by command line.