I am currently working on a desktop application that automatically generates accounting records from invoices (if you are interested, see https://mrcarson.de). As the postings are transferred to the accounting software via an API Monkey Office which is only available as a desktop application, my application must also be implemented in part as a desktop app. And since I work on a Mac, it has to be a macOS application.
With .NET Core and Avalonia, it is easy to create an application that can run on both Windows and macOS. So far so good. In the IDE, in my case JetBrains Rider, everything works. But as soon as I try to start the application as a macOS App Bundle, it crashes. The macOS App Bundle does not start.
macOS App Bundle
Applications on the Mac are deployed as so-called app bundles. Basically, these are directories that must have a predefined structure. The extension .app may indicate a file, but they are actually directories. In the Mac Finder, it is not easy to switch to such directories. If you double-click on a directory that ends with the extension .app, macOS tries to start the directory as an application. To switch to the directory, right-click or Ctrl-click in the Finder to open the context menu of the app bundle. There you will find the menu item "Show package contents" which will take you to the .app directory.
An app bundle directory has the following structure:
- The directory Contents in which the app is stored.
- The file plist contains all the important information about the app, in particular which file should be executed at startup.
- In the directory Contents/MacOS is the executable program.
- In the directory Contents/Resources there is a file with the icons in various sizes, e.g. icns.
There are other directories and files, but I will limit myself to the minimum here.
Create App Bundle
To create an app bundle, the .NET Core application must first be created with the command dotnet publishbe translated. The processor architecture must be taken into account. This can be either Intel (osx-x64) or Arm (osx-arm64). An example call looks like this:
dotnet publish ../ -c Release -r osx-arm64 -p:PublishReadyToRun=false -p:TieredCompilation=false -p:PublishTrimmed=false -p:PublishSingleFile=false --self-contained -o "../publish-macos-arm64" -p:UseAppHost=true -nowarn:CCD0001
Subsequently, in the specified output directory, here publish-macos-arm64, the finished translated application. The application can be started in this directory by double-clicking on the executable file.
Within the application, the current directory (Directory.GetCurrentDirectory()) then the directory in which the executable file is located, in this case publish-macos-arm64. The same applies to the current directory when running within the IDE. Usually, the application is compiled in the directory bin/Debug/net9.0 resp. bin/Release/net9.0 and started from there. The current directory is then this directory. This detail will become relevant later. But first let's take a look at how an app bundle can be created.
To do this, it is sufficient to create the Info.plist file and copy the program into the App Bundle directory structure. I do this using a bash script:
#!/bin/bash
APP_NAME="../mrcarson-macos-arm64.app"
PUBLISH_OUTPUT_DIRECTORY="../publish-macos-arm64/."
INFO_PLIST="Info.plist"
ICON_FILE="icon.icns"
create_app() {
echo "[INFO] Creating macOS app bundle: $1 from $2"
if [ -d "$1" ]
then
rm -rf "$1"
fi
mkdir "$1"
mkdir "$1/Contents"
mkdir "$1/Contents/MacOS"
mkdir "$1/Contents/Resources"
cp "$INFO_PLIST" "$1/Contents/Info.plist"
cp "$ICON_FILE" "$1/Contents/Resources/icon.icns"
cp -a "$2" "$1/Contents/MacOS"
}
create_app "$APP_NAME" "$PUBLISH_OUTPUT_DIRECTORY"
After the script has been executed, the following directory exists in the example mrcarson-macos-arm64.app. The application can be started by double-clicking. Almost.
Firstly, the App Bundle still needs to be signed. This requires an Apple developer account. I will also leave this detail out here. Secondly, there is another tiny detail that prevents the app from starting.
macOS App Bundle does not start
Applications often use the file system to store the program settings or to write a log file, for example. In my case, I have stored the log files in the directory logs is stored. If the application is running in the IDE, the following directory is created below bin/Debug/net9.0 logs and the log files are written. Additional files can also be created without any problems if only the file name is specified. The files are then saved in the current directory bin/Debug/net9.0 created.
It took me almost two days to figure out why my app won't launch when I try to launch the App Bundle:
An app bundle sets the current directory to the root directory "/".
All attempts to create a file or directory there naturally fail because the authorizations are not sufficient.
Solution
The solution is to set the current directory first when starting the application. In my example, I do this as follows:
var userDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MrCarson");
if (!Directory.Exists(userDataPath)) {
Directory.CreateDirectory(userDataPath);
}
Directory.SetCurrentDirectory(userDataPath);
The special directory Environment.SpecialFolder.ApplicationData for macOS is the directory /Users//Library/Application Support/. In this directory, I create a directory for my application if it does not already exist, in this case "MrCarson". The current directory for the process is then set to this directory. The application then also runs as an app bundle.
Conclusion
A macOS application that is started via an App Bundle has its current directory in the root directory "/". It remains to be seen who came up with this nonsensical default. The fact is that an application must consciously decide where to store files instead of assuming that the current directory corresponds to the directory from which the application was started.