Hay entornos donde no se usa root en absoluto, así que ni `sudo -n` ni una cuenta root son opciones. Este modo actualiza una instalación existente dejando su unit de systemd intacto, que es lo único que una actualización necesita de verdad: reemplazar el binario y reiniciar el proceso. Se apoya en dos hechos, uno de ellos contrario a lo que decía el propio repo: - `install` NO sufre ETXTBSY. A diferencia de `cp` —que abre con O_TRUNC—, desvincula el destino antes de crearlo, y por eso `make install` funciona sobre binarios en ejecución. Comprobado: `cp` sobre un ELF corriendo da "Text file busy" y `install` no. La consecuencia es la que importa: reemplazar el binario exige escritura en el DIRECTORIO, no en el archivo. El comentario de install.sh, BUILD.md y el CHANGELOG afirmaban lo contrario y mandaban al operador a diagnosticar un archivo en uso cuando lo que tenía era un EACCES. - El unit corre como el usuario que instaló (cadena SUDO_USER) y trae Restart=always, así que esa cuenta puede señalizar el proceso y systemd lo relevanta con el binario nuevo. No hace falta systemctl ni tocar /etc. Robustez, que es donde estaba el trabajo real: - **Reversible.** Respalda el binario antes de reemplazarlo y, si el nuevo no arranca, lo restaura y confirma que el proceso volvió. Sin esto, una actualización fallida deja el servidor sin agente. Si tampoco puede revertir, conserva el respaldo y lo dice en vez de fingir éxito. - **No interrumpe restauraciones.** El agente no atiende SIGTERM: matarlo a media restauración deja ese respaldo vetado para siempre (has_blocking_job_by_hash) y puede dejar la base en SINGLE_USER. Se comprueba Temp/ dos veces —antes de copiar y otra vez justo antes de señalizar, para cerrar la ventana— y sale con 75 (EX_TEMPFAIL), que el panel traduce a "reintenta luego" y no a un fallo. - **Diagnostica por qué no volvió**: distingue un unit sin Restart=always de un StartLimitBurst agotado, con el comando de recuperación. - Omite el bootstrap de 20 s: es redundante en una actualización (ensure_runtime_layout corre en cada arranque) y una segunda instancia junto a la viva purgaría el Temp de la que está trabajando. Un bug que solo aparecía fuera del camino feliz: con `set -euo pipefail`, un `$(pgrep ... | head -1)` sin resultados hace fallar la sustitución y `set -e` mataba el script en silencio — justo en el caso "el proceso no volvió", que es el que había que manejar. Por eso el rollback no se ejecutaba nunca.
316 lines
14 KiB
Markdown
316 lines
14 KiB
Markdown
# 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.
|
|
|
|
```bash
|
|
./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.sh` en ubuntu:22.04).
|
|
- Windows: **`powershell.exe`** accesible desde WSL + **Python 3.11+** instalado en Windows
|
|
(`build.ps1` lo 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](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](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
|
|
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.
|
|
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
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:
|
|
```bash
|
|
systemctl status cloudrestoreas
|
|
journalctl -u cloudrestoreas -f
|
|
sudo systemctl restart cloudrestoreas # tras editar config/.env
|
|
```
|
|
|
|
### Windows — `install.ps1` (autocontenido, sin NSSM ni descargas)
|
|
|
|
```powershell
|
|
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 # 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.
|
|
|
|
> `scripts/dev-setup.ps1` es otra cosa: prepara el entorno de **desarrollo** (Python, venv,
|
|
> `requirements.txt`) para correr `python 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:
|
|
|
|
```bash
|
|
./install.sh --service --panel-env-file /tmp/panel.env
|
|
```
|
|
```powershell
|
|
.\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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
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) |
|