Esa ruta es el peor caso posible y existe en produccion: la ruta por omision C:\Aduanasoft\CloudRestoreAS es PREFIJO DE CADENA de ella, asi que cualquier comparacion hecha con startsWith daria por iguales dos instalaciones distintas — y el resultado seria el fallo silencioso otra vez, actualizar una carpeta y arrancar la otra. El flujo ya la manejaba bien (Test-SamePath compara por igualdad exacta tras normalizar), pero nada lo probaba: la emulacion usaba declarada/otra-carpeta, nombres sin relacion entre si, que un startsWith mal puesto pasaria sin problema. - Escenario `sufijo` en emular-actualizacion-windows.ps1: instalacion en ...\CloudRestoreAS-win y tarea apuntando a ...\CloudRestoreAS. Verificado en Windows: reapunta la tarea, el proceso queda corriendo desde -win y el sello en la version nueva. - Nuevo scripts/probar-funciones-install.ps1: extrae las funciones del instalador por AST y las ejercita contra una tarea simulada, sin elevacion. Cubre los casos limite de la comparacion de rutas (el par de prefijo en ambos sentidos, comillas, barra final, mayusculas, `..`, ruta vacia) y que Sync-AgentTaskPath falle cuando no puede corregir. BUILD.md documenta por que se compara por igualdad exacta y no por prefijo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
18 KiB
BUILD.md — Compilación, empaquetado y despliegue
Referencia de todos los comandos, ejecutables y scripts para generar, empaquetar, instalar y verificar CloudRestoreAS en Windows y Linux.
El ejecutable es autocontenido: no requiere Python, 7-Zip, driver ODBC ni librerías Qt instaladas en el equipo destino. Todo se embebe dentro del binario en tiempo de build.
1. Todo en una tarea (recomendado) — build-all.sh
Orquestador único. Se ejecuta desde WSL (bash). Genera Windows + Linux + los paquetes.
./build-all.sh # Windows + Linux + paquetes versionados en dist/release/
./build-all.sh --linux-only # solo Linux (Docker)
./build-all.sh --windows-only # solo Windows (PowerShell + build.ps1)
./build-all.sh --no-package # compila sin generar .tar.gz/.zip
./build-all.sh --clean # rebuild desde cero (borra venvs, bundled, dist)
./build-all.sh --publish # además publica en Gitea (ver §5; requiere GITEA_TOKEN)
./build-all.sh --help
Requisitos:
- Linux: Docker (usa
docker-build-linux.shen ubuntu:22.04). - Windows:
powershell.exeaccesible desde WSL + Python 3.11+ instalado en Windows (build.ps1lo auto-detecta en%LOCALAPPDATA%\Programs\Python\Python31X). - Es fail-soft: si no hay
powershell.exe, compila solo Linux y avisa.
El build de Windows se hace sobre la ruta
\\wsl.localhost\..., por eso tarda (~15-18 min: pip install de PySide6 + PyInstaller). El de Linux (Docker, con deps cacheadas) es rápido.
packaging/scripts/build-all.sh es un wrapper que delega en este mismo script.
2. Builds individuales
| Objetivo | Comando | Salida | Notas |
|---|---|---|---|
| Linux (Docker, recomendado) | bash packaging/scripts/docker-build-linux.sh |
dist/CloudRestoreAS |
ubuntu:22.04 con todas las deps + patchelf; garantiza autocontención |
| Linux (host) | ./build.sh |
dist/CloudRestoreAS |
Requiere que el host tenga las libs Qt/ODBC; usar solo si no hay Docker |
| Windows | .\build.ps1 (en Windows/PowerShell) |
dist\CloudRestoreAS.exe |
Auto-detecta Python 3.11+; crea venv-windows |
Ambos usan el mismo spec: packaging/CloudRestoreAS.spec (PyInstaller onefile, console=False).
3. Dependencias embebidas (build-time)
Se descargan e integran al binario. No se instalan en el destino.
| Script | Qué embebe |
|---|---|
packaging/scripts/download-bundled-deps.sh (Linux) |
7zz (7-Zip Linux), driver MS ODBC 18 + unixODBC + Kerberos/GSSAPI + libltdl + OpenSSL, y cluster Qt xcb/X11 + EGL. Aplica patchelf --set-rpath '$ORIGIN' a las .so del cluster ODBC para que resuelvan entre sí. |
packaging/scripts/download-bundled-deps.ps1 (Windows) |
7z.exe y msodbcsql18.dll. |
Versiones/URLs en packaging/bundled-versions.json.
Los binarios quedan en packaging/bundled/{linux,windows}/ (git-ignored, se generan en el build).
Ubicación en runtime (creada por el bootstrap en la primera ejecución):
config/7zip/7zz— extractor.config/odbc/lib/— driver ODBC + toda su cadena (resuelven por$ORIGIN).- Cluster Qt xcb/EGL — dentro del onefile, en
PySide6/Qt/lib.
4. Empaquetado — package-release.sh
bash packaging/scripts/package-release.sh
La versión sale de app/__init__.py (fuente única) y va en el nombre de cada paquete.
El script aborta si la versión no es puntos-y-números: el PANEL las compara como tuplas
de enteros y otro formato rompería en silencio la detección de "hay versión nueva".
Genera en dist/release/:
| Archivo | Contenido |
|---|---|
CloudRestoreAS-<version>-linux-<arch>.tar.gz |
CloudRestoreAS/ → binario + install.sh + packaging/linux/cloudrestoreas.service + LEEME.txt |
CloudRestoreAS-<version>-win-<arch>.zip |
CloudRestoreAS/ → CloudRestoreAS.exe + install.ps1 + LEEME.txt |
SHA256SUMS |
Checksums de los dos paquetes |
release.json |
Manifiesto: versión, fecha, artefactos (platform/arch/tamaño/sha256) y deps embebidas. Es lo que lee publish-release.sh para saber qué subir |
Además deja copias crudas sin versión (CloudRestoreAS.exe, CloudRestoreAS-linux) para
la verificación de autocontención de la §9. Esas no se publican.
arch se declara con CLOUDRESTORE_TARGET_ARCH (default x86_64): el .exe lo compila
el host Windows y desde WSL no hay forma de inferir su arquitectura.
5. Publicación a Gitea — publish-release.sh
Los binarios viven en el registro de paquetes genéricos de Gitea, que es la fuente de
verdad que consume el PANEL. Gitea calcula y expone el sha256 de cada archivo, así que
no hace falta mantener un manifiesto de integridad propio: el PANEL verifica sus descargas
contra ese hash.
# Requiere un PAT de Gitea con scope write:package
export GITEA_TOKEN=xxxxxxxx
bash packaging/scripts/publish-release.sh --dry-run # lista qué subiría
bash packaging/scripts/publish-release.sh # sube y verifica
bash packaging/scripts/publish-release.sh --notify-panel # y avisa al PANEL
Destino: https://git.aduanasoft.com/api/packages/ADUANASOFT/generic/cloudrestoreas/<version>/
Tras subir, el script relee la API de Gitea y compara el sha256 y el tamaño de cada
artefacto contra release.json. Si no coinciden falla: una publicación a medias no debe
pasar por buena, porque el PANEL rechazaría la descarga por hash y el error aparecería
mucho después, al intentar instalar.
Los paquetes genéricos son inmutables: reintentar la misma versión da HTTP 409. Lo
correcto es subir una versión nueva; --force borra y reemplaza, y solo aplica cuando la
versión anterior nunca se instaló en ningún servidor.
Todo en una sola tarea:
GITEA_TOKEN=xxxx ./build-all.sh --publish --notify-panel
--publish exige el build de ambas plataformas: publicar una versión a la que le falta
una dejaría en el PANEL un release que no se le puede instalar a la mitad de los servidores,
y corregirlo obligaría a quemar el número de versión.
| Variable | Default | Para qué |
|---|---|---|
GITEA_TOKEN |
— | Requerida. PAT con scope write:package |
GITEA_BASE_URL |
https://git.aduanasoft.com |
Instancia de Gitea |
GITEA_OWNER |
ADUANASOFT |
Organización dueña del paquete |
CRAS_PACKAGE |
cloudrestoreas |
Nombre del paquete genérico |
PANEL_API_URL |
— | Solo con --notify-panel |
CLOUDRESTORE_API_TOKEN |
— | Solo con --notify-panel (token de servicio del PANEL) |
Siguiente paso, en el PANEL: /versiones-cras → Sincronizar con Gitea → Activar, y de ahí Instalar / Actualizar por servidor.
6. Instalación / despliegue
Lo normal es que el PANEL instale por SSH desde /versiones-cras, sembrando además las
credenciales. Lo de abajo es el camino manual y lo que el PANEL ejecuta por dentro.
Linux — install.sh (no instala nada del sistema)
tar xzf CloudRestoreAS-<version>-linux-x86_64.tar.gz && cd CloudRestoreAS
sudo ./install.sh --service # servicio systemd 24/7 headless (recomendado en servidor)
./install.sh --user-service # 24/7 SIN privilegios: unit de systemd de usuario
./install.sh --update-in-place # actualiza una instalación existente SIN privilegios
./install.sh --desktop # autostart .desktop (requiere sesión gráfica)
./install.sh # solo instala + bootstrap; lo corres a mano
./install.sh --help
Variables: PREFIX=/opt/cloudrestoreas (destino), SERVICE_USER=<usuario> (usuario del servicio).
Detiene el servicio antes de reemplazar el binario en los modos de servicio, para que el apagado
sea ordenado. No porque la copia lo exija: install desvincula el destino antes de crearlo —a
diferencia de cp, que abre con O_TRUNC y sí da ETXTBSY—, y por eso make install funciona
sobre binarios en ejecución. Comprobado. Lo que hace falta para reemplazar el binario es permiso de
escritura en el directorio, no en el archivo. (Esta nota decía lo contrario y mandó a más de
uno por la pista equivocada al diagnosticar un EACCES.)
Sin privilegios
--user-service instala bajo el home con un unit de systemd de usuario (lingering, y si el
destino no lo permite, @reboot en el crontab del usuario más un vigilante). Sirve para
instalaciones nuevas donde nunca vas a tener root.
--update-in-place actualiza una instalación que ya existe, dejando su unit intacto: solo
reemplaza el binario y señaliza al proceso para que Restart=always lo relevante. Exige que el
directorio sea escribible por la cuenta, que el unit corra con ese mismo usuario y que tenga
Restart=always. Se niega si hay una restauración en curso (sale con 75, EX_TEMPFAIL) y
revierte al binario anterior si el nuevo no arranca.
Servicio systemd:
systemctl status cloudrestoreas
journalctl -u cloudrestoreas -f
sudo systemctl restart cloudrestoreas # tras editar config/.env
Windows — install.ps1 (autocontenido, sin NSSM ni descargas)
Expand-Archive CloudRestoreAS-<version>-win-x86_64.zip -DestinationPath .
cd CloudRestoreAS
.\install.ps1 -Service # tarea programada ONSTART como SYSTEM (24/7 headless)
.\install.ps1 -Desktop # arranque al iniciar sesión (tarea ONLOGON de la app)
.\install.ps1 -UpdateInPlace # actualiza una instalación existente, conservando su tarea
.\install.ps1 # solo instala + bootstrap
Get-Help .\install.ps1 -Detailed
Parámetros: -Prefix (default C:\Aduanasoft\CloudRestoreAS), -PanelEnvFile.
-Service requiere PowerShell como Administrador (la tarea corre como SYSTEM). El
arranque 24/7 se resuelve con una tarea programada, no con NSSM: descargarlo violaría la
regla de que en el servidor destino no se instala ni se baja nada.
Si la cuenta pertenece a Administradores y aun así se rechaza, el mensaje lo dice explícitamente:
es el token filtrado por UAC, que es lo que recibe una sesión de OpenSSH. No se arregla
cambiando de cuenta sino con LocalAccountTokenFilterPolicy=1 (DWORD) en
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System.
Garantías al reemplazar el binario (las mismas en los dos instaladores)
Reemplazar el binario de un servidor en producción no puede dejarlo sin restaurador:
- No se actúa si hay una restauración en curso (
Temp\no vacío): se sale con 75 (EX_TEMPFAIL), que el PANEL traduce a "reintenta luego" y no a "falló la instalación". Interrumpirla dejaría ese respaldo vetado en cada escaneo posterior y la base enSINGLE_USER. - Se respalda el binario anterior antes de pisarlo (
.CloudRestoreAS.exe.prev). - Se confirma que el agente volvió a arrancar y, si no, se revierte al binario anterior. El respaldo solo se descarta tras esa confirmación.
La tarea programada tiene que apuntar a lo que se instaló
Reemplazar el binario es una operación por ruta; Start-ScheduledTask es por nombre y
ejecuta la ruta que la tarea lleva registrada en su acción. Cuando las dos no coinciden —una
instalación fuera de la carpeta por omisión, o movida de sitio— se copiaba el binario nuevo en un
lado y se arrancaba el viejo del otro: el run terminaba en verde y el servidor seguía igual.
install.ps1 compara la acción de la tarea con el -Prefix y la reapunta si difieren,
conservando disparador, principal, ajustes y argumentos. Si no puede corregirla, falla: arrancar a
sabiendas el binario anterior es peor que abortar. Y la confirmación de arranque mira la ruta
del proceso, no solo su nombre — un agente viejo que nunca se detuvo satisface igual de bien un
Get-Process -Name CloudRestoreAS.
Rutas de instalación personalizadas
C:\Aduanasoft\CloudRestoreAS-win es el caso a tener presente, y existe en producción: la ruta por
omisión C:\Aduanasoft\CloudRestoreAS es prefijo de cadena de ella. Por eso las rutas se
comparan por igualdad exacta tras normalizar (comillas, barra final, mayúsculas) y nunca con
startsWith — que daría por iguales dos instalaciones distintas. En el panel eso vive en un solo
sitio, sameWindowsPath(); en el instalador, en Test-SamePath.
Para reproducirlo y comprobarlo sin un servidor, desde WSL o Windows:
scripts\emular-actualizacion-windows.ps1 -Installer .\install.ps1 # caso roto
scripts\emular-actualizacion-windows.ps1 -Installer .\install.ps1 -Escenario alineada # caso normal
scripts\emular-actualizacion-windows.ps1 -Installer .\install.ps1 -Escenario sufijo # ...-win
scripts\probar-funciones-install.ps1 # casos límite
La emulación monta un agente falso (un .exe real que se queda vivo), una instalación en una
carpeta y una tarea apuntando a otra, corre el instalador y dice si la actualización surtió efecto.
probar-funciones-install.ps1 extrae las funciones del instalador por AST y las ejercita contra una
tarea simulada. Ninguno de los dos necesita elevación ni toca la instalación real de la máquina.
-UpdateInPlace además no vuelve a registrar la tarea (así no pisa ajustes hechos sobre ella) y
se salta el bootstrap: una segunda instancia purgaría el Temp\ de la que está viva. Es el modo
que usa el PANEL para actualizar.
El arranque headless no depende del entorno de la tarea: --headless hace que el binario elija
el plugin Qt offscreen en cualquier plataforma, que es lo que le permite correr como SYSTEM en
la sesión 0, donde no hay escritorio interactivo.
scripts/dev-setup.ps1es otra cosa: prepara el entorno de desarrollo (Python, venv,requirements.txt) para correrpython runner.py. No sirve para desplegar el binario.
Siembra de credenciales del PANEL
Ambos instaladores aceptan un archivo KEY=valor con las claves CLOUDRESTORE_PANEL_*,
que fusionan en config/.env (replace-or-append, idempotente, con lista blanca) y luego
borran:
./install.sh --service --panel-env-file /tmp/panel.env
.\install.ps1 -Service -PanelEnvFile C:\Temp\panel.env
Va por archivo y no por argumentos a propósito: un token en la línea de comandos queda
visible en ps y en el historial del servidor destino.
7. Ejecución manual y flags del binario
# Linux servidor sin pantalla (headless): motor de restauración sin GUI
QT_QPA_PLATFORM=offscreen ./CloudRestoreAS --start-engine --headless
# Linux con escritorio o Windows: abre la GUI normal
./CloudRestoreAS
| Flag / variable | Efecto |
|---|---|
--start-engine |
Inicia el motor de restauración al arrancar |
--headless |
Fuerza modo sin interfaz (Qt offscreen), para servidores sin display |
--minimized |
Inicia minimizado en la bandeja |
--version |
Imprime CloudRestoreAS <version> (<platform>/<arch>) y termina. En Windows requiere consola del padre (el .exe es console=False); el instalador remoto prefiere leer config/.version |
QT_QPA_PLATFORM=offscreen |
Plataforma Qt sin display (el binario ya cae a esto automáticamente si no hay DISPLAY/WAYLAND_DISPLAY en Linux) |
En Linux sin DISPLAY, el binario selecciona offscreen solo; con display usa xcb.
8. Configuración (primera ejecución)
El binario crea automáticamente: config/, config/.env, Entrada/, Procesados/,
Fallados/, Temp/. Editar config/.env:
CLOUDRESTORE_AUTO_START=true # el motor arranca solo
CLOUDRESTORE_PANEL_API_URL=... # servicio PANEL_BASES_ANEXO24 (enrutamiento)
CLOUDRESTORE_PANEL_API_TOKEN=...
CLOUDRESTORE_PANEL_INSTANCE_KEY=...
El PANEL entrega el servidor SQL destino y su data_folder. Con SQL Server sobre Linux
serán rutas POSIX (p. ej. /var/opt/mssql/data); el RESTORE ... MOVE adapta el separador
automáticamente según el formato del data_folder.
9. Verificación de autocontención (contenedor pelado)
Confirma que el binario Linux corre sin instalar NADA del sistema:
docker run --rm -v "$PWD/dist:/dist:ro" debian:12-slim bash -c '
cp /dist/CloudRestoreAS /root/app && cd /root
QT_QPA_PLATFORM=offscreen timeout 12 ./app --headless --start-engine 2>&1 | \
grep -iE "offscreen|Ventana principal|Traceback|platform plugin"
# ODBC: driver + cadena resuelven por $ORIGIN (debe dar 0)
ldd /root/config/odbc/lib/libmsodbcsql-18*.so* 2>&1 | grep -c "not found"
# 7-Zip embebido
/root/config/7zip/7zz | head -1
'
Esperado: arranca en offscreen sin errores Qt, 0 deps ODBC faltantes, 7zz ejecuta.
10. Artefactos y ubicaciones
| Ruta | Qué es |
|---|---|
dist/CloudRestoreAS |
Binario Linux onefile |
dist/CloudRestoreAS.exe |
Ejecutable Windows onefile |
dist/release/CloudRestoreAS-<version>-*.{tar.gz,zip} |
Paquetes de despliegue publicables (binario + instalador + docs) |
dist/release/release.json |
Manifiesto de la versión (lo consume publish-release.sh) |
dist/release/SHA256SUMS |
Checksums de los paquetes |
config/.version |
Sello de la versión que corrió (lo lee el instalador remoto por SFTP) |
config/.bundled_deps |
Sello del bundled-versions.json desplegado; si cambia, se re-copian config/7zip y config/odbc |
packaging/bundled/{linux,windows}/ |
Deps embebidas (generadas en build; git-ignored) |
venv-linux/, venv-windows/ |
Entornos virtuales de build (git-ignored) |
11. Scripts de referencia rápida
| Script | Propósito |
|---|---|
build-all.sh |
Todo en uno (Windows + Linux + paquetes) desde WSL |
build.sh / build.ps1 |
Build individual Linux / Windows |
packaging/scripts/docker-build-linux.sh |
Build Linux en contenedor controlado |
packaging/scripts/download-bundled-deps.sh / .ps1 |
Descarga+embebe deps |
packaging/scripts/package-release.sh |
Genera los paquetes versionados + SHA256SUMS + release.json |
packaging/scripts/publish-release.sh |
Publica en los paquetes genéricos de Gitea y verifica el sha256 |
install.sh / install.ps1 |
Instalador de despliegue Linux / Windows (autocontenidos) |
scripts/dev-setup.ps1 |
Entorno de desarrollo en Windows (Python + venv). No sirve para desplegar |
packaging/linux/cloudrestoreas.service |
Unit systemd (24/7 headless) |