Implémenter un système d'auto-update complet pour une application Python

DéploiementPython
← Tous les articles

Architecture d'un auto-update robuste

Un système d'auto-update Python fiable repose sur 5 composants distincts : le checker de version (interroge le serveur), le downloader delta (télécharge seulement les différences), l'applicateur (installe la mise à jour), le launcher (redémarre proprement) et le rollback (restaure si échec). Universal Installer V2 fournit tous ces composants.

Un auto-update qui plante laisse l'application dans un état incohérent. La règle absolue : ne jamais modifier les fichiers actifs en cours d'exécution. Toujours écrire dans un dossier temporaire, puis basculer atomiquement.

Vérification de version

from universal_installer.updater import UpdateChecker
from packaging.version import Version

checker = UpdateChecker(
    manifest_url="https://cdn.exemple.fr/releases/manifest.json",
    public_key_path="keys/public.pem",
    current_version="1.1.0"
)

update_info = checker.check()

if update_info.available:
    print(f"Mise à jour disponible : {update_info.version}")
    print(f"Taille patch : {update_info.patch_size_mb:.1f} MB")
    print(f"Taille complète : {update_info.full_size_mb:.1f} MB")
    print(f"Changelog : {update_info.changelog_url}")
else:
    print("Application à jour.")

Téléchargement delta

from universal_installer.updater import DeltaDownloader
import threading

downloader = DeltaDownloader(update_info)

# Téléchargement en arrière-plan avec callback de progression
def on_progress(downloaded_mb, total_mb, speed_mbps):
    pct = int(downloaded_mb / total_mb * 100)
    update_ui_progress(pct, f"{speed_mbps:.1f} MB/s")

thread = threading.Thread(
    target=downloader.download,
    kwargs={{
        'output_path': 'tmp/update.uxcd',
        'progress_callback': on_progress,
        'verify_signature': True,  # ECDSA P-384
        'resume': True             # reprise si interrompu
    }}
)
thread.start()
# L'application continue à fonctionner pendant le téléchargement

Application de la mise à jour

from universal_installer.updater import UpdateApplicator

applicator = UpdateApplicator(
    current_dir=r"C:\MonApp",
    backup_dir=r"C:\MonApp_backup",
    patch_path="tmp/update.uxcd"
)

# L'applicateur :
# 1. Sauvegarde current_dir → backup_dir
# 2. Extrait le patch dans un dossier tmp
# 3. Fusionne les fichiers modifiés avec la version actuelle
# 4. Vérifie le SHA-256 de chaque fichier mis à jour
# 5. Valide l'intégrité globale de la nouvelle version
result = applicator.apply()

if not result.success:
    print(f"Échec : {result.error}")
    applicator.rollback()  # restaure backup automatiquement

Redémarrage propre

Impossible de remplacer un EXE en cours d'exécution sur Windows. La technique : lancer un process updater séparé qui attend la fermeture de l'application principale, remplace les fichiers, puis relance :

import subprocess, sys, os
from pathlib import Path

def schedule_restart_update(patch_path: str):
    updater_exe = Path(sys.executable).parent / "ui-updater.exe"

    # Lancer l'updater qui attend PID courant + applique + relance
    subprocess.Popen([
        str(updater_exe),
        "--wait-pid", str(os.getpid()),
        "--patch", patch_path,
        "--app", sys.executable,
        "--backup", r"C:\MonApp_backup"
    ], creationflags=subprocess.DETACHED_PROCESS)

    # Fermer l'application principale proprement
    save_application_state()
    sys.exit(0)

Rollback automatique

# Dans ui-updater.exe — après application du patch
def verify_update_and_launch(app_path: str, backup_path: str) -> bool:
    # Lancer la nouvelle version avec timeout de démarrage
    proc = subprocess.Popen([app_path, "--post-update-check"])
    try:
        proc.wait(timeout=30)  # 30s pour confirmer le démarrage
        if proc.returncode == 0:
            shutil.rmtree(backup_path)  # Succès → supprimer le backup
            return True
    except subprocess.TimeoutExpired:
        proc.kill()

    # Échec → rollback
    shutil.rmtree(Path(app_path).parent)
    shutil.copytree(backup_path, Path(app_path).parent)
    subprocess.Popen([backup_path])  # relancer l'ancienne version
    return False

UX de mise à jour

Conclusion

Un auto-update Python robuste requiert trois éléments non négociables : le téléchargement en arrière-plan sans bloquer l'UI, l'écriture dans un dossier temporaire avant de basculer (jamais en place), et le rollback automatique si la nouvelle version ne démarre pas. Universal Installer V2 fournit tous ces composants prêts à l'emploi avec vérification ECDSA et delta UXC.

📦
PRODUIT LIÉ
Universal Installer V2.0
Découvrir V2 → Manuel complet
← Article précédent Article suivant →