Deploy a .NET Desktop App on macOS
Developing an application and running it from an IDE (Integrated Development Environment) is one thing; distributing it to users is a whole other level of complexity.
This post approaches this complexity by exploring one specific case, divided into manageable steps.
The starting point is a "Hello World"-like .NET 8 desktop application. The end point is the same application packaged as a macOS app with an icon and a database.
Notably, this post does not cover the app signing process nor distributing the app beyond one's own computer.
The source code is available on GitHub here.
Preliminaries
A modern framework choice for developing desktop applications in .NET is AvaloniaUI.
The templates can be installed with the following command:
dotnet new install Avalonia.Templates
The templates' availability can be checked with the dotnet new list --author Avalonia
command, producing output such as:
Template Name Short Name Language Tags
----------------------------------- ------------------------- -------- -----------------------------------------
Avalonia .NET App avalonia.app [C#],F# Desktop/Xaml/Avalonia/Windows/Linux/macOS
Avalonia .NET MVVM App avalonia.mvvm [C#],F# Desktop/Xaml/Avalonia/Windows/Linux/macOS
Avalonia Cross Platform Application avalonia.xplat [C#],F# Desktop/Xaml/Avalonia/Browser/Mobile
Avalonia Resource Dictionary avalonia.resource Desktop/Xaml/Avalonia/Windows/Linux/macOS
Avalonia Styles avalonia.styles Desktop/Xaml/Avalonia/Windows/Linux/macOS
Avalonia TemplatedControl avalonia.templatedcontrol [C#],F# Desktop/Xaml/Avalonia/Windows/Linux/macOS
Avalonia UserControl avalonia.usercontrol [C#],F# Desktop/Xaml/Avalonia/Windows/Linux/macOS
Avalonia Window avalonia.window [C#],F# Desktop/Xaml/Avalonia/Windows/Linux/macOS
With the templates in place, the project can be created by running the following commands, which create a solution, a project, and add the project to the solution:
dotnet new sln --name MacosDeployDemo --output macos-deploy-demo
cd macos-deploy-demo
dotnet new avalonia.mvvm --name MacosDeployDemo.UI
dotnet sln add MacosDeployDemo.UI
This commit shows the result.
Running the app from an IDE results in this window:
Warm Up
The .NET CLI command to prepare an application for deployment is dotnet publish
, with documentation here.
The output can be found at ./MacosDeployDemo.UI/bin/Release/net8.0/publish/
.
The app can be launched using the ./MacosDeployDemo.UI/bin/Release/net8.0/publish/MacosDeployDemo.UI
command. This step ensures the application operates as intended when executed independently of the IDE.
Customised Publish
The publish command for later use can be slightly customised as follows: dotnet publish --runtime osx-arm64 --configuration Release --self-contained
--runtime osx-arm64
targets macOS with an ARM processor, also referred to as Apple M series, Apple Silicon, or AArch64. More information on runtime identifiers can be found here.--configuration Release
builds in Release mode for optimised performance. This is the default behaviour for .NET version 8 and above but is included here for clarity.--self-contained
packages the runtime, making it executable on target systems without additional dependencies.
Icon
An icon visually represents the application and should have the .icns
extension.
An image editor such as Canva can be used to create an icon. Canva also provides icon templates with the required 1024x1024 dimensions and allows .png
file downloads.
A tool like CloudConvert can be used to convert this .png
to an .icns
file.
This commit adds the prepared icon to the assets folder.
Bundling
MacOS applications have the extension .app
. This is actually a directory rather than a single file.
Using Finder, its contents can be examined by right-clicking and selecting "Show Package Contents":
Here is the structure of the .app
directory:
MyApp.app
|
--Contents\
|
--MacOS\ (the output of "dotnet publish")
| |
| --MyApp
--Resources\
| |
| --MyApp.icns (icon file)
--Info.plist (application metadata)
Creating such a directory is called bundling.
When bundling the app for the first time, it is a good exercise to do all the steps manually: create directories, copy the icon, copy the executables, and prepare the Info.plist
file. Since this is tedious, this commit automates the process with a shell script.
Everything is ready: running the script with the sh deploy.sh
command and double-clicking HelloWorld.app
launches the app.
Updates are supported too: modifying the greeting (this commit), re-running the script, and launching the app results in a new greeting:
Data Persistence
Thus far, the app is stateless, meaning it does not save data across sessions. This commit introduces data persistence using EF-Core and SQLite. It does nothing more than creating a database when the app starts.
The app works when launched from the IDE, but nothing happens on double-clicking HelloWorld.app
.
The problem lies with relative paths. When debugging, the data.db
file is created in the ./MacosDeployDemo.UI/bin/Debug/net8.0/
folder, but when launching the bundled app, it does not work well with relative paths.
The convention is to save data to a special folder /Users/Username/Library/Application Support/AppName
. As a consequence, all copies of the app, wherever they reside, share this data. Moreover, deleting and reinstalling the app does not affect the data.
This commit fixes the problem by setting the location of the database file to this special folder. More information about special folders support in .NET can be found here.
The app now runs successfully. The app data folder within the application support folder contains the data.db
file. The app can be copied to the applications folder:
Next Steps
There are at least two pathways from here. The first is to apply the deployment process to apps beyond this demo app. The second is to understand the app distribution process beyond one's own computer.