macOS
macOS applications are typically distributed in a .app application bundle. To make .NET Core and Avalonia projects work in a .app bundle, some extra legwork has to be done after your application has gone through the publishing process.
With Avalonia, you'll have a .app folder structure that looks like this:
1
MyProgram.app
2
|
3
----Contents\
4
|
5
------_CodeSignature\ (stores code signing information)
6
| |
7
| ------CodeResources
8
|
9
------MacOS\ (all your DLL files, etc. -- the output of `dotnet publish`)
10
| |
11
| ---MyProgram
12
| |
13
| ---MyProgram.dll
14
| |
15
| ---Avalonia.dll
16
|
17
------Resources\
18
| |
19
| -----MyProgramIcon.icns (icon file)
20
|
21
------Info.plist (stores information on your bundle identifier, version, etc.)
22
------embedded.provisionprofile (file with signing information)
Copied!
For more information on Info.plist, see Apple's documentation here.

Making the application bundle

There are a few options available for creating the .app file/folder structure. You can do this on any operating system, since a .app file is just a set of folders laid out in a specific format and the tooling isn't specific to one operating system. However, if you build on Windows outside of WSL, the executable may not have the right attributes for execution on macOS -- you may have to run chmod +x on the published binary output (the output generated by dotnet publish) from a Unix machine. This is the binary output that ends up in the folder MyApp.app/Contents/MacOS/, and the name should match CFBundleExecutable.
The .app structure relies on the Info.plist file being properly formatted and containing the right information. Use Xcode to edit Info.plist, it has auto-completion for all properties. Make sure that:
    The value of CFBundleExecutable matches the binary name generated by dotnet publish -- typically this is the same as your .dll assembly name without .dll.
    ​CFBundleName is set to the display name for your application. If this is longer than 15 characters, set CFBundleDisplayName too.
    ​CFBundleIconFile is set to the name of your icns icon file (including extension)
    ​CFBundleIdentifier is set to a unique identifier, typically in reverse-DNS format -- e.g. com.myapp.macos.
    ​NSHighResolutionCapable is set to true (<true/> in the Info.plist).
    ​CFBundleVersion is set to the version for your bundle, e.g. 1.4.2.
    ​CFBundleShortVersionString is set to the user-visible string for your application's version, e.g. Major.Minor.Patch.
If you need a protocol registration or file associations - open plist files from other apps in Applications folder and check out their fields.
Example protocol:
1
<key>CFBundleURLTypes</key>
2
<array>
3
<dict>
4
<key>CFBundleURLName</key>
5
<string>AppName</string>
6
<key>CFBundleTypeRole</key>
7
<string>Viewer</string>
8
<key>CFBundleURLSchemes</key>
9
<array>
10
<string>i8-AppName</string>
11
</array>
12
</dict>
13
</array>
Copied!
Example file association
1
<key>CFBundleDocumentTypes</key>
2
<array>
3
<dict>
4
<key>CFBundleTypeName</key>
5
<string>Sketch</string>
6
<key>CFBundleTypeExtensions</key>
7
<array>
8
<string>sketch</string>
9
</array>
10
<key>CFBundleTypeIconFile</key>
11
<string>icon.icns</string>
12
<key>CFBundleTypeRole</key>
13
<string>Viewer</string>
14
<key>LSHandlerRank</key>
15
<string>Default</string>
16
</dict>
17
</array>
Copied!
More documentation on possible Info.plist keys is available here.
If at any point the tooling gives you an error that your assets file doesn't have a target for osx-64, add the following runtime identifiers to the top <PropertyGroup> in your .csproj:
1
<RuntimeIdentifiers>osx-x64</RuntimeIdentifiers>
Copied!
Add other runtime identifiers as necessary. Each one should be separated by a semicolon (;).

Notes on the .app executable file

The file that is actually executed by macOS when starting your .app bundle will not have the standard .dll extension. If your publish folder contents, which go inside the .app bundle, do not have both a MyApp (exectuable) and a MyApp.dll, things are probably not generating properly, and macOS will probably not be able to start your .app properly.
​Some recent changes in the way .NET Core is distributed and notarized on macOS have caused the MyApp executable (also called the "app host" in the linked documentation) to not be generated. You need this file to be generated in order for your .app to function properly. To make sure this gets generated, do one of the following:
    Add the following to your .csproj file:
1
<PropertyGroup>
2
<UseAppHost>true</UseAppHost>
3
</PropertyGroup>
Copied!
    Add -p:UseAppHost=true to your dotnet publish command.

dotnet-bundle

​dotnet-bundle is a NuGet package that publishes your project and then creates the .app file for you.
You'll first have to add the project as a PackageReference in your project. Add it to your project via NuGet package manager or by adding the following line to your .csproj file:
1
<PackageReference Include="Dotnet.Bundle" Version="*" />
Copied!
After that, you can create your .app by executing the following on the command line:
1
dotnet restore -r osx-x64
2
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:UseAppHost=true
Copied!
You can specify other parameters for the dotnet msbuild command. For instance, if you want to publish in release mode:
1
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -property:Configuration=Release -p:UseAppHost=true
Copied!
or if you want to specify a different app name:
1
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:CFBundleDisplayName=MyBestThingEver -p:UseAppHost=true
Copied!
Instead of specifying CFBundleDisplayName, etc., on the command line, you can also specify them in your project file:
1
<PropertyGroup>
2
<CFBundleName>AppName</CFBundleName> <!-- Also defines .app file name -->
3
<CFBundleDisplayName>MyBestThingEver</CFBundleDisplayName>
4
<CFBundleIdentifier>com.example</CFBundleIdentifier>
5
<CFBundleVersion>1.0.0</CFBundleVersion>
6
<CFBundlePackageType>APPL</CFBundlePackageType>
7
<CFBundleSignature>????</CFBundleSignature>
8
<CFBundleExecutable>AppName</CFBundleExecutable>
9
<CFBundleIconFile>AppName.icns</CFBundleIconFile> <!-- Will be copied from output directory -->
10
<NSPrincipalClass>NSApplication</NSPrincipalClass>
11
<NSHighResolutionCapable>true</NSHighResolutionCapable>
12
</PropertyGroup>
Copied!
By default, dotnet-bundle will put the .app file in the same place as the publish output: [project directory]/bin/{Configuration}/netcoreapp3.1/osx-x64/publish/MyBestThingEver.app.
For more information on the parameters you can send, see the dotnet-bundle documentation.
If you created the .app on Windows, make sure to run chmod +x MyApp.app/Contents/MacOS/AppName from a Unix machine. Otherwise, the app will not start on macOS.

Manual

First, publish your application (dotnet publish documentation):
1
dotnet publish -r osx-x64 --configuration Release -p:UseAppHost=true
Copied!
Create your Info.plist file, adding or modifying keys as necessary:
1
<?xml version="1.0" encoding="UTF-8"?>
2
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
<plist version="1.0">
4
<dict>
5
<key>CFBundleIconFile</key>
6
<string>myicon-logo.icns</string>
7
<key>CFBundleIdentifier</key>
8
<string>com.identifier</string>
9
<key>CFBundleName</key>
10
<string>DotPurple</string>
11
<key>CFBundleVersion</key>
12
<string>1.0.0</string>
13
<key>LSMinimumSystemVersion</key>
14
<string>10.12</string>
15
<key>CFBundleExecutable</key>
16
<string>MyApp.Avalonia</string>
17
<key>CFBundleInfoDictionaryVersion</key>
18
<string>6.0</string>
19
<key>CFBundlePackageType</key>
20
<string>APPL</string>
21
<key>CFBundleShortVersionString</key>
22
<string>1.0</string>
23
<key>NSHighResolutionCapable</key>
24
<true/>
25
</dict>
26
</plist>
Copied!
You can then create your .app folder structure as outlined at the top of this page. If you want a script to do it for you, you can use something like this (macOS/Unix):
1
#!/bin/bash
2
APP_NAME="/path/to/your/output/MyApp.app"
3
PUBLISH_OUTPUT_DIRECTORY="/path/to/your/publish/output/netcoreapp3.1/osx-64/publish/."
4
# PUBLISH_OUTPUT_DIRECTORY should point to the output directory of your dotnet publish command.
5
# One example is /path/to/your/csproj/bin/Release/netcoreapp3.1/osx-x64/publish/.
6
# If you want to change output directories, add `--output /my/directory/path` to your `dotnet publish` command.
7
INFO_PLIST="/path/to/your/Info.plist"
8
ICON_FILE="/path/to/your/myapp-logo.icns"
9
​
10
if [ -d "$APP_NAME" ]
11
then
12
rm -rf "$APP_NAME"
13
fi
14
​
15
mkdir "$APP_NAME"
16
​
17
mkdir "$APP_NAME/Contents"
18
mkdir "$APP_NAME/Contents/MacOS"
19
mkdir "$APP_NAME/Contents/Resources"
20
​
21
cp "$INFO_PLIST" "$APP_NAME/Contents/Info.plist"
22
cp "$ICON_FILE" "$APP_NAME/Contents/Resources/$ICON_FILE"
23
cp -a "$PUBLISH_OUTPUT_DIRECTORY" "$APP_NAME/Contents/MacOS"
Copied!
If you created the .app on Windows, make sure to run chmod +x MyApp.app/Contents/MacOS/AppName from a Unix machine. Otherwise, the app will not start on macOS.

Signing Your App

Once you have your .app file created, you'll probably want to sign your app so that it can be notarized and distributed to your users without Gatekeeper giving you a hassle. Notarization is required for apps distributed outside the app store starting in macOS 10.15 (Catalina), and you'll have to enable hardened runtime and run codesign on your .app in order to notarize it successfully.
You'll need a Mac computer for this step, unfortunately, as we have to run the codesign command line tool that comes with Xcode.

Running codesign and enabling hardened runtime

Enabling hardened runtime is done in the same step as code signing. You have to codesign everything in the .app bundle under the Contents/MacOS folder, which is easiest to do with a script since there are a lot of files. In order to sign your files, you need an Apple developer account. In order to notarize your app, you'll need to do the following steps with a Developer ID certificate, which requires a paid Apple developer subscription.
You'll also need to have the Xcode command line tools installed. You can get those by installing Xcode and running it or by running xcode-select --install on the command line and following the prompts to install the tools
First, enable Hardened Runtime with exceptions by creating an MyAppEntitlements.entitlements file:
1
<?xml version="1.0" encoding="UTF-8"?>
2
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
<plist version="1.0">
4
<dict>
5
<key>com.apple.security.cs.allow-jit</key>
6
<true/>
7
<key>com.apple.security.automation.apple-events</key>
8
<true/>
9
</dict>
10
</plist>
Copied!
Then, run this script to do all the code signing for you:
1
#!/bin/bash
2
APP_NAME="/path/to/your/output/MyApp.app"
3
ENTITLEMENTS="/path/to/your/MyAppEntitlements.entitlements"
4
SIGNING_IDENTITY="Developer ID: MyCompanyName" # matches Keychain Access certificate name
5
​
6
find "$APP_NAME/Contents/MacOS/"|while read fname; do
7
if [[ -f $fname ]]; then
8
echo "[INFO] Signing $fname"
9
codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$fname"
10
fi
11
done
12
​
13
echo "[INFO] Signing app file"
14
​
15
codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$APP_NAME"
Copied!
The --options=runtime part of the codesign line is what enables the hardened runtime with your app. Because .NET Core may not be fully compatible with hardened runtime, we add some exceptions to use JIT-compiled code and allow for Apple Events to be sent. The JIT-compiled code exception is required to run Avalonia apps under hardened runtime. We add the second exception for Apple Events to fix an error that shows up in Console.app.
Note: Microsoft lists some other hardened runtime exceptions as being required for .NET Core. The only one that is actually needed to run an Avalonia app is com.apple.security.cs.allow-jit. The others may impose security risks with your application. Use with caution.
Once your app is code signed, you can verify that it signed properly by making sure that the following command outputs no errors:
1
codesign --verify --verbose /path/to/MyApp.app
Copied!

Notarizing your software

Notarization allows your app to be distributed outside the macOS App Store. You can read more about it here. If you run into any issues during the process, Apple has a helpful document of potential fixes here.
For more information on customizing your notarization workflow and more flags you may need to send when running xcrun altool, check out Apple's documentation.
The following steps were modified from this StackOverflow post:
    1.
    Make sure your .app is code signed properly
    2.
    Stick your .app in a .zip file, e.g. MyApp.zip.
    3.
    Run xcrun altool --notarize-app -f MyApp.zip --primary-bundle-id com.unique-identifier-for-this-upload -u username -p password. You can use a password in your keychain by passing -p "@keychain:AC_PASSWORD", where AC_PASSWORD is the key. The account has to be registered as an Apple Developer.
    4.
    If the upload is successful, you'll get a UUID back for your request token like this: 28fad4c5-68b3-4dbf-a0d4-fbde8e6a078f
    5.
    You can check notarization status using that token like this: xcrun altool --notarization-info 28fad4c5-68b3-4dbf-a0d4-fbde8e6a078f -u username -p password. This could take some time -- eventually it will succeed or fail.
    6.
    If it succeeds, you have to staple the notarization to the app: xcrun stapler staple MyApp.app. You can validate this by running xcrun stapler validate MyApp.app.
Once notarization is complete, you should be able to distribute your application!
If you distribute your app in a .dmg, you will want to modify the steps slightly:
    1.
    Notarize your .app as normal (in a .zip file)
    2.
    Add your notarized and stapled (xcrun stapler) app into the DMG (the DMG now has the notarized/stapled .app file inside of it).
    3.
    Notarize your .dmg file (same basic xcrun altool command, just with the .dmg file for the -f flag instead of the .zip)
    4.
    Staple the notarization to the .dmg file: xcrun stapler staple MyApp.dmg

App Store Packaging

You need a lot of things:

Getting certificates

    go to Xcode > Preferences > Account > Manage Certificates...
    Add them if they do not exists.
    Export them with a password.
    Open them and import into KeyChain Access.
    In KeyChain you should see this certificates 3rd Party Mac Developer Installer and Apple Distribution. If cert names are started with another strings - you've created a wrong certificate. Try again.
    Expand imported keys in KeyChain and double click on a private key inside.
    Go to Access Control Tab.
    Select Allow all applications to access this item in case you don't want to enter a mac profile password for every file sign.

Sandbox and entitlements

App Store required app to be launched inside a sandbox. That means app will have no access to everything and cannot harm user's PC.
Your app should be ready for this and do not crash if any folder is read/write protected.
You should read all (entitlements documentation)[https://developer.apple.com/documentation/bundleresources/entitlements] and peek ones your app will access.
First entitlements file is to sign all files inside .app/Content/MacOS/ folder. It should look like this.
1
<?xml version="1.0" encoding="UTF-8"?>
2
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
<plist version="1.0">
4
<dict>
5
<key>com.apple.security.app-sandbox</key>
6
<true/>
7
<key>com.apple.security.inherit</key>
8
<true/>
9
</dict>
10
</plist>
Copied!
Second entitlements file is to sign app package. Should contain all app's permissions. Here is an example:
1
<?xml version="1.0" encoding="UTF-8"?>
2
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
<plist version="1.0">
4
<dict>
5
<key>com.apple.security.cs.allow-jit</key>
6
<true/>
7
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
<true/>
9
<key>com.apple.security.cs.disable-library-validation</key>
10
<true/>
11
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
12
<true/>
13
<key>com.apple.security.app-sandbox</key>
14
<true/>
15
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
16
<array>
17
<string>com.apple.coreservices.launchservicesd</string>
18
</array>
19
</dict>
20
</plist>
Copied!
Also here is some optional parameters your app can need:
1
<key>com.apple.security.network.client</key>
2
<true/>
3
<key>com.apple.security.network.server</key>
4
<true/>
5
<key>com.apple.security.automation.apple-events</key>
6
<true/>
7
<key>com.apple.security.files.user-selected.read-write</key>
8
<true/>
9
<key>com.apple.security.files.bookmarks.document-scope</key>
10
<true/>
11
<key>com.apple.security.application-groups</key>
12
<array>
13
<string>[Your Team ID].[Your App ID]</string>
14
</array>
Copied!

Packaging script

Here is example packaging script with comments
1
#cleanup folders
2
rm -rf "App/AppName.app/Contents/MacOS/"
3
rm -rf "App/AppName.app/Contents/CodeResources"
4
rm -rf "App/AppName.app/Contents/_CodeSignature"
5
rm -rf "App/AppName.app/Contents/embedded.provisionprofile"
6
mkdir -p "App/AppName.app/Contents/Frameworks/"
7
mkdir -p "App/AppName.app/Contents/MacOS/"
8
​
9
#Build app
10
dotnet publish ../../ProjectFolder/AppName.csproj -c release -f net5.0 -r osx-x64 --self-contained true
11
​
12
#Move app
13
cd ..
14
cd ..
15
cp -R -f ProjectFolder/bin/release/net5.0/osx-x64/publish/* "build/osx/App/AppName.app/Contents/MacOS/"
16
cd "build/osx/"
17
​
18
APP_ENTITLEMENTS="AppEntitlements.entitlements"
19
FILE_ENTITLEMENTS="FileEntitlements.entitlements"
20
APP_SIGNING_IDENTITY="Apple Distribution: [***]"
21
INSTALLER_SIGNING_IDENTITY="3rd Party Mac Developer Installer: [***]"
22
APP_NAME="App/AppName.app"
23
​
24
echo "[INFO] Switch provisionprofile to AppStore"
25
\cp -R -f AppNameAppStore.provisionprofile "App/AppName.app/Contents/embedded.provisionprofile"
26
​
27
find "$APP_NAME/Contents/MacOS/"|while read fname; do
28
if [[ -f $fname ]]; then
29
echo "[INFO] Signing $fname"
30
codesign --force --timestamp --deep --options=runtime --entitlements "$FILE_ENTITLEMENTS" --sign "$APP_SIGNING_IDENTITY" "$fname"
31
fi
32
done
33
​
34
echo "[INFO] Signing app file"
35
codesign --force --timestamp --deep --options=runtime --entitlements "$APP_ENTITLEMENTS" --sign "$APP_SIGNING_IDENTITY" "$APP_NAME"
36
​
37
echo "[INFO] Creating AppName.pkg"
38
productbuild --component App/AppName.app /Applications --sign "$INSTALLER_SIGNING_IDENTITY" AppName.pkg
Copied!

Testing a package

Copy your .app into Applications folder and launch it. If it launches correctly - you did everything right. If it crashes - open Console app and check a crash report.

Uploading a package to app store

Open a Transporter app, sign in, select your *.pkg package and wait for validation and uploading to app store.
If you will receive any errors - fix them, package app again, remove file in Transporter and select it again.
When upload succedes - you will see your package in App Store Connect.
Last modified 25d ago