Nucleus
Packaging & distribution

Auto-update

Vérifiez, téléchargez, validez et installez les mises à jour de votre application depuis Kotlin, sans service tiers.

Le module updater-runtime vérifie, télécharge, valide et installe les mises à jour de votre application depuis Kotlin. Il a une moitié build-time — les métadonnées latest-*.yml générées à côté de vos installateurs — et une moitié runtime, la classe NucleusUpdater. Le format des métadonnées est compatible avec le format de mise à jour d'electron-builder.

Ajouter la dépendance

build.gradle.kts
dependencies {
    implementation("dev.nucleusframework:nucleus.updater-runtime:2.4.3")
    // Optionnel, pour les réseaux d'entreprise avec une CA racine privée :
    implementation("dev.nucleusframework:nucleus.native-http:2.4.3")
}

Vérifier et installer une mise à jour

Créez un NucleusUpdater, interrogez le provider configuré, puis téléchargez et installez :

import dev.nucleusframework.updater.NucleusUpdater
import dev.nucleusframework.updater.UpdateResult
import dev.nucleusframework.updater.provider.GitHubProvider
import java.io.File

val updater = NucleusUpdater {
    provider = GitHubProvider(owner = "myorg", repo = "myapp")
}

when (val result = updater.checkForUpdates()) {
    is UpdateResult.Available -> {
        var installer: File? = null
        updater.downloadUpdate(result.info).collect { progress ->
            println("${progress.percent.toInt()}%")
            progress.file?.let { installer = it }
        }
        // installAndRestart lance l'installateur, quitte, puis relance
        installer?.let(updater::installAndRestart)
    }
    UpdateResult.NotAvailable -> println("À jour")
    is UpdateResult.Error -> println("Erreur : ${result.exception.message}")
}

checkForUpdates() est une fonction suspend ; appelez-la depuis une coroutine.

Formats pris en charge

Le runtime met à jour les formats installés en place et laisse les formats gérés par un store à leur store.

PlateformeMise à jour possibleGéré par le store
macOSDMG, ZIPPKG
WindowsNSIS, NSIS Web, MSIAppX/MSIX
LinuxDEB, RPM, AppImageSnap, Flatpak

macOS a besoin d'un ZIP en plus du DMG

L'updater remplace le .app silencieusement depuis le ZIP ; le DMG ne sert qu'à l'installation initiale. Ajoutez TargetFormat.Zip à côté de TargetFormat.Dmg pour que les deux soient dans le même release — latest-mac.yml référence les deux.

Fonctionnement

Métadonnées

Le plugin écrit un fichier YAML par plateforme, listant chaque installateur avec son SHA-512 et sa taille. Les releases multi-arch ou multi-plateforme ont besoin d'un seul YAML par plateforme qui liste chaque architecture ; la CI les fusionne dans le job de release. Un latest-mac.yml ressemble à ceci :

version: 1.2.3
files:
  - url: MyApp-1.2.3-macos-arm64.dmg
    sha512: VkJl1gDqcBHYbYhMb0HRI...
    size: 102400000
  - url: MyApp-1.2.3-macos-arm64.zip
    sha512: qJ8a5gFDCwv0R2rW6lM3k...
    size: 98000000
releaseDate: '2026-03-01T12:00:00.000Z'

Canaux

Le tag de version pilote le canal : v1.0.0 correspond à latest, v1.0.0-beta.1 à beta, et v1.0.0-alpha.1 à alpha. Chaque canal a son propre *-mac.yml, *.yml et *-linux.yml. Les utilisateurs sur beta voient latest et beta ; ceux sur alpha voient les trois.

Hébergement

L'hébergement se configure dans nativeDistributions.publish { } (voir publication) :

  • GitHub Releases — le workflow de release gère tout de bout en bout.
  • S3 — renseignez les credentials via des variables d'environnement ; le plugin upload à côté du YAML.
  • HTTP générique — upload vous-même sur n'importe quel host statique ; l'updater récupère <baseUrl>/latest-*.yml.

Le bloc publish { } ne fait que générer la config electron-builder — il n'upload pas. C'est la CI qui upload.

Téléchargements différentiels

Depuis 2.3, downloadUpdate tente un téléchargement différentiel avant un fetch complet. Il aligne les block maps electron-builder du nouvel artefact sur une copie en cache de l'ancien, copie les blocs inchangés depuis le disque, et récupère le reste par requêtes HTTP range. Les blocs sont appariés par contenu : un saut 1.0 → 1.7 est donc aussi différentiel — aucun fichier delta pair-wise n'est produit.

Le plugin Gradle Nucleus publie déjà les maps dont le runtime a besoin :

FormatBlock map
NSIS, ZIP / DMG macOSCompanion gzippé autonome <artifact>.blockmap à côté de l'installeur
AppImageMap appendée dans le trailer AppImage (blockMapSize dans latest-linux.yml)

Comportement :

  • AppImage — l'exécutable en cours est l'artefact précédent, donc la première mise à jour peut déjà être différentielle.
  • Autres formats — l'updater met en cache chaque téléchargement réussi (et sa map) sous cacheDir (défaut %LOCALAPPDATA%/nucleus/updates/<appId> sous Windows, ~/.cache/nucleus/updates/<appId> ailleurs). La deuxième mise à jour devient différentielle.
  • Map manquante, échec de range request ou mismatch de digest → repli silencieux sur un téléchargement complet.
val updater = NucleusUpdater {
    provider = GitHubProvider(owner = "myorg", repo = "myapp")
    differentialDownload = true   // défaut
    // cacheDir = File("/var/cache/myapp/updates")  // override optionnel
}

updater.downloadUpdate(info).collect { progress ->
    val kind = if (progress.isDifferential) "différentiel" else "complet"
    println(
        "${progress.percent.toInt()}% " +
            "(${progress.bytesDownloaded}/${progress.totalBytes}, $kind)",
    )
    progress.file?.let { installer = it }
}

DownloadProgress.bytesDownloaded / totalBytes comptent les octets transférés : en différentiel, totalBytes est plus petit que la taille de l'artefact et percent suit le temps d'attente. isDifferential vaut true quand le chemin différentiel a abouti.

Mettez differentialDownload = false pour forcer les téléchargements complets (CDN sans support des ranges HTTP, par exemple).

Détection après mise à jour

Après installAndRestart ou installAndQuit, l'updater écrit un fichier marqueur dans le répertoire app-data de la plateforme (résolu depuis NucleusApp.appId). Au lancement suivant, wasJustUpdated() retourne true et consumeUpdateEvent() livre un UpdateEvent(previousVersion, newVersion, updateLevel) que vous pouvez utiliser pour une bannière « Quoi de neuf ».

Configurer les réseaux d'entreprise

Si vos utilisateurs sont derrière un proxy d'entreprise avec une CA racine privée, le java.net.http.HttpClient par défaut échoue au handshake TLS. Injectez un NativeHttpClient qui lit le trust store de l'OS :

import dev.nucleusframework.nativehttp.NativeHttpClient

val updater = NucleusUpdater {
    provider = GitHubProvider(owner = "myorg", repo = "myapp")
    httpClient = NativeHttpClient.create()
}

Voir native-ssl pour les détails du trust manager.

Mettre en place les mises à jour sans mot de passe sur Linux

Par défaut, appliquer une mise à jour DEB ou RPM passe par pkexec, qui affiche un dialogue de mot de passe PolicyKit. Vous pouvez laisser l'app installer les mises à jour vérifiées sans mot de passe, en conditionnant l'install à une vérification de signature.

Ça s'appuie sur la signature Linux. Activez-le sur le bloc signing :

build.gradle.kts
linux {
    signing {
        enabled.set(true)
        keyId.set("AB12CD34EF56")
        silentUpdate.set(true)   // nécessite enabled = true
    }
}

Le payload du paquet et les scripts post-install mettent en place les mises à jour sans mot de passe ainsi :

  1. Un helper de mise à jour livré dans le paquet en /opt/<App>/nucleus-update-helper (pour que dpkg -S / rpm -qf l'attribuent au paquet). afterInstall ne fait que durcir les permissions et installer une policy polkit scopée à ce binaire (allow_active=yes) ; afterRemove retire la policy.
  2. Votre clé publique de signature, bundlée dans /opt/<App>/resources/nucleus-update.pub.asc.
  3. Au moment de la mise à jour, le runtime télécharge le paquet et sa signature détachée <file>.asc (la CI upload les *.asc à côté des paquets quand les secrets GPG Linux sont présents), résout le helper en remontant depuis bin/, puis lance pkexec <helper> <file>.
  4. Le helper vérifie la signature contre la clé bundlée, contrôle que le nom du paquet correspond à l'app installée, et seulement alors lance dpkg -i ou rpm -U.

Les mises à jour AppImage remplacent le binaire en place pendant que l'ancien mount FUSE tient encore l'inode, attendent la sortie de l'ancien processus (verrou single-instance), et relancent avec un environnement nettoyé (APPDIR, APPIMAGE, LD_LIBRARY_PATH, … retirés) pour ne pas lier la nouvelle image au squashfs démonté.

La règle polkit allow_active=yes est scopée au seul binaire helper, et le helper refuse tout paquet dont la signature ne valide pas contre la clé bundlée : une install sans mot de passe ne peut donc pas installer un paquet arbitraire — il faudrait votre clé privée de signature. Si le helper ou le <file>.asc est absent, le runtime retombe sur l'install pkexec avec prompt de mot de passe et journalise la raison. L'install sans mot de passe nécessite une session locale active.

Référence de l'API

Méthodes runtime

NucleusUpdater expose :

  • suspend fun checkForUpdates(): UpdateResult — retourne Available(info, level), NotAvailable ou Error(exception).
  • fun downloadUpdate(info: UpdateInfo): Flow<DownloadProgress> — émet la progression ; l'émission finale a file != null.
  • fun installAndRestart(file: File) — lance l'installateur, quitte le processus, relance après installation.
  • fun installAndQuit(file: File) — installation silencieuse, pas de relance. La mise à jour s'applique au prochain lancement manuel.
  • fun isUpdateSupported(): Boolean — indique si le type d'exécutable courant peut s'auto-mettre à jour.
  • fun wasJustUpdated(): Boolean et fun consumeUpdateEvent(): UpdateEvent? — détection après mise à jour.

Types de résultat et de niveau

  • UpdateResult.Available(info: UpdateInfo, level: UpdateLevel)
  • UpdateResult.NotAvailable
  • UpdateResult.Error(exception: UpdateException)

UpdateLevel indique l'ampleur du saut : MAJOR, MINOR, PATCH ou PRE_RELEASE. Branchez votre UI dessus — dialogue forcé pour les bumps majeurs, installation silencieuse pour les patches.

Champs du builder

NucleusUpdater { } configure un UpdaterConfig :

ChampDéfautNotes
currentVersiondepuis NucleusApp / jpackage / markerUtilisé pour le canal et la comparaison
providerrequisGitHubProvider, GenericProvider, …
channel"latest"Nom du canal de release
allowDowngradefalse
allowPrereleasefalseAussi true si currentVersion contient -
executableTypeautoOverride du type de paquet détecté
httpClientclient JDK avec redirectionsUtilisez NativeHttpClient pour les CA privées
differentialDownloadtrueAssemble depuis le cache + range requests ; sûr de laisser à true
cacheDirdéfaut plateforme sous nucleus/updates/<appId>Artefact précédent + block map

Providers

  • GitHubProvider(owner, repo, token? = null)
  • GenericProvider(baseUrl)

Les en-têtes d'authentification personnalisés proviennent de authHeaders() du provider, pas de la config.

Commandes d'installation

OSFormatCommande
LinuxDEBpkexec dpkg -i <file>
LinuxRPMpkexec rpm -U <file>
LinuxAppImageremplacement sur place
macOSDMG/ZIPopen <file> / extraction
WindowsNSIS/EXE<file> /S
WindowsMSImsiexec /i <file> /passive

Exceptions

UpdateException est le type de base. Sous-types : NetworkException, ChecksumException, NoMatchingFileException, ParseException.

Notes

  • Les SHA-512 sont vérifiés après téléchargement ; un échec supprime le fichier et remonte une erreur.
  • Les mises à jour Linux sans mot de passe (signing.silentUpdate) ajoutent une deuxième vérification, plus forte : une signature GPG validée contre la clé bundlée dans l'app.
  • Les tokens GitHub passent par l'en-tête Authorization, jamais dans les paramètres d'URL.
  • Pour le Mac App Store, le Microsoft Store, Snapcraft et Flathub, le store gère les mises à jour ; ignorez updater-runtime entièrement.

Et ensuite

  • Publication — configurer les providers dans nativeDistributions.publish { }.
  • CI/CD — le workflow de release qui génère et uploade les métadonnées.
  • Signature de code — signature GPG des DEB/RPM et mises à jour sans mot de passe.