# 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--linux-.tar.gz` | `CloudRestoreAS/` → binario + `install.sh` + `packaging/linux/cloudrestoreas.service` + `LEEME.txt` | | `CloudRestoreAS--win-.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//` 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--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 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--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: 1. **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 en `SINGLE_USER`. 2. **Se respalda el binario anterior** antes de pisarlo (`.CloudRestoreAS.exe.prev`). 3. **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: ```powershell 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.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 (/)` 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--*.{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) |