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
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.
| Plateforme | Mise à jour possible | Géré par le store |
|---|---|---|
| macOS | DMG, ZIP | PKG |
| Windows | NSIS, NSIS Web, MSI | AppX/MSIX |
| Linux | DEB, RPM, AppImage | Snap, 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 :
| Format | Block map |
|---|---|
| NSIS, ZIP / DMG macOS | Companion gzippé autonome <artifact>.blockmap à côté de l'installeur |
| AppImage | Map 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 :
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 :
- Un helper de mise à jour livré dans le paquet en
/opt/<App>/nucleus-update-helper(pour quedpkg -S/rpm -qfl'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. - Votre clé publique de signature, bundlée dans
/opt/<App>/resources/nucleus-update.pub.asc. - 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 depuisbin/, puis lancepkexec <helper> <file>. - 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 -iourpm -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— retourneAvailable(info, level),NotAvailableouError(exception).fun downloadUpdate(info: UpdateInfo): Flow<DownloadProgress>— émet la progression ; l'émission finale afile != 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(): Booleanetfun consumeUpdateEvent(): UpdateEvent?— détection après mise à jour.
Types de résultat et de niveau
UpdateResult.Available(info: UpdateInfo, level: UpdateLevel)UpdateResult.NotAvailableUpdateResult.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 :
| Champ | Défaut | Notes |
|---|---|---|
currentVersion | depuis NucleusApp / jpackage / marker | Utilisé pour le canal et la comparaison |
provider | requis | GitHubProvider, GenericProvider, … |
channel | "latest" | Nom du canal de release |
allowDowngrade | false | |
allowPrerelease | false | Aussi true si currentVersion contient - |
executableType | auto | Override du type de paquet détecté |
httpClient | client JDK avec redirections | Utilisez NativeHttpClient pour les CA privées |
differentialDownload | true | Assemble depuis le cache + range requests ; sûr de laisser à true |
cacheDir | dé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
| OS | Format | Commande |
|---|---|---|
| Linux | DEB | pkexec dpkg -i <file> |
| Linux | RPM | pkexec rpm -U <file> |
| Linux | AppImage | remplacement sur place |
| macOS | DMG/ZIP | open <file> / extraction |
| Windows | NSIS/EXE | <file> /S |
| Windows | MSI | msiexec /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-runtimeentiè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.