Zum Inhalt springen
Build Station

macOS-Apps verteilen

Flutter-macOS-Apps über den Mac App Store oder direkt verteilen: Developer ID, Notarisierung, DMG per GitHub-Release oder Webhook, Sparkle-Updates.

Die Build Station bringt eine gebaute macOS-App (flutter build macos, Output-Typ macOS (.app)) auf einem von drei Wegen zu deinen Nutzern. Den Weg wählst du pro Projekt in Projekt bearbeiten › Upload › Store-Upload einrichten im Tab macOS unter Verteilung. Im Projekt-Editor führt auch macOS-Verteilung einrichten … dorthin.

Weg Was passiert Braucht zusätzlich
Mac App Store archivieren, für App Store Connect exportieren, mit xcrun altool hochladen Team und automatische Signierung im Xcode-Projekt, App in der Sandbox
GitHub-Release (DMG) mit Developer ID signieren, DMG bauen, notarisieren, als Release v<Version> per gh veröffentlichen Zertifikat „Developer ID Application”, gh auth login
HTTPS-Webhook (DMG) wie GitHub, aber ein signierter POST an deinen eigenen Server Zertifikat, Webhook-Adresse und Geheimnis

Alle drei Wege nutzen den App-Store-Connect-Schlüssel aus dem Tab „App Store”, also dieselben Zugangsdaten wie der iOS-Upload. Beim Direktvertrieb dient er der Notarisierung. Fehlt er, weist der Tab darauf hin. Wie du den Schlüssel anlegst, steht unter App Store und TestFlight.

Der automatische Upload nach einem erfolgreichen Build funktioniert wie bei den anderen Stores, siehe Artefakte und Upload.

Mac App Store

  1. In App Store Connect bei der App die Plattform macOS hinzufügen oder eine eigene macOS-App mit derselben Bundle-ID anlegen.
  2. Im Xcode-Projekt (macos/Runner.xcworkspace) ein Team setzen und „Automatically manage signing” einschalten. Die App muss in der Sandbox laufen.
  3. Zertifikat und Provisioning-Profil holt Xcode beim Export mit dem API-Schlüssel selbst. Soll Xcode dabei auch Zertifikate anlegen, braucht der Schlüssel die Rolle Admin. Sonst legst du die Zertifikate einmal in Xcode unter Settings › Accounts an.

Der Ablauf ist xcodebuild archive, dann -exportArchive für App Store Connect, dann xcrun altool --upload-app -t macos. Validiert wird vorab wie bei iOS. Build-Nummern zählen für macOS getrennt von iOS.

Direktvertrieb mit Developer ID

Außerhalb des Stores muss eine Mac-App mit einem Zertifikat Developer ID Application signiert und von Apple notarisiert sein, sonst blockiert macOS sie.

Einrichten

  1. Im Apple Developer Portal ein Zertifikat „Developer ID Application” anlegen. Das darf nur der Account Holder.
  2. Das Zertifikat in den Schlüsselbund des Macs legen, auf dem die Station läuft.
  3. Im Tab macOS die Verteilung wählen. Signier-Identität (optional) bleibt leer, wenn genau eine „Developer ID Application” im Schlüsselbund liegt.

Für GitHub-Release (DMG):

  • Unter Repository (owner/repo) das Ziel eintragen.
  • Hochgeladen wird mit der GitHub-CLI. Einmal gh auth login auf diesem Mac ausführen.

Für HTTPS-Webhook (DMG):

  • Webhook-Adresse (https://…) deines Servers eintragen.
  • Geheimnis für die Signatur eintragen oder mit Erzeugen und kopieren erzeugen und auf dem Server hinterlegen. Es liegt im Schlüsselbund.
  • Für Sparkle die Download-Adresse des Servers angeben, unter der DMG und appcast.xml danach liegen.

Was beim Upload passiert

  1. Signieren: Eine Kopie der App wird von innen nach außen signiert, also erst die Sparkle-Bestandteile, Frameworks und Bibliotheken, dann die App. Mit Hardened Runtime und Zeitstempel. Die Entitlements kommen aus macos/Runner/Release.entitlements des Projekts.
  2. DMG: Ein DMG mit Verknüpfung zu „Programme”, ebenfalls signiert. Es liegt neben dem Artefakt im Sammelordner unter distribution/.
  3. Notarisieren: mit xcrun notarytool submit --wait. Entscheidend ist der Status „Accepted”. Lehnt Apple ab, steht Apples Log im Upload-Log. Danach heftet stapler staple das Ticket an.
  4. Sparkle (optional): EdDSA-Signatur des DMG und eine appcast.xml daneben.
  5. Veröffentlichen: als GitHub-Release oder per Webhook. Gibt es auf GitHub schon ein Release derselben Version, werden seine Dateien ersetzt.

Das Developer-ID-Zertifikat steht mit Ablaufdatum in der Übersicht. Läuft es ab, laufen schon notarisierte Versionen weiter, nur neue lassen sich nicht mehr signieren.

Sparkle-Updates

Mit Sparkle findet die installierte App neue Versionen selbst. Dafür braucht deine App das Sparkle-Framework, in Flutter am einfachsten über das Paket auto_updater. Findet die Station Sparkle nicht im Projekt, warnt sie.

So richtest du es ein:

  1. Im Tab macOS Updates über Sparkle anbieten einschalten.
  2. Erzeugen legt ein Schlüsselpaar an. Hast du schon einen Schlüssel aus Sparkles generate_keys -x, übernimmst du ihn mit Importieren …. Der private Schlüssel geht in den Schlüsselbund.
  3. In Info.plist eintragen schreibt den öffentlichen Schlüssel (SUPublicEDKey), die Feed-Adresse (SUFeedURL) und SUEnableAutomaticChecks in macos/Runner/Info.plist.
  4. Die Änderung ins Repository committen. Wirksam ist sie ab dem nächsten Build.

Die Feed-Adresse ergibt sich aus dem Ziel:

Ziel SUFeedURL
GitHub https://github.com/<owner>/<repo>/releases/latest/download/appcast.xml
Webhook <Download-Adresse>/appcast.xml

Erzeug den Schlüssel nicht neu, wenn schon Versionen draußen sind. Die kennen nur den alten Schlüssel und nehmen neue Updates nicht an. Sie müssen dann einmal von Hand aktualisiert werden.

Der Appcast enthält nur das jüngste Update. sparkle:version ist die Build-Nummer (CFBundleVersion), danach entscheidet Sparkle, ob etwas neuer ist. Die Release Notes aus der Upload-Rückfrage erscheinen im Update-Fenster.

Das Webhook-Protokoll

Für den eigenen Server: Die Station schickt einen POST mit multipart/form-data.

Feld Inhalt
metadata JSON mit event (macos.release), app, bundleId, version, build, releaseNotes, downloadUrl und files[] mit field, name, size, sha256
dmg das notarisierte DMG
appcast die appcast.xml (nur mit Sparkle)

Dazu zwei Header:

X-BuildStation-Timestamp: 1760475600
X-BuildStation-Signature: sha256=<hex HMAC-SHA256(geheimnis, "<timestamp>.<metadata>")>

Dein Server sollte:

  1. die Signatur über genau den Text des Feldes metadata prüfen,
  2. alte Zeitstempel verwerfen, etwa älter als fünf Minuten,
  3. die SHA-256 der Dateien mit den Metadaten vergleichen,
  4. DMG und appcast.xml unter der Download-Adresse ablegen.

Jede Antwort mit Status 2xx gilt als Erfolg. Die Signaturprüfung in Node:

const expected = 'sha256=' + crypto.createHmac('sha256', secret)
  .update(`${req.headers['x-buildstation-timestamp']}.${fields.metadata}`)
  .digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-buildstation-signature']))) {
  return res.status(401).end();
}