Salta al contenuto principale

04 - Cross-platform deploy: macOS + Windows + Linux

Distribuire un'app Tauri 2 su macOS, Windows e Linux: pacchetti, firma del codice, notarizzazione, SmartScreen, GitHub Actions e chiavi dell'updater.

Cosa ne pensi di questo articolo?

04 - Cross-platform deploy: macOS + Windows + Linux
Tauri Rust DesktopFoto:Daniil KomovPexels

In Breve

Distribuire un'app Tauri 2 su macOS, Windows e Linux: pacchetti, firma del codice, notarizzazione, SmartScreen, GitHub Actions e chiavi dell'updater.

Un'app che funziona in sviluppo non è ancora un'app che si installa

Hai un'app Tauri che gira sul tuo portatile e devi farla arrivare a persone su macOS, Windows e Linux. Ogni sistema tratta in modo diverso un file scaricato dal browser: macOS mostra un avviso se l'app non è firmata, Windows SmartScreen avvisa che l'app non è considerata attendibile, Linux non chiede nulla. La risposta breve è in tre parti. Costruisci su un runner per sistema operativo, perché i pacchetti e la firma dipendono dalla macchina su cui lavori. Firma per macOS e Windows con i meccanismi di ciascuno, e notarizza su macOS. Tieni separata la chiave dell'updater, che non ha nulla a che fare con la firma del sistema operativo.

Che cosa ho provato, perché qui conta: nessun passaggio di firma o di notarizzazione è stato eseguito, perché non ho un certificato Apple né uno Windows e la macchina di scrittura non ha Rust né Cargo. Quello che ho verificato è la parte controllabile: tauri.conf.json contro lo schema ufficiale, il workflow come YAML, i nomi degli input di ogni azione contro il suo action.yml e i comandi di Apple e di Tauri contro le rispettive documentazioni, consultate il 2026-11-13. Prima di questo articolo c'è la scelta dei plugin, perché l'updater è uno di essi.

Che cosa produce tauri build e come lo configuri

Il comando tauri build compila l'app e la impacchetta nei formati del sistema su cui gira: su Windows un installer MSI (costruito con WiX v3, quindi solo su Windows) e un programma di installazione NSIS (-setup.exe); su macOS un .app e un .dmg; su Linux deb, rpm e AppImage. La configurazione sta nella sezione bundle di tauri.conf.json.

Configurazione di distribuzione con aggiornamenti automatici
{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "Appunti",
  "version": "1.0.0",
  "identifier": "dev.example.appunti",
  "build": {
    "frontendDist": "../dist",
    "beforeBuildCommand": "npm run build"
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "createUpdaterArtifacts": true,
    "windows": {
      "digestAlgorithm": "sha256",
      "timestampUrl": "http://timestamp.comodoca.com",
      "webviewInstallMode": { "type": "downloadBootstrapper" }
    }
  },
  "plugins": {
    "updater": {
      "pubkey": "INCOLLA-QUI-IL-CONTENUTO-DELLA-CHIAVE-PUBBLICA",
      "endpoints": [
        "https://github.com/ORGANIZZAZIONE/REPO/releases/latest/download/latest.json"
      ]
    }
  }
}

Chiave per chiave. bundle.targets accetta "all", un singolo formato o una lista (deb, rpm, appimage, msi, nsis, app, dmg); se non la scrivi vale "all", cioè tutti i formati possibili sul sistema corrente, e restringerla accorcia la build. createUpdaterArtifacts vale false per impostazione predefinita: con true Tauri produce anche gli archivi e le firme che l'updater usa, e per farlo gli serve la chiave privata descritta più avanti. webviewInstallMode decide come l'installer Windows ottiene WebView2 e vale downloadBootstrapper se non lo scrivi (la tabella dei pesi è nell' articolo sul confronto con Electron).

digestAlgorithm e timestampUrl riguardano la firma Windows: il primo è l'algoritmo dell'impronta (la guida di Tauri indica che di solito è sha256), il secondo un server di marca temporale, che la guida descrive come il server usato per verificare l'ora in cui è stata fatta la firma. Nella sezione plugins.updater la chiave pubblica deve essere il contenuto, non il percorso di un file, e gli indirizzi in endpoints devono essere HTTPS in produzione; Tauri passa all'indirizzo successivo solo se il primo risponde con un codice diverso da 2xx. Ho validato il file contro lo schema JSON della versione 2.12.5: risulta valido.

Un runner per sistema operativo

Si può cross-compilare qualcosa, ma la documentazione lo presenta come ultima risorsa. Le ragioni stanno nei vincoli di ciascun sistema:

  • Windows: l'MSI si costruisce solo su Windows; l'NSIS si può costruire anche da Linux e macOS, ma serve un comando di firma esterno.
  • macOS: la firma richiede un dispositivo Apple, per i termini del programma sviluppatori citati dalla guida.
  • Linux: il binario va compilato sul sistema più vecchio che vuoi supportare, perché glibc rompe la compatibilità con le versioni precedenti; la guida indica come basi adatte Ubuntu 22.04 e Debian 12, che offrono WebKitGTK 4.1.

Messi insieme, questi tre vincoli rendono la strada semplice quella di un job per sistema operativo, ciascuno sulla propria macchina. Per Linux su processore Arm, la guida segnala che dall'agosto 2025 GitHub mette a disposizione runner Arm per i repository pubblici; nei repository privati una build in emulazione di un'app appena creata impiega circa un'ora e costa minuti fatturati.

macOS: firma e notarizzazione

La notarizzazione è un controllo automatico di Apple: il servizio scansiona il software alla ricerca di codice malevolo e di problemi di firma, e se tutto va bene emette un ticket che Gatekeeper trova online. Per il software distribuito fuori dal Mac App Store con un certificato Developer ID è obbligatoria dal macOS 10.15 (per il software compilato dopo il 1° giugno 2019); il servizio richiede un certificato «Developer ID», l'Hardened Runtime e una marca temporale sicura. Dal 1° novembre 2023 accetta solo invii fatti con notarytool: per notarizzare non usare più il vecchio altool (resta in uso, nella guida di Tauri, per caricare le app iOS su App Store Connect).

La parte comoda è che con Tauri non devi richiamare notarytool a mano: la guida dice che, impostate le variabili d'ambiente, basta rilanciare la build e Tauri firma e notarizza.

Firma, notarizzazione e verifica su un Mac (schema da adattare, non eseguito)
# 1. elenca le identità di firma presenti nel portachiavi
security find-identity -v -p codesigning

# 2. firma: l'identità è quella "Developer ID Application" della riga precedente
export APPLE_SIGNING_IDENTITY="Developer ID Application: Nome Cognome (TEAMID)"

# 3. notarizzazione con ID Apple e password specifica per app
export APPLE_ID="nome@example.org"
export APPLE_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="TEAMID"

npm run tauri build -- --bundles dmg

# 4. verifiche con gli strumenti di Apple
APP=src-tauri/target/release/bundle/macos/Appunti.app
codesign -vvv --deep --strict "$APP"
spctl -vvv --assess --type exec "$APP"

APPLE_SIGNING_IDENTITY è il nome della voce del portachiavi, quello che il primo comando elenca; in alternativa si usa bundle.macOS.signingIdentity in configurazione. Per notarizzare hai due strade: l'ID Apple con una password specifica per app e il Team ID (APPLE_ID, APPLE_PASSWORD, APPLE_TEAM_ID), oppure una chiave API di App Store Connect (APPLE_API_ISSUER, APPLE_API_KEY, APPLE_API_KEY_PATH). In CI la seconda è più pulita, ma richiede di scrivere il file della chiave sul runner, quindi il workflow più sotto usa la prima per restare corto. Un account gratuito non permette di notarizzare: la guida cita 99 dollari l'anno per quello a pagamento, cifra da riverificare sul sito di Apple.

Le due verifiche finali vengono dalla documentazione di Apple: codesign -vvv --deep --strict controlla la firma, compresi i contenuti annidati, e spctl -vvv --assess --type exec chiede al sistema se l'app verrebbe eseguita con le politiche in vigore. La graffatura (stapling) allega il ticket al file così che Gatekeeper lo trovi anche senza rete; con --skip-stapling puoi saltarla in un primo giro. Se non hai nessuna identità, la firma ad hoc ("signingIdentity": "-") evita il messaggio «danneggiato» sui Mac Apple Silicon, ma l'utente deve comunque consentire l'app in Privacy e sicurezza.

Windows: SmartScreen, firma e reputazione

Su Windows la firma non è necessaria per eseguire l'app: se manca, SmartScreen mostra un avviso a chi scarica dal browser. C'è però un'eccezione: Windows 11 con Smart App Control attivo blocca le app sconosciute e non firmate anche se non scaricate dal browser, e l'utente non può consentirne una sola, può solo spegnere la funzione. Un punto in cui i testi più vecchi sono superati: la documentazione di Tauri spiega che dal 2024 un certificato EV non dà più reputazione immediata, perché Microsoft ha tolto il trattamento speciale dal suo programma di radici attendibili; certificati EV e OV costruiscono reputazione allo stesso modo, e una nuova versione firmata può mostrare l'avviso con entrambi.

Se firmi ogni versione con lo stesso certificato, la reputazione si accumula fra una release e l'altra; puoi anche inviare il file a Microsoft per una revisione manuale, senza garanzia di esito. Il procedimento con un file .pfx, un'impronta digitale (certificateThumbprint) e digestAlgorithm è documentato da Tauri ma vale per i certificati OV ottenuti prima del 1° giugno 2023: per gli altri la guida rimanda alla documentazione di chi li emette. Il meccanismo generale è bundle.windows.signCommand, un comando qualunque in cui %1 è il file da firmare.

Configurazione aggiuntiva solo per Windows, con Azure Artifact Signing
{
  "bundle": {
    "windows": {
      "signCommand": "artifact-signing-cli -e https://wus2.codesigning.azure.net -a MyAccount -c MyProfile -d Appunti %1"
    }
  }
}

Il file è l'esempio della guida (servizio chiamato prima Azure Trusted Signing): -e è l'endpoint dell'account, -a il nome dell'account, -c il profilo del certificato e -d la descrizione, che compare nel prompt UAC per un MSI. Non lo mescolo alla configurazione principale: lo passo con --config solo dal runner Windows (il riferimento della CLI aggiunge che i file chiamati tauri.windows.conf.json, tauri.macos.conf.json e simili vengono uniti da soli), e ho validato la configurazione unita a quella principale contro lo schema. Lo strumento si installa con cargo install artifact-signing-cli e legge AZURE_CLIENT_ID, AZURE_CLIENT_SECRET e AZURE_TENANT_ID, da tenere fra i segreti. La stessa pagina mostra l'alternativa con Azure Key Vault. Quando cross-compili da Linux o macOS, un signCommand è obbligatorio.

Linux: la firma è facoltativa, le dipendenze no

Per i pacchetti Linux non esiste un obbligo di firma, ma l'AppImage può incorporarne una GPG. L'AppImage in sé non la verifica: lo fa l'utente con lo strumento validate, e per renderlo sensato devi pubblicare l'identificativo della chiave su un canale autenticato, come il tuo sito servito in HTTPS.

Firma di un AppImage con GPG (schema da adattare, non eseguito)
export SIGN=1
export APPIMAGETOOL_SIGN_PASSPHRASE="$GPG_PASSPHRASE"
export APPIMAGETOOL_FORCE_SIGN=1

npm run tauri build -- --bundles appimage

# mostra la firma incorporata
./src-tauri/target/release/bundle/appimage/Appunti_1.0.0_amd64.AppImage --appimage-signature

SIGN=1 abilita la firma, APPIMAGETOOL_SIGN_PASSPHRASE evita la finestra di richiesta della password (necessario in CI) e APPIMAGETOOL_FORCE_SIGN=1 fa fallire la build se la firma non riesce: senza, l'AppImage viene prodotto comunque, e rischi di pubblicare un file non firmato credendolo firmato. L'ultima riga mostra la firma incorporata. Quanto ai pacchetti, il deb predefinito dichiara come dipendenze libwebkit2gtk-4.1-0 e libgtk-3-0 e può includere file aggiuntivi con bundle.linux.deb.files. L'AppImage, invece, porta con sé le librerie: da 2-6 MB passa a oltre 70 MB, come scrive la guida.

Automatizzare tutto con GitHub Actions

Il workflow qui sotto costruisce l'app su quattro job (due macOS, Ubuntu 22.04, Windows), li firma e crea una release in bozza con gli installer. È adattato dall'esempio della guida di Tauri.

Workflow di rilascio per i tre sistemi (schema da adattare, non eseguito)
name: publish

on:
  workflow_dispatch:
  push:
    tags:
      - 'app-v*'

jobs:
  publish-tauri:
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: macos-latest
            args: '--target aarch64-apple-darwin'
          - platform: macos-latest
            args: '--target x86_64-apple-darwin'
          - platform: ubuntu-22.04
            args: ''
          - platform: windows-latest
            args: '--config src-tauri/tauri.windows-sign.conf.json'
    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v7

      - name: dipendenze di sistema (solo Ubuntu)
        if: matrix.platform == 'ubuntu-22.04'
        run: |
          sudo apt-get update
          sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils

      - uses: actions/setup-node@v7
        with:
          node-version: lts/*
          cache: npm

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}

      - uses: swatinem/rust-cache@v2
        with:
          workspaces: './src-tauri -> target'

      - run: npm ci

      - name: importa il certificato Apple (solo macOS)
        if: matrix.platform == 'macos-latest'
        env:
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
          security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security set-keychain-settings -t 3600 -u build.keychain
          security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
          CERT_ID=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application" | head -n 1 | awk -F'"' '{print $2}')
          echo "CERT_ID=$CERT_ID" >> "$GITHUB_ENV"

      - name: installa lo strumento di firma Windows
        if: matrix.platform == 'windows-latest'
        run: cargo install artifact-signing-cli

      - uses: tauri-apps/tauri-action@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
          AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        with:
          tagName: app-v__VERSION__
          releaseName: 'Appunti v__VERSION__'
          releaseBody: 'Scarica il file per il tuo sistema dagli asset.'
          releaseDraft: true
          prerelease: false
          args: ${{ matrix.args }}

I punti da capire. La matrice ha due voci macOS perché il processore Apple Silicon e quello Intel hanno target diversi; fail-fast: false lascia proseguire gli altri job se uno fallisce. permissions: contents: write serve per creare la release. L'installazione delle dipendenze di sistema vale solo su Ubuntu; targets di rust-toolchain serve solo ai runner macOS; workspaces di rust-cache rispetta la struttura predefinita di un progetto Tauri. Ho usato npm ci al posto di npm install perché rispetta il file di blocco e fallisce se è disallineato: richiede che il file ci sia.

Il passo del certificato Apple crea un portachiavi temporaneo, vi importa il file .p12 decodificato dal segreto e salva in CERT_ID il nome dell'identità «Developer ID Application». L'azione tauri-action legge le variabili d'ambiente di firma, di notarizzazione e dell'updater. Con tagName: app-v__VERSION__ l'azione sostituisce __VERSION__ con la versione dell'app e crea il tag, e releaseDraft: true lascia la release in bozza finché non l'hai controllata. Trigger e tag stanno in un solo posto: pubblichi spingendo un tag app-vX.Y.Z.

Controllo dei nomi degli input contro i file action.yml (eseguito)
import urllib.request

import yaml

workflow = yaml.safe_load(open("publish.yml", encoding="utf-8"))
steps = workflow["jobs"]["publish-tauri"]["steps"]

for step in steps:
    uses = step.get("uses")
    if not uses:
        continue
    repo, ref = uses.split("@")
    url = f"https://raw.githubusercontent.com/{repo}/{ref}/action.yml"
    with urllib.request.urlopen(url, timeout=30) as response:
        action = yaml.safe_load(response.read())
    dichiarati = set(action.get("inputs", {}))
    usati = set(step.get("with", {}))
    mancanti = sorted(usati - dichiarati)
    print(f"{uses}: input usati {sorted(usati)} -> {'tutti dichiarati' if not mancanti else 'NON dichiarati: ' + str(mancanti)}")

# Output (due esecuzioni identiche, 2026-11-13):
# actions/checkout@v7: input usati [] -> tutti dichiarati
# actions/setup-node@v7: input usati ['cache', 'node-version'] -> tutti dichiarati
# dtolnay/rust-toolchain@stable: input usati ['targets'] -> tutti dichiarati
# swatinem/rust-cache@v2: input usati ['workspaces'] -> tutti dichiarati
# tauri-apps/tauri-action@v1: input usati ['args', 'prerelease', 'releaseBody', 'releaseDraft', 'releaseName', 'tagName'] -> tutti dichiarati

Per non fidarmi della memoria ho scritto un controllo: legge il workflow, scarica per ogni azione il suo action.yml al riferimento usato (v7, stable, v2, v1) e confronta gli input che passo con quelli dichiarati. Tutti risultano dichiarati. Il controllo prova che i nomi esistono, non che i valori siano giusti, e non sostituisce un'esecuzione vera del workflow su un repository di prova: quella non l'ho fatta.

Gli aggiornamenti: una chiave diversa dalla firma del sistema

Il plugin updater verifica con una firma propria che l'aggiornamento arrivi da te. È una coppia di chiavi diversa dai certificati di Apple o Microsoft, e non si può disattivare.

Chiavi dell'updater e variabili di build (schema da adattare, non eseguito)
# genera la coppia di chiavi dell'updater (diversa dalla firma del sistema operativo)
npm run tauri signer generate -- -w ~/.tauri/appunti.key

# in fase di build servono nell'ambiente (un file .env non basta)
export TAURI_SIGNING_PRIVATE_KEY="$HOME/.tauri/appunti.key"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="la-tua-password"
npm run tauri build

Il comando signer generate crea la chiave privata (nel percorso dato con -w) e la pubblica da incollare nel campo pubkey della configurazione. La privata va nell'ambiente di build: la guida avverte che un file .env non funziona. Se perdi la chiave privata non puoi più pubblicare aggiornamenti per chi ha già installato l'app: conservala in un gestore di segreti e fanne una copia. Il file latest.json che tauri-action carica nella release (se l'updater è configurato, e uploadUpdaterJson vale true per impostazione predefinita) contiene almeno version e, per ogni piattaforma, url e signature. L'updater è solo desktop, come mostra la classificazione del precedente articolo.

Prima di rilasciare

Prova il pacchetto su macchine che non sono la tua, perché sulla tua ci sono già runtime, certificati e librerie:

  • Installa e avvia l'app su una macchina o una VM pulita per ciascun sistema.
  • Su macOS esegui codesign e spctl come sopra; su Windows guarda se compare l'avviso SmartScreen.
  • Su un Windows senza WebView2 (un Windows 10 precedente alla versione 1803, per esempio) controlla che l'installer lo scarichi, o scegli la modalità offline.
  • Su Linux avvia l'AppImage sulla distribuzione più vecchia che vuoi supportare.
  • Installa la versione 1.0.0 e verifica che si aggiorni alla successiva con l'updater.

L'ultima prova è quella che si dimentica e che non puoi ripetere dopo il rilascio: se la chiave pubblica nell'app non corrisponde alla privata che ha firmato l'aggiornamento, gli utenti che hanno già installato non si aggiorneranno e dovranno scaricare di nuovo l'app a mano.

Domande frequenti

Posso compilare per Windows da un Mac o da Linux?

Solo l'installer NSIS, e come ultima risorsa: la documentazione lo descrive come meno collaudato. L'MSI richiede Windows, e la firma richiede un signCommand esterno perché quella predefinita funziona solo su Windows.

Mi serve un certificato EV per evitare l'avviso di SmartScreen?

No, e non basta: dal 2024 EV e OV costruiscono reputazione allo stesso modo secondo la documentazione di Tauri, e una nuova versione firmata può mostrare l'avviso con entrambi. Firmare sempre con lo stesso certificato aiuta la reputazione ad accumularsi.

La notarizzazione su macOS è obbligatoria?

Per il software distribuito fuori dal Mac App Store con un certificato Developer ID, sì dal macOS 10.15. Per il Mac App Store non serve perché la revisione include controlli equivalenti. Con un account Apple gratuito non è possibile.

Come distribuisco un'app per Android e iOS?

Con i pacchetti degli store e con un procedimento a parte, descritto in Tauri mobile su Android e iOS; l'updater qui descritto non vale su mobile.

Che cosa sai fare adesso

Sai quali pacchetti produce Tauri su ciascun sistema e quali chiavi di tauri.conf.json li governano, perché serve un runner per sistema, come firmare e notarizzare su macOS con le variabili d'ambiente, e come stanno oggi le cose su SmartScreen e certificati EV. Sai impostare un workflow di rilascio verificato nei nomi degli input, tenere separata la chiave dell'updater e preparare una prova su macchine pulite. Quello che resta da fare con i tuoi segreti reali è eseguire il workflow una volta su un repository di prova.

Fonti

Il file tauri.conf.json (anche unito alla configurazione Windows) è stato validato contro lo schema JSON di Tauri 2.12.5 e il workflow è stato letto come YAML; lo script di controllo degli input è stato eseguito due volte con la stessa uscita. Nessun passaggio di firma, notarizzazione o build è stato eseguito: i blocchi di shell e il workflow sono schemi da adattare, non eseguiti.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti