# Tauri-Release und Auto-Update Der Desktop-Client ist auf Auto-Update vorbereitet, aber nicht scharf geschaltet. Dieses Dokument beschreibt den vollständigen Weg von den Signing-Keys bis zum ausgelieferten `latest.json` — und was dafür im Repo noch fehlt. Alle Pfade sind relativ zu `crates/tauri-app/`, sofern nicht anders angegeben. ## Ist-Zustand | Baustein | Stand | |---|---| | Rust-Plugin | `tauri_plugin_updater::Builder::new().build()` ist in `src-tauri/src/lib.rs` registriert | | Capability | `src-tauri/capabilities/default.json` enthält `updater:default` → `allow-check`, `allow-download`, `allow-install`, `allow-download-and-install` für das `main`-Window | | Config | `src-tauri/tauri.conf.json`, Block `plugins.updater`: leerer `pubkey`, Platzhalter-Endpoint `https://releases.maarcadetweet.local/{{target}}/{{arch}}/{{current_version}}` | | Updater-Artefakte | `bundle.createUpdaterArtifacts` ist **nicht** gesetzt (Default: aus) — der Build erzeugt keine `.sig`-Dateien und kein `.app.tar.gz` | | Aufruf im Client | **Fehlt.** Weder das npm-Paket `@tauri-apps/plugin-updater` noch ein `check()`-Aufruf existieren (weder in `src/` noch in `src-tauri/src/`) | | CLI | `@tauri-apps/cli` ist als devDependency installiert, lokal geprüft: `npx tauri --version` → `tauri-cli 2.11.4` | | Plugin-Version | `tauri-plugin-updater` 2.10.1 laut `src-tauri/Cargo.lock` | **Wichtig:** Die Felder `active` und `dialog` in `plugins.updater` stammen aus Tauri v1. Der v2-Updater kennt sie nicht — seine `Config` besteht aus `endpoints`, `pubkey`, `windows`, `dangerousInsecureTransportProtocol`, `dangerousAcceptInvalidCerts`, `dangerousAcceptInvalidHostnames`. Unbekannte Schlüssel werden beim Deserialisieren still ignoriert. `"active": false` schaltet den Updater also **nicht** ab; dass heute nichts passiert, liegt allein daran, dass niemand `check()` aufruft. Wer den Updater wirklich deaktivieren will, entfernt den `plugins.updater`-Block und die `tauri_plugin_updater`-Registrierung. ## 1. Signing-Keys erzeugen Der Updater akzeptiert nur signierte Artefakte. Das Schlüsselpaar wird einmal erzeugt und danach nie wieder gewechselt — ein neuer Public Key macht alle installierten Clients update-unfähig. ```bash cd crates/tauri-app npx tauri signer generate -w ~/.tauri/maarcadetweet.key ``` Optionen laut `npx tauri signer generate --help`: | Flag | Bedeutung | |---|---| | `-w, --write-keys ` | Privaten Schlüssel in eine Datei schreiben (sonst nur stdout) | | `-p, --password ` | Passwort für den privaten Schlüssel setzen | | `-f, --force` | Vorhandene Datei überschreiben | | `--ci` | Keine interaktiven Rückfragen | Ergebnis: `~/.tauri/maarcadetweet.key` (privat, **niemals** ins Repo) und `~/.tauri/maarcadetweet.key.pub` (öffentlich, base64). Der Inhalt der `.pub`-Datei ist genau das, was in `pubkey` gehört. Für den Build-Rechner bzw. die CI: ```bash export TAURI_SIGNING_PRIVATE_KEY="$(cat ~/.tauri/maarcadetweet.key)" export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="…" ``` `TAURI_SIGNING_PRIVATE_KEY` akzeptiert den Schlüsselinhalt oder einen Pfad. Fehlt die Variable beim Build mit aktivierten Updater-Artefakten, bricht das Bundling ab. ## 2. Konfiguration — ohne den Dev-Zustand zu brechen Die laufende `src-tauri/tauri.conf.json` soll unangetastet bleiben (leerer `pubkey`, keine Updater-Artefakte). Die CLI kann Konfigurationen zur Build-Zeit zusammenführen: ``` -c, --config JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file ``` Der saubere Weg ist deshalb eine **Release-Overlay-Datei**, die nur die Release-Unterschiede enthält. Anlegen als `crates/tauri-app/src-tauri/tauri.release.conf.json` (existiert noch nicht): ```json { "bundle": { "createUpdaterArtifacts": true }, "plugins": { "updater": { "endpoints": [ "https://releases.example.org/maarcadetweet/latest.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6…HIER_DER_INHALT_VON_.key.pub…", "windows": { "installMode": "passive" } } } } ``` Build damit: ```bash cd crates/tauri-app npx tauri build --config src-tauri/tauri.release.conf.json ``` Feldbedeutungen (geprüft an `tauri-plugin-updater` 2.11 / `tauri-utils` 2.9): | Feld | Bedeutung | |---|---| | `bundle.createUpdaterArtifacts` | `true` erzeugt die Updater-Artefakte (`.sig`, unter macOS zusätzlich das `.app.tar.gz`). Alternativ `"v1Compatible"` für v1-Clients | | `plugins.updater.pubkey` | Public Key aus Schritt 1. Pflichtfeld — fehlt es ganz, scheitert das Deserialisieren der Plugin-Config | | `plugins.updater.endpoints` | Liste von URLs, der Reihe nach probiert. Müssen `https` sein: in Release-Builds bricht ein `http`-Endpoint mit `InsecureTransportProtocol` ab (im Debug-Build nur eine Warnung), es sei denn `dangerousInsecureTransportProtocol` ist gesetzt | | `plugins.updater.windows.installMode` | `passive` (Default), `basicUi` oder `quiet` | ### Endpoint-Platzhalter In der Endpoint-URL werden ersetzt: `{{current_version}}`, `{{target}}`, `{{arch}}` und `{{bundle_type}}`. Achtung auf einen Fallstrick — der bereits in der Config stehende Platzhalter-Endpoint benutzt genau diese Form: * `{{target}}` wird in der **URL** nur durch das Betriebssystem ersetzt (`linux`, `darwin`, `windows`) — nicht durch `darwin-aarch64`. * `{{arch}}` liefert die Architektur (`x86_64`, `aarch64`, `i686`, `armv7`, `riscv64`). * `{{bundle_type}}` liefert den laufenden Bundle-Typ: `app`, `msi`, `nsis`, `appimage`, `deb` oder `rpm` — bzw. `unknown`, wenn er nicht bestimmbar ist. Ein statisches `latest.json` (eine Datei für alle Plattformen) ist der einfachere Weg und kommt ohne Platzhalter aus. ## 3. `latest.json` Der Updater versteht zwei Formate. Für einen einfachen Static-File-Server ist das **statische Format** richtig: ```json { "version": "0.2.0", "notes": "Thread-Ansicht, schnellere Timeline", "pub_date": "2026-01-15T10:00:00Z", "platforms": { "darwin-aarch64": { "signature": "", "url": "https://releases.example.org/maarcadetweet/0.2.0/maarcadetweet_aarch64.app.tar.gz" }, "darwin-x86_64": { "signature": "…", "url": "https://releases.example.org/maarcadetweet/0.2.0/maarcadetweet_x64.app.tar.gz" }, "linux-x86_64": { "signature": "…", "url": "https://releases.example.org/maarcadetweet/0.2.0/maarcadetweet_0.2.0_amd64.AppImage" }, "windows-x86_64": { "signature": "…", "url": "https://releases.example.org/maarcadetweet/0.2.0/maarcadetweet_0.2.0_x64-setup.exe" } } } ``` Regeln, die der Plugin-Code vorgibt: * **Schlüssel der `platforms`-Map:** Der Updater sucht in dieser Reihenfolge nach `{os}-{arch}-{installer}` und danach nach `{os}-{arch}`. `os` ∈ `linux` | `darwin` | `windows`, `arch` ∈ `i686` | `x86_64` | `armv7` | `aarch64` | `riscv64`, `installer` ∈ `appimage` | `deb` | `rpm` | `app` | `msi` | `nsis`. Die Kurzform `darwin-aarch64` reicht also; die lange Form `windows-x86_64-nsis` ist nur nötig, wenn man MSI und NSIS gleichzeitig ausliefert. * **`version`** wird als Semver geparst. Ein Update wird nur angeboten, wenn `release.version > current_version` — die Version kommt aus dem Feld `version` in `tauri.conf.json` (heute `0.1.0`). Ein Release ohne Versionsbump wird stillschweigend ignoriert. * **`signature`** ist der **Inhalt** der zugehörigen `.sig`-Datei, nicht deren URL. * **`notes`** und **`pub_date`** sind optional; `pub_date` als RFC 3339. * Ein dynamischer Endpoint darf mit **`204 No Content`** antworten, wenn es kein Update gibt. Dann liefert er statt der `platforms`-Map ein flaches Objekt mit `version`, `url` und `signature` für genau die anfragende Plattform. ## 4. Build pro Plattform `tauri build` ist ein Cross-Build nur eingeschränkt — jede Plattform wird auf ihrem eigenen Host (oder Runner) gebaut. ```bash cd crates/tauri-app # macOS (Apple Silicon) npx tauri build --config src-tauri/tauri.release.conf.json # macOS Universal (beide Rust-Targets müssen installiert sein) npx tauri build --target universal-apple-darwin \ --config src-tauri/tauri.release.conf.json # Windows / Linux jeweils auf dem passenden Host, gleicher Aufruf ``` Ablauf laut `build.beforeBuildCommand` in `tauri.conf.json`: erst `npm run build` (Vite → `crates/tauri-app/dist`, entspricht `frontendDist: "../dist"`), dann der Cargo-Release-Build, dann das Bundling. `bundle.targets` steht auf `"all"`. Nützliche Flags (aus `npx tauri build --help`): | Flag | Zweck | |---|---| | `-t, --target ` | Ziel-Triple, oder `universal-apple-darwin` | | `-b, --bundles ` | Nur bestimmte Bundle-Typen erzeugen | | `--no-bundle` | Nur das Binary, kein Installer | | `-c, --config ` | Config-Overlay (siehe oben) | | `--ci` | Keine interaktiven Rückfragen | ## 5. Wo die Artefakte landen `crates/tauri-app/src-tauri/Cargo.toml` deklariert ein eigenes `[workspace]`, der Build-Output liegt deshalb **nicht** im Repo-Root-`target/`, sondern unter `crates/tauri-app/src-tauri/target/release/bundle/`. Mit `--target ` schiebt sich das Triple dazwischen: `src-tauri/target//release/bundle/`. | Plattform | Pfad unter `bundle/` | Für den Updater relevant | |---|---|---| | macOS | `macos/maarcadetweet.app` | `macos/maarcadetweet.app.tar.gz` + `.tar.gz.sig` | | macOS | `dmg/maarcadetweet__.dmg` | nein (nur für die Erstinstallation) | | Windows | `msi/maarcadetweet___.msi` | `.msi` + `.msi.sig` | | Windows | `nsis/maarcadetweet__-setup.exe` | `.exe` + `.exe.sig` | | Linux | `appimage/maarcadetweet__.AppImage` | `.AppImage` + `.AppImage.sig` | | Linux | `deb/…deb`, `rpm/…rpm` | nein | Die `.sig`-Dateien entstehen nur, wenn `createUpdaterArtifacts` aktiv ist und die Signing-Env-Variablen gesetzt sind. Für das `latest.json` wird der **Dateiinhalt** der `.sig` gebraucht, das Artefakt selbst wird nicht ausgeliefert. ## 6. Was im Client noch fehlt Config und Signaturen allein bewirken nichts — der Client fragt nirgends nach Updates. Es gibt zwei Wege: ### a) Vom Frontend aus ```bash cd crates/tauri-app npm install @tauri-apps/plugin-updater @tauri-apps/plugin-process ``` ```ts import { check } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; const update = await check(); if (update) { await update.downloadAndInstall(); await relaunch(); } ``` Die Capability erlaubt `check`, `download`, `install` und `downloadAndInstall` bereits. **Zusätzlich nötig:** `tauri-plugin-process` als Rust-Dependency, `tauri_plugin_process::init()` in `lib.rs` und `"process:default"` in `capabilities/default.json` — sonst schlägt `relaunch()` fehl. ### b) Vom Rust-Layer aus ```rust use tauri_plugin_updater::UpdaterExt; if let Ok(Some(update)) = app.updater()?.check().await { update.download_and_install(|_, _| {}, || {}).await?; } ``` Ohne npm-Abhängigkeit, dafür ohne UI-Kontrolle im Frontend. Der Aufruf gehört in den `setup`-Hook oder hinter ein Tauri-Command, das die Svelte-Seite aufrufen kann. Welcher Weg genommen wird, ist **noch offen** — im Repo existiert keiner von beiden. ## 7. Release-Checkliste 1. `version` in `src-tauri/tauri.conf.json` erhöhen (Semver; der Vergleich entscheidet, ob ein Update angeboten wird). `package.json` und `src-tauri/Cargo.toml` der Konsistenz halber mitziehen. 2. Auf jedem Plattform-Host mit gesetzten `TAURI_SIGNING_PRIVATE_KEY*`-Variablen und dem Release-Overlay bauen. 3. Installer und Updater-Artefakte einsammeln, `.sig`-Inhalte auslesen. 4. `latest.json` schreiben und zusammen mit den Artefakten unter einer `https`-URL veröffentlichen. Die URL muss zum `endpoints`-Eintrag passen. 5. Mit einer älteren installierten Version gegenprüfen, dass `check()` das Update findet und die Signaturprüfung durchgeht. ## 8. CI: Tag-Release über Gitea Actions `.gitea/workflows/release.yml` baut Schritt 2 der Checkliste für **Windows und Linux** automatisch. Trigger ist ein Tag `v*.*.*` (zusätzlich manuell per `workflow_dispatch`, dann ohne Release-Anlage). | Job | `runs-on` | Runner | Bundles | |---|---|---|---| | `windows` | `windows` | winbuild, 192.168.1.69 (on-demand) | `msi/*.msi`, `nsis/*-setup.exe` | | `linux` | `ubuntu-latest` | ci-runner, 192.168.1.72 | `deb/*.deb`, `rpm/*.rpm`, `appimage/*.AppImage` | Beide Jobs laufen `npm ci` (es gibt eine `package-lock.json`) und danach den npm-Skript-Umweg `npm run tauri -- build --ci` aus `crates/tauri-app/`, damit die im Lock gepinnte `@tauri-apps/cli` benutzt wird und nicht die zufällig auf dem Runner installierte. Die Pfade sind die aus Abschnitt 5. Beide laden ihre Bundles als Job-Artefakt hoch **und** hängen sie an dasselbe Gitea-Release zum Tag (anlegen, und falls der andere Job schneller war, das vorhandene per Tag holen). Die Release-Beschreibung kommt aus dem passenden `## []`-Abschnitt einer `CHANGELOG.md`, sobald es eine gibt — bis dahin steht dort der Commit-SHA. Zwei Dinge, die der Workflow *nicht* tut: * **macOS.** Es gibt keinen macOS-Runner. `.dmg`/`.app` werden lokal nach Abschnitt 4 gebaut und im Gitea-Release von Hand angehängt. * **Signierte Updater-Artefakte.** Der Workflow baut ohne Release-Overlay und ohne `TAURI_SIGNING_PRIVATE_KEY*`; es entstehen also keine `.sig`-Dateien (Abschnitt 2 und 5). Für ein echtes Auto-Update müssen Overlay-Datei und Secrets ergänzt und der Build-Aufruf um `--config src-tauri/tauri.release.conf.json` erweitert werden. **Vor dem Tag zu bumpen** (Schritt 1 der Checkliste): Die Release-Version kommt aus dem Tag, die Version im *Dateinamen* aus `src-tauri/tauri.conf.json`. Ohne Bump heißt das Artefakt zu `v0.2.0` weiterhin `maarcadetweet_0.1.0_x64-setup.exe`. `src-tauri/Cargo.toml` und `package.json` mitziehen — alle drei stehen aktuell auf `0.1.0`. > **Falle:** Sobald `.gitea/workflows/` existiert, ignoriert Gitea > `.github/workflows/` vollständig — kommentarlos, ohne roten Lauf. Im > Nachbarprojekt `lserver` waren Tests dadurch einen Tag lang still > abgeschaltet. Dieses Repo hat kein `.github/`, und das soll so bleiben: neue > Workflows gehören nach `.gitea/workflows/`. ## 9. Offene Punkte * Kein Release-Overlay im Repo — die Datei aus Abschnitt 2 muss angelegt werden. Der Endpoint `https://releases.maarcadetweet.local/…` in der aktuellen Config ist ein Platzhalter und existiert nicht. * Kein Update-Server und kein Skript, das `latest.json` erzeugt (`scripts/` ist leer). Der CI-Workflow aus Abschnitt 8 baut und veröffentlicht Installer, aber keine Updater-Artefakte. * Keine Code-Signierung/Notarisierung für macOS und keine Authenticode-Signatur für Windows konfiguriert (`bundle` enthält weder `macOS.signingIdentity` noch `windows.certificateThumbprint`). Der Tauri-Updater-Schlüssel ersetzt das nicht — ohne Notarisierung meldet Gatekeeper die App als nicht verifiziert. * Die Felder `active` und `dialog` in `plugins.updater` sind v1-Reste ohne Wirkung (siehe oben) und sollten beim Scharfschalten entfernt werden.