Ich arbeite derzeit an einer Desktop Anwendung, mit der Buchungssätze automatisch aus Rechnungen erzeugt werden (wen es interessiert, siehe https://mrcarson.de). Da die Buchungen über eine API in die Buchhaltungssoftware Monkey Office importiert werden, die nur als Desktop Anwendung zur Verfügung steht, muss auch meine Anwendung in Teilen als Desktop App implementiert werden. Und da ich auf dem Mac arbeite, muss es eine macOS Anwendung sein.
Mit .NET Core und Avalonia ist es problemlos möglich, eine Anwendung zu erstellen, die sowohl unter Windows als auch macOS ausgeführt werden kann. Soweit so gut. In der IDE, in meinem Fall JetBrains Rider, läuft alles. Doch sobald ich versuche, die Anwendung als macOS App Bundle zu starten, crasht sie. Das macOS App Bundle startet nicht.
macOS App Bundle
Anwendungen auf dem Mac werden als sogenannte App Bundles deployed. Im Grunde sind das Verzeichnisse, die eine vorgegebene Struktur haben müssen. Die Extension .app mag auf eine Datei hindeuten, doch es sind tatsächlich Verzeichnisse. Im Mac Finder kann man nicht ohne weiteres in solche Verzeichnisse hineinwechseln. Macht man einen Doppelklick auf ein Verzeichnis, das auf die Extension .app endet, versucht macOS das Verzeichnis als Anwendung zu starten. Um in das Verzeichnis zu wechseln, muss man im Finder mit der rechten Maustaste oder Ctrl-Klick das Kontextmenü des App Bundles öffnen. Dort gibt es den Menüpunkt „Paketinhalt zeigen“ mit dem in das .app Verzeichnis gewechselt wird.
Ein App Bundle Verzeichnis hat folgenden Aufbau:
- Es muss das Verzeichnis Contents enthalten, in dem die App abgelegt ist.
- Die Datei plist enthält alle wichtigen Angaben zur App, insbesondere, welche Datei beim Start ausgeführt werden soll.
- Im Verzeichnis Contents/MacOS liegt das ausführbare Programm.
- Im Verzeichnis Contents/Resources liegt eine Datei mit den Icons in diversen Größen, bspw. icns.
Es gibt noch weitere Verzeichnisse und Dateien, ich beschränke mich hier auf das Minimum.
App Bundle erstellen
Um ein App Bundle zu erstellen, muss die .NET Core Anwendung zunächst mit dem Befehl dotnet publishübersetzt werden. Dabei muss die Prozessorarchitektur beachtet werden. Diese kann entweder Intel (osx-x64) oder Arm (osx-arm64) sein. Ein beispielhafter Aufruf sieht wie folgt aus:
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
Anschließend liegt im angegebenen Ausgabe Verzeichnis, hier publish-macos-arm64, die fertig übersetzte Anwendung. Die Anwendung kann in diesem Verzeichnis mit einem Doppelklick auf die ausführbare Datei gestartet werden.
Innerhalb der Anwendung ist das aktuelle Verzeichnis (Directory.GetCurrentDirectory()) dann das Verzeichnis, in dem sich die ausführbare Datei befindet, in diesem Fall publish-macos-arm64. Für das aktuelle Verzeichnis gilt bei Ausführung innerhalb der IDE ähnliches. Üblicherweise wird die Anwendung beim Übersetzen im Verzeichnis bin/Debug/net9.0 bzw. bin/Release/net9.0 erzeugt und von dort gestartet. Aktuelles Verzeichnis ist dann eben dieses Verzeichnis. Später wird dieses Detail relevant. Doch schauen wir uns zunächst an, wie ein App Bundle erstellt werden kann.
Dazu ist es ausreichend, die Datei Info.plist zu erstellen und das Programm in die App Bundle Verzeichnisstruktur zu kopieren. Ich mache dies über ein Bash Skript:
#!/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"
Nachdem das Skript ausgeführt wurde, existiert im Beispiel das Verzeichnis mrcarson-macos-arm64.app. Per Doppelklick lässt sich die Anwendung starten. Fast..
Zum einen muss das App Bundle noch signiert werden. Dazu ist ein Apple Developer Account erforderlich. Auch dieses Detail lasse ich hier weg. Zum anderen gibt es noch ein klitzekleines Detail, das den Start der App verhindert.
macOS App Bundle startet nicht
Häufig verwenden Anwendungen das Dateisystem, um dort bspw. die Einstellungen des Programms abzulegen oder ein Logfile zu schreiben. In meinem Fall habe ich die Logfiles im Verzeichnis logs abgelegt. Läuft die Anwendung in der IDE, wird unterhalb von bin/Debug/net9.0 das Verzeichnis logs angelegt und die Logfiles werden geschrieben. Auch das Erstellen weiterer Dateien gelingt problemlos, wenn lediglich der Dateiname angegeben wird. Die Dateien werden dann im aktuellen Verzeichnis bin/Debug/net9.0 angelegt.
Es hat mich fast zwei Tage gekostet herauszufinden, warum meine Anwendung nicht startet, wenn ich versuche, das App Bundle zu starten:
Ein App Bundle setzt das aktuelle Verzeichnis auf das Wurzelverzeichnis „/“.
Alle Versuche, dort eine Datei oder ein Verzeichnis anzulegen scheitern natürlich, weil die Berechtigungen dazu nicht ausreichen.
Lösung
Die Lösung liegt darin, beim Start der Anwendung als erstes das aktuelle Verzeichnis zu setzen. In meinem Beispiel mache ich das wie folgt:
var userDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MrCarson");
if (!Directory.Exists(userDataPath)) {
Directory.CreateDirectory(userDataPath);
}
Directory.SetCurrentDirectory(userDataPath);
Das spezielle Verzeichnis Environment.SpecialFolder.ApplicationData ist bei macOS das Verzeichnis /Users/<username>/Library/Application Support/. In diesem Verzeichnis lege ich, sofern es noch nicht existiert, ein Verzeichnis für meine Anwendung an, hier „MrCarson“. Anschließend wird das aktuelle Verzeichnis für den Prozess auf dieses Verzeichnis gesetzt. Damit läuft die Anwendung dann auch als App Bundle.
Fazit
Eine macOS Anwendung, die über ein App Bundle gestartet wird, hat ihr aktuelles Verzeichnis im Wurzelverzeichnis „/“. Wer sich diesen unsinnigen Default ausgedacht hat, sei dahingestellt. Fakt ist, dass sich eine Anwendung bewusst entscheiden muss, wo sie Dateien ablegt, statt davon auszugehen, dass das aktuelle Verzeichnis dem entspricht, aus dem die Anwendung gestartet wurde.