Apps que se actualizan solas
pyzed.updater permite que una app hecha con pyzed build se actualice sola desde las GitHub Releases de su
repositorio: busca la versión más nueva, descarga la compilación para este sistema, la verifica y cambia la carpeta de
la app por la nueva cuando la app se reinicia. Pyzed Design se actualiza con este mismo módulo.
sequenceDiagram
participant App as Tu app (v1.0.0)
participant GH as GitHub Releases
App->>GH: latest("acme/shop")
GH-->>App: v1.1.0 + shop-windows-x86_64.zip (y su SHA-256)
App->>GH: download(asset)
Note over App: verifica el SHA-256, descomprime el zip
App->>App: replace_on_exit(carpeta nueva) y se cierra
Note over App: un pequeño script espera, cambia las carpetas y arranca la v1.1.0
Qué debe contener una versión
Sección titulada «Qué debe contener una versión»-
Una etiqueta de versión como
v1.1.0(versiones preliminares:v1.2.0-rc.1,v1.2.0-beta.2…). -
Un zip por sistema, llamado
<app>-<sistema>-<arq>.zip:Sistema Ejemplo Windows shop-windows-x86_64.zipmacOS shop-macos-arm64.zip,shop-macos-x86_64.zipLinux shop-linux-x86_64.zip,shop-linux-aarch64.zipupdater.asset_name("shop")devuelve el nombre para la máquina donde se ejecuta. -
Dentro de cada zip, exactamente una entrada: la carpeta de la app tal como la creó
pyzed build(Shop/), oShop.appen macOS (comprímela conditto -c -k --keepParentpara conservar la firma del paquete).
La página de CI tiene un flujo que compila y sube estos zip.
Buscar, descargar, reiniciar
Sección titulada «Buscar, descargar, reiniciar»Las funciones bloquean mientras hablan con la red, así que ejecútalas fuera del hilo de la interfaz, por ejemplo con
asyncio.to_thread en una corrutina (mira Async). Una app completa:
import asyncioimport os
import pyzed as pzfrom pyzed import ui, updater
__version__ = "1.0.0" # mantenla igual que en pyproject.tomlREPO = "acme/shop" # el repositorio de GitHub con las versionesAPP = "shop" # la parte <app> de los nombres de los zip
class Shop(pz.App): title = "Shop"
def __init__(self): self.release = None # una versión más nueva, cuando la hay self.ready = None # la versión nueva descargada, lista para cambiarla self.status = ""
def on_start(self): if updater.is_frozen(): # solo una app empaquetada puede reemplazarse return self.check()
async def check(self): try: release = await asyncio.to_thread(updater.latest, REPO) except updater.UpdateError as err: return ("status", f"No se pudo buscar actualizaciones: {err}") if updater.is_newer(release.version, __version__) and release.asset_for(updater.asset_name(APP)): return ("found", release) return ("status", "Tienes la última versión.")
async def download(self): asset = self.release.asset_for(updater.asset_name(APP)) self.status = f"Descargando {self.release.version}…" try: archive = await asyncio.to_thread(updater.download, asset) bundle = await asyncio.to_thread(updater.extract, archive) except updater.UpdateError as err: return ("status", f"La actualización falló: {err}") return ("ready", bundle)
def restart(self): updater.replace_on_exit(self.ready) # cambia las carpetas cuando este proceso termina... os._exit(0) # ...así que termínalo ya; la versión nueva arranca sola
def update(self, message): kind, value = message if kind == "found": self.release, self.status = value, f"La versión {value.version} está disponible." elif kind == "ready": self.ready, self.status = value, "Reinicia para terminar la actualización." else: self.status = value
def view(self): acciones = [] if self.ready: acciones.append(ui.button("Reiniciar para actualizar", self.restart, variant="solid")) elif self.release: acciones.append(ui.button("Descargar e instalar", self.download, variant="solid")) return pz.column([ui.heading("Shop"), ui.muted(f"Versión {__version__}"), ui.label(self.status), *acciones], spacing=10, padding=24)Las funciones
Sección titulada «Las funciones»| Función | Qué hace |
|---|---|
latest(repo, prerelease=False) |
La Release más nueva de "dueño/nombre". Con prerelease=True también cuentan las preliminares (un “canal preview”). |
release(repo, version) |
La versión indicada ("1.1.0" o "v1.1.0"). |
is_newer(candidata, actual) |
Compara versiones; una versión final es más nueva que sus preliminares (1.2.0 > 1.2.0-rc.1). |
asset_name(app) |
"<app>-<sistema>-<arq>.zip" para esta máquina. |
Release.asset_for(nombre) |
El archivo de la versión con ese nombre, o None si la versión no tiene compilación para este sistema. |
download(asset, folder=None, progress=None) |
Descarga en folder (una carpeta temporal si no se indica), verifica el SHA-256 y devuelve la ruta. progress recibe la fracción completada, de 0 a 1. Si folder ya tiene el archivo con el SHA-256 correcto, lo reutiliza. |
extract(archivo) |
Descomprime el zip y devuelve la app que contiene. |
replace_on_exit(bundle, relaunch=True, args=(), keep=()) |
Arranca un pequeño script que espera a que la app se cierre, cambia la carpeta vieja por bundle (restaurando la vieja si falla) y arranca la versión nueva. keep lista archivos a conservar, como el desinstalador de un instalador ("unins*"). Para antes los procesos hijos de la app: en Windows una carpeta en uso no se puede mover (el script reintenta unos 30 segundos y luego vuelve a abrir la versión vieja). |
is_frozen() |
Si es una app empaquetada. |
install_dir() |
La carpeta (o .app) de la app en ejecución, None si corre desde Python. |
can_update_in_place() |
Si la app está empaquetada y su carpeta admite escritura, para que replace_on_exit funcione. |
Una Release tiene version, tag, notes (las notas de la versión en Markdown: muéstralas con
ui.markdown), url, published, prerelease y assets. Los errores son
UpdateError con un mensaje legible (“could not reach GitHub”, “rate limit”…): los problemas de red nunca cierran la
app.
A tener en cuenta
Sección titulada «A tener en cuenta»- Importa dónde vive la app. La app solo puede reemplazar una carpeta en la que puede escribir. Comprueba
updater.can_update_in_place()antes de ofrecer la actualización; si esFalse(por ejemplo una app enC:\Program Files), manda a la gente a la página de descarga. Las instalaciones por usuario (%LOCALAPPDATA%\Programsen Windows,~/.local/shareen Linux,/Applicationspara administradores en macOS) funcionan. - Solo compilaciones en carpeta. Una compilación
--onefileno puede actualizarse así. - Las apps firmadas siguen firmadas en macOS si el zip se creó con
dittoa partir de una.appfirmada (y notarizada). - La API de GitHub tiene límite de 60 peticiones por hora por IP sin token: busca una vez al día, no en cada arranque.