macOS Deployment
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:
MyProgram.app
|
----Contents\
|
------_CodeSignature\ (stores code signing information)
| |
| ------CodeResources
|
------MacOS\ (all your DLL files, etc. -- the output of `dotnet publish`)
| |
| ---MyProgram
| |
| ---MyProgram.dll
| |
| ---Avalonia.dll
|
------Resources\
| |
| -----MyProgramIcon.icns (icon file)
|
------Info.plist (stores information on your bundle identifier, version, etc.)
------embedded.provisionprofile (file with signing information)
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
CFBundleExecutablematches the binary name generated bydotnet publish-- typically this is the same as your.dllassembly name without.dll. CFBundleNameis set to the display name for your application. If this is longer than 15 characters, setCFBundleDisplayNametoo.CFBundleIconFileis set to the name of youricnsicon file (including extension)CFBundleIdentifieris set to a unique identifier, typically in reverse-DNS format -- e.g.com.myapp.macos.NSHighResolutionCapableis set to true (<true/>in theInfo.plist).CFBundleVersionis set to the version for your bundle, e.g. 1.4.2.CFBundleShortVersionStringis 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:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>AppName</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLSchemes</key>
<array>
<string>i8-AppName</string>
</array>
</dict>
</array>
Example file association
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Sketch</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>sketch</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>icon.icns</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
</array>
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:
<RuntimeIdentifiers>osx-x64</RuntimeIdentifiers>
Add other runtime identifiers as necessary. Each one should be separated by a semicolon (;).
Notes on creating icon files
This type of icon file can not only be created on Apple devices, but it is also possible on Linux devices.
You can find more information about how you can achieve that in this blog post:
Creating Mac OS X Icons (icns) on Linux
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 (executable) 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
.csprojfile:
<PropertyGroup>
<UseAppHost>true</UseAppHost>
</PropertyGroup>
- Add
-p:UseAppHost=trueto yourdotnet publishcommand.
dotnet-bundle
dotnet-bundle is unmaintained but should still work.
It is recommended that you target net6-macos, which will handle package generation.
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:
<PackageReference Include="Dotnet.Bundle" Version="*" />
After that, you can create your .app by executing the following on the command line:
dotnet restore -r osx-x64
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:UseAppHost=true
You can specify other parameters for the dotnet msbuild command. For instance, if you want to publish in release mode:
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -property:Configuration=Release -p:UseAppHost=true
or if you want to specify a different app name:
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:CFBundleDisplayName=MyBestThingEver -p:UseAppHost=true
Instead of specifying CFBundleDisplayName, etc., on the command line, you can also specify them in your project file:
<PropertyGroup>
<CFBundleName>AppName</CFBundleName> <!-- Also defines .app file name -->
<CFBundleDisplayName>MyBestThingEver</CFBundleDisplayName>
<CFBundleIdentifier>com.example</CFBundleIdentifier>
<CFBundleVersion>1.0.0</CFBundleVersion>
<CFBundlePackageType>APPL</CFBundlePackageType>
<CFBundleSignature>????</CFBundleSignature>
<CFBundleExecutable>AppName</CFBundleExecutable>
<CFBundleIconFile>AppName.icns</CFBundleIconFile> <!-- Will be copied from output directory -->
<NSPrincipalClass>NSApplication</NSPrincipalClass>
<NSHighResolutionCapable>true</NSHighResolutionCapable>
</PropertyGroup>
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):
dotnet publish -r osx-x64 --configuration Release -p:UseAppHost=true
Create your Info.plist file, adding or modifying keys as necessary:
<?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>CFBundleIconFile</key>
<string>myicon-logo.icns</string>
<key>CFBundleIdentifier</key>
<string>com.identifier</string>
<key>CFBundleName</key>
<string>MyApp</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
<key>CFBundleExecutable</key>
<string>MyApp.Avalonia</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
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):
#!/bin/bash
APP_NAME="/path/to/your/output/MyApp.app"
PUBLISH_OUTPUT_DIRECTORY="/path/to/your/publish/output/netcoreapp3.1/osx-64/publish/."
# PUBLISH_OUTPUT_DIRECTORY should point to the output directory of your dotnet publish command.
# One example is /path/to/your/csproj/bin/Release/netcoreapp3.1/osx-x64/publish/.
# If you want to change output directories, add `--output /my/directory/path` to your `dotnet publish` command.
INFO_PLIST="/path/to/your/Info.plist"
ICON_FILE="/path/to/your/myapp-logo.icns"
if [ -d "$APP_NAME" ]
then
rm -rf "$APP_NAME"
fi
mkdir "$APP_NAME"
mkdir "$APP_NAME/Contents"
mkdir "$APP_NAME/Contents/MacOS"
mkdir "$APP_NAME/Contents/Resources"
cp "$INFO_PLIST" "$APP_NAME/Contents/Info.plist"
cp "$ICON_FILE" "$APP_NAME/Contents/Resources/$ICON_FILE"
cp -a "$PUBLISH_OUTPUT_DIRECTORY" "$APP_NAME/Contents/MacOS"