I have been trying to build SaxonCS for .NET such that I could deliver it on MacOS without warning messages for a long time. It has not been an easy or enjoyable adventure. Here are some breadcrumbs for the next poor soul forced to tread down this path.
You can’t do this with .NET 5. That’s probably less important today than it was when I started. I don’t understand the details, but something has been fixed in .NET 6 that isn’t going to be backported to .NET 5. (There’s a comment to that effect in an issue, but I can’t now locate that issue.)
There are several problems that have to be solved. The application has to be built such that it will run when signed. All of the various pieces have to be (correctly) signed. A DMG must be constructed to distribute the application (maybe I don’t have to do this step, but it’s reasonably what users expect). The DMG has to be signed. And the whole thing has to be notarized by Apple so that it will open without warnings.
The objective
A complete, hands off, CI-driven build of a C# application to produce a MacOS DMG file that a user can open and use without any warnings about unsigned code or potentially malicious applications.
Prerequisites
Before you begin, there are some things you have to have setup.
You need dotnet 6 (or later, I assume, but we’re planning to ship Saxon 12 with .NET 6 so that’s what I’m using).
You need the nuget command line tool. This is distinct from the nuget subcommand of the dotnet tool. As far as I can tell, only the former can actually install packages.
On a Mac, you need the Mono framework and some other fiddling to make it work. Because that’s the way it is. The details are outlined on the tools page linked above.
You’ll need XCode. I’m using version 14.2. I think you need to install the XCode command line tools and you need to run XCode at least once, it does a bunch of initialization the first time it runs.
You need an Apple developer account and you have to go through the dance necessary to create a developer ID certificate. The certificate and (at least some of) the certificate chain need to be downloaded and installed in your keychain. I don’t remember the exact details, but I seem to recall that it was spelled out reasonably well in the Apple developer documentation.
I decided to use DMG Canvas to build the DMG. In addition to building the DMG, this application does the sign and notarize dance with Apple for me. $20 well spent, I think. I assume these steps can be done manually, but I’m not inspired to try to figure out how just at the moment.
Use the nuget command to install Dotnet.Bundle
:
$ nuget install Dotnet.Bundle
This package constructs the “bundle” of files that MacOS expects
for an application. (That’s the application.app
directory
and its descendants.)
Application files
Start with your application. In our case, this complex beast:
using System;
namespace HelloWorld
{
public class HelloWorld
{
public static void Main(string[] arg)
{
Console.WriteLine("Hello, World!");
}
}
}
You will also need a .csproj
file. Here’s one that works
for me:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
<UseHardenedRuntime>true</UseHardenedRuntime>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<IncludeSymbolsInSingleFile>false</IncludeSymbolsInSingleFile>
</PropertyGroup>
<PropertyGroup>
<CFBundleName>HelloWorld</CFBundleName>
<CFBundleDisplayName>HelloWorld</CFBundleDisplayName>
<CFBundleIdentifier>com.saxonica.helloworld</CFBundleIdentifier>
<CFBundleVersion>1.0.0</CFBundleVersion>
<CFBundleShortVersionString>1.0.0</CFBundleShortVersionString>
<CFBundleExecutable>HelloWorld</CFBundleExecutable>
<CFBundleIconFile>HelloWorld.icns</CFBundleIconFile>
<NSPrincipalClass>NSApplication</NSPrincipalClass>
<NSHighResolutionCapable>true</NSHighResolutionCapable>
<NSRequiresAquaSystemAppearance>false</NSRequiresAquaSystemAppearance>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
</ItemGroup>
</Project>
The first property group specifies properties of the build, the second defines how the application will be bundled, and the last item group is necessary to make the bundler part of the build.
Notes:
- You must specify
net6
for the framework and create a single file, self-contained application. - You must use the hardened runtime.
- You must include native libraries for self extraction.
- You must not include symbols in the single file, that’s an option that apparently stopped working in .NET 5.
- I created
HelloWorld.icns
from a PNG with ImageMagick.
Building the application
Add Dotnet.Bundle
to the project:
$ dotnet add package Dotnet.Bundle
(You only have to do this once.)
Build the application:
$ dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:Configuration=Release
You can run the bundled application to make sure it works:
$ bin/Release/net6.0/osx-x64/publish/HelloWorld.app/Contents/MacOS/HelloWorld
Hello, World!
Sign the application
Next we have to sign the application. But before we can do that, we have
to make an entitlements plist file. I called mine entitlements.plist
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
</dict>
</plist>
Now we can sign it:
$ codesign --force --options runtime --entitlements ./entitlements.plist --deep \
--sign "Developer ID Application: YOUR DEVELOPER ID GOES HERE" \
--timestamp bin/Release/net6.0/osx-x64/publish/HelloWorld.app
You must use the entitlements option and the timestamp option. (You probably need all the other options too, but those were the ones that I initially overlooked.)
You can run it again to make sure it still works:
$ bin/Release/net6.0/osx-x64/publish/HelloWorld.app/Contents/MacOS/HelloWorld
Hello, World!
(It didn’t for me for the longest time!)
Construct the DMG
Fire up the DMG Canvas application. (Yes, I know, I said I wanted this to be a hands-off process. I believe DMG Canvas can be automated, but I haven’t tried to figure out exactly how yet.)
The first time you open it up, go to the Preferences dialog and add your Apple ID and a one-time password on the Notarization tab:
This will enable signing and notarizing the DMG later.
On the main screen, add the application to the canvas. On the right hand side, choose the second tab and select “Code Sign and Notarize” in the drop down. You’ll have to specify the certificate you want to use, your Apple ID, and the primary bundle ID. (I have no idea what that means in this context, but you have to put something in there.)
Click the “Build” button in the upper right corner, fill in the details,
Hit Save and wait (nervously, and for quite a while) for the results!
With luck, it all goes smoothly and you get back a signed, notarized DMG file.
There’s obviously more to be done in the DMG: it needs a background image, the
standard symlink to /Applications
should be present, etc. But
I got a working DMG file out of it so, I’m
declaring victory for the moment.