feature/generador-instaladores-linux-windows
This commit is contained in:
191
BUILD.md
Normal file
191
BUILD.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# 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 + dist/release/*.tar.gz y *.zip
|
||||||
|
./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 --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
|
||||||
|
```
|
||||||
|
|
||||||
|
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` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Instalación / despliegue
|
||||||
|
|
||||||
|
### Linux — `install.sh` (no instala nada del sistema)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xzf CloudRestoreAS-linux.tar.gz && cd CloudRestoreAS
|
||||||
|
|
||||||
|
sudo ./install.sh --service # servicio systemd 24/7 headless (recomendado en servidor)
|
||||||
|
./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).
|
||||||
|
|
||||||
|
Servicio systemd:
|
||||||
|
```bash
|
||||||
|
systemctl status cloudrestoreas
|
||||||
|
journalctl -u cloudrestoreas -f
|
||||||
|
sudo systemctl restart cloudrestoreas # tras editar config/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Copiar `CloudRestoreAS.exe` a una carpeta y ejecutarlo (o usar `install.ps1`). Al iniciar
|
||||||
|
crea `config/` y un icono en la bandeja. Ver [packaging/LEEME.txt](packaging/LEEME.txt).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 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 |
|
||||||
|
| `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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Artefactos y ubicaciones
|
||||||
|
|
||||||
|
| Ruta | Qué es |
|
||||||
|
|---|---|
|
||||||
|
| `dist/CloudRestoreAS` | Binario Linux onefile |
|
||||||
|
| `dist/CloudRestoreAS.exe` | Ejecutable Windows onefile |
|
||||||
|
| `dist/release/*.tar.gz` / `*.zip` | Paquetes de despliegue (binario + instalador + docs) |
|
||||||
|
| `packaging/bundled/{linux,windows}/` | Deps embebidas (generadas en build; git-ignored) |
|
||||||
|
| `venv-linux/`, `venv-windows/` | Entornos virtuales de build (git-ignored) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 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 `.tar.gz` / `.zip` |
|
||||||
|
| `install.sh` / `install.ps1` | Instalador Linux / Windows |
|
||||||
|
| `packaging/linux/cloudrestoreas.service` | Unit systemd (24/7 headless) |
|
||||||
44
README.md
44
README.md
@@ -24,6 +24,9 @@ CloudRestoreAS es una aplicación Windows de escritorio desarrollada en Python 3
|
|||||||
### Sistema Operativo
|
### Sistema Operativo
|
||||||
- Windows 10/11
|
- Windows 10/11
|
||||||
- Windows Server 2019/2022 o superior
|
- Windows Server 2019/2022 o superior
|
||||||
|
- Linux x86-64 (servidor o escritorio). En servidor **headless** (sin pantalla) corre
|
||||||
|
automáticamente en modo Qt `offscreen`. El binario Linux es **autocontenido**: no
|
||||||
|
requiere instalar Python, 7-Zip, driver ODBC ni librerías Qt en el destino.
|
||||||
|
|
||||||
### Software Requerido
|
### Software Requerido
|
||||||
|
|
||||||
@@ -75,25 +78,46 @@ CloudRestoreAS es una aplicación Windows de escritorio desarrollada en Python 3
|
|||||||
|
|
||||||
### Opción 2: Ejecutable portable (recomendado para producción)
|
### Opción 2: Ejecutable portable (recomendado para producción)
|
||||||
|
|
||||||
**Windows** — generar:
|
**Todo de una vez (desde WSL)** — genera Windows + Linux + los paquetes de release:
|
||||||
```powershell
|
|
||||||
.\build.ps1
|
|
||||||
```
|
|
||||||
Salida: `dist\CloudRestoreAS.exe`
|
|
||||||
|
|
||||||
**Linux** — generar:
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh
|
./build-all.sh # ambos + dist/release/*.tar.gz y *.zip
|
||||||
|
./build-all.sh --linux-only # solo Linux (Docker)
|
||||||
|
./build-all.sh --windows-only # solo Windows (PowerShell + build.ps1)
|
||||||
|
./build-all.sh --clean # rebuild desde cero
|
||||||
```
|
```
|
||||||
Salida: `dist/CloudRestoreAS`
|
Requiere Docker (para Linux) y, para Windows, `powershell.exe` accesible desde WSL con Python 3.11+ instalado en Windows.
|
||||||
|
|
||||||
**Desplegar en cada equipo** (sin Python, sin instalador):
|
**Solo Windows** — generar: `.\build.ps1` → `dist\CloudRestoreAS.exe`
|
||||||
|
**Solo Linux** — generar: `./build.sh` (o `packaging/scripts/docker-build-linux.sh` para el entorno controlado) → `dist/CloudRestoreAS`
|
||||||
|
|
||||||
|
> 📦 Referencia completa de compilación, empaquetado, instalación (`install.sh`/systemd),
|
||||||
|
> flags del binario y verificación: **[BUILD.md](BUILD.md)**.
|
||||||
|
|
||||||
|
> El build de Linux embebe dentro del binario, además de la app: las libs de sistema
|
||||||
|
> de Qt (cluster xcb/X11 + EGL), el driver **ODBC 18** con sus dependencias
|
||||||
|
> (unixODBC, Kerberos/GSSAPI, OpenSSL) y el **7-Zip** (`7zz`). Por eso el destino no
|
||||||
|
> necesita `apt install` de nada. (El build se hace en un entorno con esas libs; ver
|
||||||
|
> `packaging/scripts/download-bundled-deps.sh` y `docker-build-linux.sh`.)
|
||||||
|
|
||||||
|
**Desplegar en cada equipo** (sin Python, sin instalador, sin dependencias de sistema):
|
||||||
1. Copiar **solo** el ejecutable a una carpeta.
|
1. Copiar **solo** el ejecutable a una carpeta.
|
||||||
2. Ejecutarlo (doble clic o `./CloudRestoreAS`).
|
2. Ejecutarlo (doble clic o `./CloudRestoreAS`).
|
||||||
3. Se crean automáticamente: `config/`, `Entrada/`, `Procesados/`, `Fallados/`, `Temp/`.
|
3. Se crean automáticamente: `config/`, `Entrada/`, `Procesados/`, `Fallados/`, `Temp/`.
|
||||||
4. Editar `config/.env` (URL, token e instancia del panel).
|
4. Editar `config/.env` (URL, token e instancia del panel).
|
||||||
5. Reiniciar la aplicación.
|
5. Reiniciar la aplicación.
|
||||||
|
|
||||||
|
**Despliegue en servidor Linux (headless / SQL Server sobre Linux):**
|
||||||
|
```bash
|
||||||
|
sudo ./install.sh --service # servicio systemd 24/7 (offscreen, motor auto-inicio)
|
||||||
|
# o manual:
|
||||||
|
QT_QPA_PLATFORM=offscreen ./CloudRestoreAS --start-engine --headless
|
||||||
|
```
|
||||||
|
`install.sh` **no instala ni descarga nada** del sistema: solo coloca el binario, hace
|
||||||
|
el bootstrap de `config/` y registra el arranque (systemd headless o autostart de
|
||||||
|
escritorio con `--desktop`). El `data_folder` del destino viene del PANEL; con SQL
|
||||||
|
Server sobre Linux serán rutas POSIX (p. ej. `/var/opt/mssql/data`) y el `RESTORE`
|
||||||
|
las adapta automáticamente.
|
||||||
|
|
||||||
Ver `packaging/LEEME.txt` para instrucciones resumidas.
|
Ver `packaging/LEEME.txt` para instrucciones resumidas.
|
||||||
|
|
||||||
## ⚙️ Configuración Inicial
|
## ⚙️ Configuración Inicial
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from ..constants import (
|
|||||||
DIR_PROCESADOS,
|
DIR_PROCESADOS,
|
||||||
DIR_TEMP,
|
DIR_TEMP,
|
||||||
ENV_PATH,
|
ENV_PATH,
|
||||||
|
IS_WINDOWS,
|
||||||
LOGS_DIR,
|
LOGS_DIR,
|
||||||
ODBC_DIR,
|
ODBC_DIR,
|
||||||
SEVEN_ZIP_DIR,
|
SEVEN_ZIP_DIR,
|
||||||
@@ -25,10 +26,32 @@ from .env_loader import render_env_template
|
|||||||
|
|
||||||
|
|
||||||
def _copy_tree_if_missing(src: Path, dest: Path) -> None:
|
def _copy_tree_if_missing(src: Path, dest: Path) -> None:
|
||||||
if not src.is_dir() or dest.exists():
|
if not src.is_dir():
|
||||||
return
|
return
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
# dest puede existir pero VACÍO: ensure_runtime_layout crea ODBC_DIR/SEVEN_ZIP_DIR
|
||||||
shutil.copytree(src, dest)
|
# antes de llamar aquí. Solo saltar si ya tiene contenido (evita re-copiar en cada
|
||||||
|
# arranque). Antes se saltaba por dest.exists(), dejando 7-Zip/ODBC sin desplegar.
|
||||||
|
if dest.exists() and any(dest.iterdir()):
|
||||||
|
return
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copytree(src, dest, dirs_exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_seven_zip_executable() -> None:
|
||||||
|
"""
|
||||||
|
Asegura el bit de ejecución del 7-Zip embebido en Linux/macOS. shutil.copytree
|
||||||
|
suele preservar los permisos, pero esto es una salvaguarda barata: si el binario
|
||||||
|
quedó sin 'x', la extracción fallaría con PermissionError.
|
||||||
|
"""
|
||||||
|
if IS_WINDOWS:
|
||||||
|
return
|
||||||
|
for exe_name in ("7zz", "7za", "7z"):
|
||||||
|
exe_path = SEVEN_ZIP_DIR / exe_name
|
||||||
|
if exe_path.is_file():
|
||||||
|
try:
|
||||||
|
exe_path.chmod(0o755)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _write_env_if_missing() -> None:
|
def _write_env_if_missing() -> None:
|
||||||
@@ -71,6 +94,7 @@ def ensure_runtime_layout() -> None:
|
|||||||
|
|
||||||
_copy_tree_if_missing(BUNDLED_SOURCE_7ZIP, SEVEN_ZIP_DIR)
|
_copy_tree_if_missing(BUNDLED_SOURCE_7ZIP, SEVEN_ZIP_DIR)
|
||||||
_copy_tree_if_missing(BUNDLED_SOURCE_ODBC, ODBC_DIR)
|
_copy_tree_if_missing(BUNDLED_SOURCE_ODBC, ODBC_DIR)
|
||||||
|
_ensure_seven_zip_executable()
|
||||||
|
|
||||||
from ..constants import DB_PATH
|
from ..constants import DB_PATH
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,24 @@ Driver={driver_line}
|
|||||||
UsageCount=1
|
UsageCount=1
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
|
# Los .so embebidos viven en config/odbc/lib (no en la raíz). Buscar ahí
|
||||||
|
# primero y usar SIEMPRE la ruta absoluta del archivo versionado real
|
||||||
|
# (p. ej. libmsodbcsql-18.5.so.1.1); un nombre suelto como
|
||||||
|
# "libmsodbcsql-18.so" no resuelve porque ese symlink apunta a /opt.
|
||||||
|
lib_dir = odbc_dir / "lib"
|
||||||
|
search_dirs = [d for d in (lib_dir, odbc_dir) if d.is_dir()]
|
||||||
so_path = None
|
so_path = None
|
||||||
for pattern in ("libmsodbcsql-18*.so*", "msodbcsql-18*.so*"):
|
for base in search_dirs:
|
||||||
matches = list(odbc_dir.glob(pattern))
|
# Preferir el archivo real versionado sobre symlinks (que pueden estar rotos).
|
||||||
if matches:
|
candidates = sorted(
|
||||||
so_path = matches[0]
|
(p for p in base.glob("libmsodbcsql-18*.so*") if p.is_file() and not p.is_symlink()),
|
||||||
|
key=lambda p: len(p.name),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
if not candidates:
|
||||||
|
candidates = [p for p in base.glob("libmsodbcsql-18*.so*") if p.exists()]
|
||||||
|
if candidates:
|
||||||
|
so_path = candidates[0]
|
||||||
break
|
break
|
||||||
driver_line = str(so_path.resolve()) if so_path else "libmsodbcsql-18.so"
|
driver_line = str(so_path.resolve()) if so_path else "libmsodbcsql-18.so"
|
||||||
content = f"""[ODBC Driver 18 for SQL Server]
|
content = f"""[ODBC Driver 18 for SQL Server]
|
||||||
|
|||||||
@@ -378,6 +378,24 @@ class SQLServerManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
app_logger.warning("No se pudo cerrar la conexión SQL tras el RESTORE")
|
app_logger.warning("No se pudo cerrar la conexión SQL tras el RESTORE")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sql_path_sep(data_folder: str) -> str:
|
||||||
|
"""
|
||||||
|
Separador de rutas que espera el SQL Server destino, deducido del formato
|
||||||
|
de data_folder. Windows (C:\\..., o contiene '\\') usa '\\'; SQL Server
|
||||||
|
sobre Linux (rutas POSIX '/var/opt/mssql/...') usa '/'. Las rutas del MOVE
|
||||||
|
son del filesystem del SERVIDOR, no de donde corre la app.
|
||||||
|
"""
|
||||||
|
if "\\" in data_folder:
|
||||||
|
return "\\"
|
||||||
|
# Ruta POSIX explícita ("/...") o sin separador Windows → '/'.
|
||||||
|
if data_folder.startswith("/"):
|
||||||
|
return "/"
|
||||||
|
# Heurística: "C:" u otra letra de unidad ⇒ Windows.
|
||||||
|
if len(data_folder) >= 2 and data_folder[1] == ":":
|
||||||
|
return "\\"
|
||||||
|
return "/"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_move_clauses(db_name, data_folder, logical_files) -> list:
|
def _build_move_clauses(db_name, data_folder, logical_files) -> list:
|
||||||
"""
|
"""
|
||||||
@@ -386,7 +404,13 @@ class SQLServerManager:
|
|||||||
- 1er data → {db}.mdf, siguientes → {db}_N.ndf
|
- 1er data → {db}.mdf, siguientes → {db}_N.ndf
|
||||||
- 1er log → {db}_log.ldf, siguientes → {db}_log_N.ldf
|
- 1er log → {db}_log.ldf, siguientes → {db}_log_N.ldf
|
||||||
- otros tipos (FILESTREAM/full-text) → {db}_{nombre_lógico_saneado}
|
- otros tipos (FILESTREAM/full-text) → {db}_{nombre_lógico_saneado}
|
||||||
|
|
||||||
|
El separador se adapta al SO del SQL Server destino (deducido de
|
||||||
|
data_folder), de modo que funciona tanto con SQL Server en Windows
|
||||||
|
(C:\\...\\db.mdf) como en Linux (/var/opt/mssql/data/db.mdf).
|
||||||
"""
|
"""
|
||||||
|
sep = SQLServerManager._sql_path_sep(data_folder)
|
||||||
|
base = data_folder.rstrip("\\/")
|
||||||
clauses = []
|
clauses = []
|
||||||
data_idx = 0
|
data_idx = 0
|
||||||
log_idx = 0
|
log_idx = 0
|
||||||
@@ -394,16 +418,16 @@ class SQLServerManager:
|
|||||||
if lf.type == 'D':
|
if lf.type == 'D':
|
||||||
suffix = "" if data_idx == 0 else f"_{data_idx}"
|
suffix = "" if data_idx == 0 else f"_{data_idx}"
|
||||||
ext = "mdf" if data_idx == 0 else "ndf"
|
ext = "mdf" if data_idx == 0 else "ndf"
|
||||||
new_path = f"{data_folder}\\{db_name}{suffix}.{ext}"
|
new_path = f"{base}{sep}{db_name}{suffix}.{ext}"
|
||||||
data_idx += 1
|
data_idx += 1
|
||||||
elif lf.type == 'L':
|
elif lf.type == 'L':
|
||||||
suffix = "" if log_idx == 0 else f"_{log_idx}"
|
suffix = "" if log_idx == 0 else f"_{log_idx}"
|
||||||
new_path = f"{data_folder}\\{db_name}_log{suffix}.ldf"
|
new_path = f"{base}{sep}{db_name}_log{suffix}.ldf"
|
||||||
log_idx += 1
|
log_idx += 1
|
||||||
else:
|
else:
|
||||||
# No descartar otros tipos: moverlos preservando el nombre lógico.
|
# No descartar otros tipos: moverlos preservando el nombre lógico.
|
||||||
safe = "".join(c if c.isalnum() else "_" for c in lf.logical_name)
|
safe = "".join(c if c.isalnum() else "_" for c in lf.logical_name)
|
||||||
new_path = f"{data_folder}\\{db_name}_{safe}"
|
new_path = f"{base}{sep}{db_name}_{safe}"
|
||||||
|
|
||||||
clauses.append(f"MOVE N'{lf.logical_name}' TO N'{new_path}'")
|
clauses.append(f"MOVE N'{lf.logical_name}' TO N'{new_path}'")
|
||||||
return clauses
|
return clauses
|
||||||
|
|||||||
@@ -104,24 +104,29 @@ class ConfigTab(QWidget):
|
|||||||
|
|
||||||
self.sql_windows_auth_checkbox = QCheckBox()
|
self.sql_windows_auth_checkbox = QCheckBox()
|
||||||
self.sql_windows_auth_checkbox.setChecked(True)
|
self.sql_windows_auth_checkbox.setChecked(True)
|
||||||
self.sql_windows_auth_checkbox.toggled.connect(self._on_auth_changed)
|
|
||||||
sql_layout.addRow("Usar Windows Auth:", self.sql_windows_auth_checkbox)
|
sql_layout.addRow("Usar Windows Auth:", self.sql_windows_auth_checkbox)
|
||||||
|
|
||||||
|
self.sql_username_input = QLineEdit()
|
||||||
|
self.sql_username_input.setEnabled(False)
|
||||||
|
sql_layout.addRow("Usuario SQL:", self.sql_username_input)
|
||||||
|
|
||||||
|
self.sql_password_input = QLineEdit()
|
||||||
|
self.sql_password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
self.sql_password_input.setEnabled(False)
|
||||||
|
sql_layout.addRow("Contraseña SQL:", self.sql_password_input)
|
||||||
|
|
||||||
|
# Conectar el handler DESPUÉS de crear usuario/contraseña: en Linux se
|
||||||
|
# desmarca el checkbox (abajo), lo que dispararía _on_auth_changed antes de
|
||||||
|
# que existan esos campos si se conectara arriba.
|
||||||
|
self.sql_windows_auth_checkbox.toggled.connect(self._on_auth_changed)
|
||||||
if sys.platform != "win32":
|
if sys.platform != "win32":
|
||||||
|
# Windows Auth no aplica en Linux: forzar SQL Auth y habilitar campos.
|
||||||
self.sql_windows_auth_checkbox.setChecked(False)
|
self.sql_windows_auth_checkbox.setChecked(False)
|
||||||
self.sql_windows_auth_checkbox.setEnabled(False)
|
self.sql_windows_auth_checkbox.setEnabled(False)
|
||||||
self.sql_windows_auth_checkbox.setToolTip(
|
self.sql_windows_auth_checkbox.setToolTip(
|
||||||
"Windows Auth no está disponible en Linux; use SQL Auth."
|
"Windows Auth no está disponible en Linux; use SQL Auth."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.sql_username_input = QLineEdit()
|
|
||||||
self.sql_username_input.setEnabled(False)
|
|
||||||
sql_layout.addRow("Usuario SQL:", self.sql_username_input)
|
|
||||||
|
|
||||||
self.sql_password_input = QLineEdit()
|
|
||||||
self.sql_password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
|
||||||
self.sql_password_input.setEnabled(False)
|
|
||||||
sql_layout.addRow("Contraseña SQL:", self.sql_password_input)
|
|
||||||
|
|
||||||
test_sql_btn = QPushButton("🔌 Probar Conexión")
|
test_sql_btn = QPushButton("🔌 Probar Conexión")
|
||||||
test_sql_btn.clicked.connect(self._test_sql_connection)
|
test_sql_btn.clicked.connect(self._test_sql_connection)
|
||||||
sql_layout.addRow("", test_sql_btn)
|
sql_layout.addRow("", test_sql_btn)
|
||||||
@@ -431,12 +436,17 @@ class ConfigTab(QWidget):
|
|||||||
line_edit.setText(folder)
|
line_edit.setText(folder)
|
||||||
|
|
||||||
def _browse_seven_zip(self):
|
def _browse_seven_zip(self):
|
||||||
"""Abre el diálogo para seleccionar 7z.exe."""
|
"""Abre el diálogo para seleccionar el ejecutable de 7-Zip."""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
title = "Seleccionar 7z.exe"
|
||||||
|
default_dir = "C:\\Program Files\\7-Zip"
|
||||||
|
file_filter = "Ejecutables (*.exe)"
|
||||||
|
else:
|
||||||
|
title = "Seleccionar 7z / 7zz"
|
||||||
|
default_dir = ""
|
||||||
|
file_filter = "Ejecutables (*)"
|
||||||
file_path, _ = QFileDialog.getOpenFileName(
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
self,
|
self, title, default_dir, file_filter
|
||||||
"Seleccionar 7z.exe",
|
|
||||||
"C:\\Program Files\\7-Zip",
|
|
||||||
"Ejecutables (*.exe)"
|
|
||||||
)
|
)
|
||||||
if file_path:
|
if file_path:
|
||||||
self.seven_zip_input.setText(file_path)
|
self.seven_zip_input.setText(file_path)
|
||||||
@@ -475,6 +485,9 @@ class ConfigTab(QWidget):
|
|||||||
|
|
||||||
def _on_auth_changed(self, checked: bool):
|
def _on_auth_changed(self, checked: bool):
|
||||||
"""Maneja el cambio en el tipo de autenticación."""
|
"""Maneja el cambio en el tipo de autenticación."""
|
||||||
|
# Guard: la señal puede dispararse durante la construcción de la UI.
|
||||||
|
if not hasattr(self, "sql_username_input"):
|
||||||
|
return
|
||||||
self.sql_username_input.setEnabled(not checked)
|
self.sql_username_input.setEnabled(not checked)
|
||||||
self.sql_password_input.setEnabled(not checked)
|
self.sql_password_input.setEnabled(not checked)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Tab de logs y soporte."""
|
"""Tab de logs y soporte."""
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
@@ -91,10 +92,15 @@ class LogsTab(QWidget):
|
|||||||
self.logs_text.setPlainText("\n".join(log_lines))
|
self.logs_text.setPlainText("\n".join(log_lines))
|
||||||
|
|
||||||
def _open_logs_folder(self):
|
def _open_logs_folder(self):
|
||||||
"""Abre la carpeta de logs en el explorador."""
|
"""Abre la carpeta de logs en el explorador de archivos del sistema."""
|
||||||
try:
|
try:
|
||||||
if LOGS_DIR.exists():
|
if LOGS_DIR.exists():
|
||||||
subprocess.run(["explorer", str(LOGS_DIR)])
|
if sys.platform == "win32":
|
||||||
|
subprocess.run(["explorer", str(LOGS_DIR)])
|
||||||
|
elif sys.platform == "darwin":
|
||||||
|
subprocess.run(["open", str(LOGS_DIR)])
|
||||||
|
else:
|
||||||
|
subprocess.run(["xdg-open", str(LOGS_DIR)])
|
||||||
else:
|
else:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
|
|||||||
131
build-all.sh
Executable file
131
build-all.sh
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# CloudRestoreAS — build de TODO en una sola tarea (ejecutar desde WSL).
|
||||||
|
#
|
||||||
|
# Linux : build en Docker (ubuntu:22.04) -> dist/CloudRestoreAS (autocontenido)
|
||||||
|
# Windows: build vía powershell.exe -> build.ps1 -> dist/CloudRestoreAS.exe
|
||||||
|
# Paquete: dist/release/CloudRestoreAS-linux.tar.gz y CloudRestoreAS-win.zip
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# ./build-all.sh # ambos + empaquetado
|
||||||
|
# ./build-all.sh --linux-only # solo Linux
|
||||||
|
# ./build-all.sh --windows-only # solo Windows
|
||||||
|
# ./build-all.sh --no-package # sin generar .tar.gz/.zip
|
||||||
|
# ./build-all.sh --clean # rebuild desde cero (borra venvs/bundled/dist)
|
||||||
|
# =============================================================================
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
DO_LINUX=1
|
||||||
|
DO_WINDOWS=1
|
||||||
|
DO_PACKAGE=1
|
||||||
|
DO_CLEAN=0
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--linux-only) DO_WINDOWS=0 ;;
|
||||||
|
--windows-only) DO_LINUX=0 ;;
|
||||||
|
--no-package) DO_PACKAGE=0 ;;
|
||||||
|
--clean) DO_CLEAN=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
*) echo "Opción desconocida: $arg" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- helpers -----------------------------------------------------------------
|
||||||
|
BLUE='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; NC='\033[0m'
|
||||||
|
say() { echo -e "${BLUE}==>${NC} $*"; }
|
||||||
|
ok() { echo -e "${GREEN}OK:${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}AVISO:${NC} $*" >&2; }
|
||||||
|
err() { echo -e "${RED}ERROR:${NC} $*" >&2; }
|
||||||
|
|
||||||
|
# SECONDS es incremental de bash; lo usamos para medir cada etapa.
|
||||||
|
stage_start() { STAGE_T0=$SECONDS; }
|
||||||
|
stage_end() { echo -e " (${1} en $((SECONDS - STAGE_T0))s)"; }
|
||||||
|
|
||||||
|
WIN_OK=0
|
||||||
|
LIN_OK=0
|
||||||
|
|
||||||
|
# --- limpieza opcional -------------------------------------------------------
|
||||||
|
if [[ "$DO_CLEAN" -eq 1 ]]; then
|
||||||
|
say "Limpieza (--clean): borrando venvs, bundled y dist"
|
||||||
|
# bundled/ y venv-linux los crea Docker como root; borrar vía contenedor.
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
docker run --rm -v "$ROOT:/app" ubuntu:22.04 bash -c \
|
||||||
|
'rm -rf /app/packaging/bundled/linux /app/venv-linux /app/dist/CloudRestoreAS /app/dist/release' \
|
||||||
|
>/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
rm -rf "$ROOT/venv-windows" "$ROOT/packaging/bundled/windows" \
|
||||||
|
"$ROOT/dist/CloudRestoreAS.exe" 2>/dev/null || true
|
||||||
|
ok "limpieza completada"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Build Linux (Docker) ----------------------------------------------------
|
||||||
|
if [[ "$DO_LINUX" -eq 1 ]]; then
|
||||||
|
say "Build Linux (Docker, autocontenido)"
|
||||||
|
stage_start
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
err "docker no disponible; no se puede construir Linux"
|
||||||
|
[[ "$DO_WINDOWS" -eq 0 ]] && exit 1
|
||||||
|
elif bash "$ROOT/packaging/scripts/docker-build-linux.sh"; then
|
||||||
|
if [[ -f "$ROOT/dist/CloudRestoreAS" ]]; then
|
||||||
|
LIN_OK=1; ok "dist/CloudRestoreAS"
|
||||||
|
else
|
||||||
|
err "el build Linux terminó sin binario"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
err "falló el build Linux"
|
||||||
|
fi
|
||||||
|
stage_end "Linux"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Build Windows (PowerShell -> build.ps1) ---------------------------------
|
||||||
|
if [[ "$DO_WINDOWS" -eq 1 ]]; then
|
||||||
|
say "Build Windows (PowerShell + build.ps1)"
|
||||||
|
stage_start
|
||||||
|
WIN_PATH="$(wslpath -w "$ROOT" 2>/dev/null || true)"
|
||||||
|
if command -v powershell.exe >/dev/null 2>&1 && [[ -n "$WIN_PATH" ]]; then
|
||||||
|
# build.ps1 auto-detecta el Python de Windows (Python313/312/311) y arma venv-windows.
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \
|
||||||
|
"Set-Location -LiteralPath '$WIN_PATH'; & .\\build.ps1"
|
||||||
|
PS_RC=$?
|
||||||
|
if [[ "$PS_RC" -eq 0 && -f "$ROOT/dist/CloudRestoreAS.exe" ]]; then
|
||||||
|
WIN_OK=1; ok "dist/CloudRestoreAS.exe"
|
||||||
|
else
|
||||||
|
err "falló el build Windows (código $PS_RC)"
|
||||||
|
[[ "$DO_LINUX" -eq 0 ]] && { stage_end "Windows"; exit 1; }
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "powershell.exe no disponible desde WSL; se omite el build Windows."
|
||||||
|
warn "Ejecuta build.ps1 en Windows para generar el .exe."
|
||||||
|
[[ "$DO_LINUX" -eq 0 ]] && exit 1
|
||||||
|
fi
|
||||||
|
stage_end "Windows"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Empaquetado -------------------------------------------------------------
|
||||||
|
if [[ "$DO_PACKAGE" -eq 1 ]]; then
|
||||||
|
say "Empaquetado (.tar.gz Linux / .zip Windows)"
|
||||||
|
stage_start
|
||||||
|
bash "$ROOT/packaging/scripts/package-release.sh" || warn "empaquetado con incidencias"
|
||||||
|
stage_end "Empaquetado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Resumen -----------------------------------------------------------------
|
||||||
|
echo
|
||||||
|
say "Resumen"
|
||||||
|
[[ "$DO_LINUX" -eq 1 ]] && { [[ "$LIN_OK" -eq 1 ]] && ok "Linux regenerado" || err "Linux NO generado"; }
|
||||||
|
[[ "$DO_WINDOWS" -eq 1 ]] && { [[ "$WIN_OK" -eq 1 ]] && ok "Windows regenerado" || warn "Windows NO generado"; }
|
||||||
|
if [[ -d "$ROOT/dist/release" ]]; then
|
||||||
|
echo "Artefactos en dist/release/:"
|
||||||
|
ls -lh "$ROOT/dist/release" | awk 'NR>1 {print " " $9 " " $5}'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Éxito si todo lo solicitado se generó.
|
||||||
|
FAIL=0
|
||||||
|
[[ "$DO_LINUX" -eq 1 && "$LIN_OK" -ne 1 ]] && FAIL=1
|
||||||
|
[[ "$DO_WINDOWS" -eq 1 && "$WIN_OK" -ne 1 ]] && FAIL=1
|
||||||
|
exit $FAIL
|
||||||
103
install.sh
Executable file
103
install.sh
Executable file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Instalador Linux de CloudRestoreAS.
|
||||||
|
#
|
||||||
|
# NO instala ni descarga NADA del sistema: el binario es 100% autocontenido
|
||||||
|
# (Qt/xcb, driver ODBC + Kerberos/OpenSSL, y 7-Zip van embebidos). Este script solo
|
||||||
|
# coloca el binario, marca permisos, hace el bootstrap de config/, y opcionalmente
|
||||||
|
# registra el arranque automático (servicio systemd headless o autostart de escritorio).
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# sudo ./install.sh --service # 24/7 headless vía systemd (recomendado en servidor)
|
||||||
|
# ./install.sh --desktop # autostart de escritorio (requiere sesión gráfica)
|
||||||
|
# ./install.sh # solo instala; sin arranque automático
|
||||||
|
#
|
||||||
|
# Variables:
|
||||||
|
# PREFIX=/opt/cloudrestoreas # carpeta destino (default)
|
||||||
|
# SERVICE_USER=<usuario> # usuario del servicio systemd (default: quien invoca)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MODE="none"
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--service) MODE="service" ;;
|
||||||
|
--desktop) MODE="desktop" ;;
|
||||||
|
-h|--help)
|
||||||
|
grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
*) echo "Opción desconocida: $arg" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PREFIX="${PREFIX:-/opt/cloudrestoreas}"
|
||||||
|
BIN_NAME="CloudRestoreAS"
|
||||||
|
|
||||||
|
# Localizar el binario: dist/CloudRestoreAS, dist/CloudRestoreAS-linux, o junto al script.
|
||||||
|
SRC=""
|
||||||
|
for cand in \
|
||||||
|
"$SCRIPT_DIR/dist/$BIN_NAME" \
|
||||||
|
"$SCRIPT_DIR/dist/${BIN_NAME}-linux" \
|
||||||
|
"$SCRIPT_DIR/$BIN_NAME" \
|
||||||
|
"$SCRIPT_DIR/${BIN_NAME}-linux"; do
|
||||||
|
if [[ -f "$cand" ]]; then SRC="$cand"; break; fi
|
||||||
|
done
|
||||||
|
if [[ -z "$SRC" ]]; then
|
||||||
|
echo "ERROR: no se encontró el binario ($BIN_NAME). Ejecuta ./build.sh primero" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==============================================="
|
||||||
|
echo "CloudRestoreAS - Instalación Linux"
|
||||||
|
echo " Binario : $SRC"
|
||||||
|
echo " Destino : $PREFIX"
|
||||||
|
echo " Modo : $MODE"
|
||||||
|
echo "==============================================="
|
||||||
|
|
||||||
|
mkdir -p "$PREFIX"
|
||||||
|
install -m 0755 "$SRC" "$PREFIX/$BIN_NAME"
|
||||||
|
echo "Binario instalado en $PREFIX/$BIN_NAME"
|
||||||
|
|
||||||
|
# Bootstrap de config/: el propio binario crea config/, .env y carpetas de trabajo
|
||||||
|
# al inicio de su arranque. Se corre una vez en modo offscreen (sin display) y se
|
||||||
|
# corta con timeout; así el usuario ya puede editar config/.env antes de habilitar
|
||||||
|
# el servicio.
|
||||||
|
echo "Inicializando config/ (bootstrap)..."
|
||||||
|
( cd "$PREFIX" && QT_QPA_PLATFORM=offscreen timeout 20 "$PREFIX/$BIN_NAME" --headless >/dev/null 2>&1 || true )
|
||||||
|
if [[ -f "$PREFIX/config/.env" ]]; then
|
||||||
|
echo "config/.env creado."
|
||||||
|
else
|
||||||
|
echo "NOTA: config/.env se creará en la primera ejecución."
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
service)
|
||||||
|
SERVICE_USER="${SERVICE_USER:-${SUDO_USER:-$(id -un)}}"
|
||||||
|
UNIT_SRC="$SCRIPT_DIR/packaging/linux/cloudrestoreas.service"
|
||||||
|
UNIT_DST="/etc/systemd/system/cloudrestoreas.service"
|
||||||
|
if [[ ! -w "$(dirname "$UNIT_DST")" ]]; then
|
||||||
|
echo "ERROR: se requiere sudo para instalar el servicio systemd" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sed -e "s|__USER__|$SERVICE_USER|g" \
|
||||||
|
-e "s|__WORKDIR__|$PREFIX|g" \
|
||||||
|
-e "s|__EXEC__|$PREFIX/$BIN_NAME|g" \
|
||||||
|
"$UNIT_SRC" > "$UNIT_DST"
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now cloudrestoreas.service
|
||||||
|
echo "Servicio systemd instalado y arrancado (usuario: $SERVICE_USER)."
|
||||||
|
echo " Estado : systemctl status cloudrestoreas"
|
||||||
|
echo " Logs : journalctl -u cloudrestoreas -f"
|
||||||
|
;;
|
||||||
|
desktop)
|
||||||
|
echo "Modo escritorio: la app registra su autostart .desktop al iniciarse."
|
||||||
|
echo "Ejecuta '$PREFIX/$BIN_NAME' en tu sesión gráfica."
|
||||||
|
;;
|
||||||
|
none)
|
||||||
|
echo "Instalación sin arranque automático."
|
||||||
|
echo "Ejecuta: QT_QPA_PLATFORM=offscreen $PREFIX/$BIN_NAME --start-engine --headless"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Siguiente paso: edita $PREFIX/config/.env (CLOUDRESTORE_PANEL_*) y reinicia."
|
||||||
|
[[ "$MODE" == "service" ]] && echo " Tras editar: sudo systemctl restart cloudrestoreas"
|
||||||
|
echo "Listo."
|
||||||
@@ -38,6 +38,17 @@ if odbc_dir.is_dir():
|
|||||||
dest = "bundled/odbc"
|
dest = "bundled/odbc"
|
||||||
binaries.append((str(item), dest))
|
binaries.append((str(item), dest))
|
||||||
|
|
||||||
|
# Libs de sistema Qt (cluster xcb/X11 + EGL) que el plugin xcb carga por dlopen.
|
||||||
|
# Van JUNTO a las librerías Qt (PySide6/Qt/lib): libQt6XcbQpa.so.6 tiene RPATH
|
||||||
|
# $ORIGIN, así que resuelve libxcb-cursor.so.0 y el resto en su propio directorio
|
||||||
|
# sin depender de LD_LIBRARY_PATH. (El stack ODBC va con el driver en bundled/odbc,
|
||||||
|
# con RPATH=$ORIGIN aplicado por download-bundled-deps.sh.) Solo aplica en Linux.
|
||||||
|
qt_sys_lib = bundled / "qt" / "lib"
|
||||||
|
if qt_sys_lib.is_dir():
|
||||||
|
for item in qt_sys_lib.iterdir():
|
||||||
|
if item.is_file() and not item.is_symlink():
|
||||||
|
binaries.append((str(item), "PySide6/Qt/lib"))
|
||||||
|
|
||||||
hiddenimports = [
|
hiddenimports = [
|
||||||
"PySide6",
|
"PySide6",
|
||||||
"pyodbc",
|
"pyodbc",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
CloudRestoreAS — despliegue portable
|
CloudRestoreAS — despliegue portable
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
El ejecutable es AUTOCONTENIDO: no requiere Python, ni 7-Zip, ni driver ODBC, ni
|
||||||
|
librerias Qt instaladas en el equipo destino. Todo va embebido dentro del binario.
|
||||||
|
|
||||||
Windows: copie CloudRestoreAS.exe a la carpeta deseada.
|
Windows: copie CloudRestoreAS.exe a la carpeta deseada.
|
||||||
Linux: copie CloudRestoreAS y ejecute chmod +x CloudRestoreAS si hace falta.
|
Linux: copie CloudRestoreAS y ejecute chmod +x CloudRestoreAS si hace falta.
|
||||||
|
|
||||||
@@ -15,4 +18,23 @@ Cerrar la ventana (X) minimiza a la bandeja; la app sigue en ejecucion.
|
|||||||
Para salir por completo: clic derecho en el icono de bandeja -> Salir,
|
Para salir por completo: clic derecho en el icono de bandeja -> Salir,
|
||||||
o menu Archivo -> Salir.
|
o menu Archivo -> Salir.
|
||||||
|
|
||||||
No requiere Python ni instaladores adicionales en el equipo destino.
|
-------------------------------------
|
||||||
|
Linux en SERVIDOR SIN ESCRITORIO (headless)
|
||||||
|
-------------------------------------
|
||||||
|
En un servidor sin pantalla (sin DISPLAY), la app arranca automaticamente en modo
|
||||||
|
"offscreen": no muestra ventana pero el motor de restauracion trabaja igual. No hay
|
||||||
|
que instalar nada del sistema.
|
||||||
|
|
||||||
|
Opcion A - servicio 24/7 (recomendado en servidor SQL):
|
||||||
|
sudo ./install.sh --service
|
||||||
|
(instala una unit systemd que corre el binario con --start-engine --headless;
|
||||||
|
ver estado: systemctl status cloudrestoreas ; logs: journalctl -u cloudrestoreas -f)
|
||||||
|
|
||||||
|
Opcion B - manual:
|
||||||
|
QT_QPA_PLATFORM=offscreen ./CloudRestoreAS --start-engine --headless
|
||||||
|
|
||||||
|
Opcion C - escritorio Linux (con pantalla):
|
||||||
|
./install.sh --desktop (registra autostart .desktop; requiere sesion grafica)
|
||||||
|
|
||||||
|
El servidor SQL destino y su carpeta de datos (data_folder) se toman del PANEL;
|
||||||
|
con SQL Server sobre Linux use rutas POSIX (p. ej. /var/opt/mssql/data).
|
||||||
|
|||||||
19
packaging/linux/cloudrestoreas.service
Normal file
19
packaging/linux/cloudrestoreas.service
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=CloudRestoreAS - Restauración automática SQL Server
|
||||||
|
After=network-online.target mssql-server.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
# __USER__ y __EXEC__ los sustituye install.sh al instalar.
|
||||||
|
User=__USER__
|
||||||
|
WorkingDirectory=__WORKDIR__
|
||||||
|
# Servidor headless: plataforma Qt offscreen (sin display) y motor auto-inicio.
|
||||||
|
Environment=QT_QPA_PLATFORM=offscreen
|
||||||
|
ExecStart=__EXEC__ --start-engine --headless
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
# El binario es autocontenido (Qt, ODBC, 7-Zip embebidos): no necesita libs del sistema.
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
34
packaging/scripts/build-all.sh
Normal file → Executable file
34
packaging/scripts/build-all.sh
Normal file → Executable file
@@ -1,34 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Genera ambos artefactos: Linux (local/Docker) y Windows (PowerShell + Python en Windows).
|
# Wrapper de compatibilidad: el orquestador canónico vive en la raíz del repo.
|
||||||
|
# Ejecuta ./build-all.sh (desde WSL) para generar Windows + Linux + paquetes.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
RELEASE="$ROOT/dist/release"
|
exec bash "$ROOT/build-all.sh" "$@"
|
||||||
mkdir -p "$RELEASE"
|
|
||||||
|
|
||||||
echo "=== Build Linux ==="
|
|
||||||
if [[ -f "$ROOT/build.sh" ]]; then
|
|
||||||
bash "$ROOT/build.sh"
|
|
||||||
cp -f "$ROOT/dist/CloudRestoreAS" "$RELEASE/CloudRestoreAS-linux"
|
|
||||||
chmod +x "$RELEASE/CloudRestoreAS-linux"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== Build Windows (via PowerShell) ==="
|
|
||||||
WSL_PATH=$(wslpath -w "$ROOT" 2>/dev/null || echo "")
|
|
||||||
if [[ -n "$WSL_PATH" ]] && command -v powershell.exe >/dev/null 2>&1; then
|
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "
|
|
||||||
Set-Location '$WSL_PATH'
|
|
||||||
& 'C:\Users\Hugo Reyes\AppData\Local\Programs\Python\Python313\python.exe' -m pip install -q -r requirements-windows.txt 2>\$null
|
|
||||||
& .\build.ps1
|
|
||||||
"
|
|
||||||
if [[ -f "$ROOT/dist/CloudRestoreAS.exe" ]]; then
|
|
||||||
cp -f "$ROOT/dist/CloudRestoreAS.exe" "$RELEASE/CloudRestoreAS.exe"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Omitido: ejecute build.ps1 en Windows manualmente"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cp -f "$ROOT/packaging/LEEME.txt" "$RELEASE/"
|
|
||||||
echo ""
|
|
||||||
echo "Artefactos en: $RELEASE"
|
|
||||||
ls -la "$RELEASE"
|
|
||||||
|
|||||||
@@ -9,10 +9,19 @@ docker run --rm \
|
|||||||
set -e
|
set -e
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
|
# Herramientas de build + libs de sistema para que PyInstaller EMBEBA el cierre
|
||||||
|
# completo de dependencias (plugin Qt xcb y driver MS ODBC). Cinturón y tirantes
|
||||||
|
# junto con el bundle explícito de download-bundled-deps.sh.
|
||||||
apt-get install -y -qq \
|
apt-get install -y -qq \
|
||||||
python3 python3-venv python3-pip curl dpkg-dev binutils unixodbc \
|
python3 python3-venv python3-pip curl dpkg-dev binutils apt-utils patchelf \
|
||||||
libglib2.0-0 libdbus-1-3 libxkbcommon0 libfontconfig1 libxcb1 libx11-6 \
|
unixodbc libodbcinst2 libltdl7 libkrb5-3 libgssapi-krb5-2 libssl3 \
|
||||||
libxcb-xkb1 libegl1 libgl1 libxi6 libxrender1 libxext6 \
|
libglib2.0-0 libdbus-1-3 libfontconfig1 libfreetype6 \
|
||||||
|
libxkbcommon0 libxkbcommon-x11-0 libegl1 libgl1 libglvnd0 \
|
||||||
|
libx11-6 libx11-xcb1 libxext6 libxrender1 libxi6 libxfixes3 libxtst6 \
|
||||||
|
libxcomposite1 libxdamage1 libxrandr2 libxkbfile1 \
|
||||||
|
libxcb1 libxcb-cursor0 libxcb-icccm4 libxcb-util1 libxcb-image0 \
|
||||||
|
libxcb-keysyms1 libxcb-render-util0 libxcb-render0 libxcb-shape0 \
|
||||||
|
libxcb-xkb1 libxcb-glx0 libxcb-randr0 libxcb-shm0 libxcb-sync1 \
|
||||||
> /dev/null
|
> /dev/null
|
||||||
chmod +x build.sh packaging/scripts/download-bundled-deps.sh
|
chmod +x build.sh packaging/scripts/download-bundled-deps.sh
|
||||||
./build.sh
|
./build.sh
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Descarga 7zz y bibliotecas ODBC para empaquetar en el binario Linux.
|
# Descarga y EMBEBE (build-time) todo lo que el binario Linux necesita en runtime:
|
||||||
|
# - 7zz (7-Zip para Linux)
|
||||||
|
# - Driver MS ODBC 18 + unixODBC
|
||||||
|
# - Libs de sistema de Qt (cluster xcb/X11 + EGL) y del stack ODBC
|
||||||
|
# (libltdl, Kerberos/GSSAPI, OpenSSL) que PySide6/pyodbc cargan por dlopen
|
||||||
|
# y que NO vienen dentro del wheel.
|
||||||
|
# Objetivo: que el onefile corra en un servidor Linux "pelado" sin instalar nada.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
@@ -7,17 +13,55 @@ VERSIONS="$ROOT/packaging/bundled-versions.json"
|
|||||||
BUNDLED="$ROOT/packaging/bundled/linux"
|
BUNDLED="$ROOT/packaging/bundled/linux"
|
||||||
SEVEN="$BUNDLED/7zip"
|
SEVEN="$BUNDLED/7zip"
|
||||||
ODBC="$BUNDLED/odbc/lib"
|
ODBC="$BUNDLED/odbc/lib"
|
||||||
|
QTLIB="$BUNDLED/qt/lib" # libs de sistema extra a embeder en la raíz de _MEIPASS
|
||||||
|
|
||||||
mkdir -p "$SEVEN" "$ODBC"
|
mkdir -p "$SEVEN" "$ODBC" "$QTLIB"
|
||||||
|
|
||||||
read_url() {
|
read_url() {
|
||||||
python3 -c "import json; print(json.load(open('$VERSIONS'))['$1'])"
|
python3 -c "import json; print(json.load(open('$VERSIONS'))['$1'])"
|
||||||
}
|
}
|
||||||
|
|
||||||
LINUX_7Z_URL="$(read_url seven_zip_linux_x64)"
|
# Descarga uno o más paquetes .deb (y NO sus dependencias), extrae sus .so y los
|
||||||
MSODBC_DEB="$(read_url msodbcsql18_deb)"
|
# copia al directorio destino con el NOMBRE DE SONAME (lo que busca el loader:
|
||||||
|
# libfoo.so.N), dereferenciando symlinks para no dejar enlaces colgantes.
|
||||||
|
# Requiere apt-get/dpkg-deb.
|
||||||
|
fetch_pkg_libs() {
|
||||||
|
local destdir="$1"; shift
|
||||||
|
if ! command -v apt-get >/dev/null 2>&1; then
|
||||||
|
echo " (apt-get no disponible; se omite descarga de $* — el entorno de build debe proveerlas)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local tmp; tmp=$(mktemp -d)
|
||||||
|
(
|
||||||
|
cd "$tmp"
|
||||||
|
apt-get download "$@" >/dev/null 2>&1 || {
|
||||||
|
# Reintento tras refrescar índices (build env con red).
|
||||||
|
apt-get update -qq >/dev/null 2>&1 || true
|
||||||
|
apt-get download "$@" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
for deb in *.deb; do
|
||||||
|
[[ -f "$deb" ]] || continue
|
||||||
|
dpkg-deb -x "$deb" x
|
||||||
|
done
|
||||||
|
[[ -d x ]] || exit 0
|
||||||
|
# 1) Symlinks SONAME del propio paquete (libfoo.so.N -> libfoo.so.N.M.P):
|
||||||
|
# copiar el contenido REAL bajo el nombre del symlink (= SONAME exacto).
|
||||||
|
while IFS= read -r link; do
|
||||||
|
cp -Lf "$link" "$destdir/$(basename "$link")" 2>/dev/null || true
|
||||||
|
done < <(find x -type l -name '*.so.*')
|
||||||
|
# 2) Archivos reales sin symlink: derivar SONAME por truncación de versión
|
||||||
|
# (libfoo.so.N.M.P -> libfoo.so.N) para no dejarlos con nombre versionado.
|
||||||
|
while IFS= read -r f; do
|
||||||
|
local soname
|
||||||
|
soname=$(basename "$f" | sed -E 's/(\.so\.[0-9]+)\..*/\1/')
|
||||||
|
[[ -e "$destdir/$soname" ]] || cp -Lf "$f" "$destdir/$soname" 2>/dev/null || true
|
||||||
|
done < <(find x -type f -name '*.so.*')
|
||||||
|
)
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
echo "==> 7-Zip Linux (7zz)"
|
echo "==> 7-Zip Linux (7zz)"
|
||||||
|
LINUX_7Z_URL="$(read_url seven_zip_linux_x64)"
|
||||||
if [[ ! -f "$SEVEN/7zz" ]]; then
|
if [[ ! -f "$SEVEN/7zz" ]]; then
|
||||||
TMP=$(mktemp -d)
|
TMP=$(mktemp -d)
|
||||||
curl -fsSL -o "$TMP/7z.tar.xz" "$LINUX_7Z_URL"
|
curl -fsSL -o "$TMP/7z.tar.xz" "$LINUX_7Z_URL"
|
||||||
@@ -31,6 +75,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==> ODBC Driver 18 + unixODBC"
|
echo "==> ODBC Driver 18 + unixODBC"
|
||||||
|
MSODBC_DEB="$(read_url msodbcsql18_deb)"
|
||||||
if [[ -z "$(ls -A "$ODBC" 2>/dev/null || true)" ]]; then
|
if [[ -z "$(ls -A "$ODBC" 2>/dev/null || true)" ]]; then
|
||||||
TMP=$(mktemp -d)
|
TMP=$(mktemp -d)
|
||||||
cd "$TMP"
|
cd "$TMP"
|
||||||
@@ -38,21 +83,74 @@ if [[ -z "$(ls -A "$ODBC" 2>/dev/null || true)" ]]; then
|
|||||||
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update -qq
|
apt-get update -qq || true
|
||||||
apt-get install -y -qq unixodbc dpkg-dev > /dev/null 2>&1 || true
|
apt-get install -y -qq unixodbc dpkg-dev > /dev/null 2>&1 || true
|
||||||
if dpkg -l unixodbc 2>/dev/null | grep -q ^ii; then
|
if dpkg -l unixodbc 2>/dev/null | grep -q ^ii; then
|
||||||
find /usr/lib -name 'libodbc*.so*' -exec cp -P {} "$ODBC/" \; 2>/dev/null || true
|
# -L: dereferencia symlinks para no dejar enlaces colgantes en el destino.
|
||||||
find /usr/lib -name 'libodbcinst*.so*' -exec cp -P {} "$ODBC/" \; 2>/dev/null || true
|
find /usr/lib -name 'libodbc*.so*' -exec cp -Lf {} "$ODBC/" \; 2>/dev/null || true
|
||||||
|
find /usr/lib -name 'libodbcinst*.so*' -exec cp -Lf {} "$ODBC/" \; 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dpkg-deb -x msodbcsql18.deb msodbcsql
|
dpkg-deb -x msodbcsql18.deb msodbcsql
|
||||||
find msodbcsql -name '*.so*' -exec cp -P {} "$ODBC/" \;
|
# Copiar SOLO el archivo real versionado del driver (evita el symlink -> /opt).
|
||||||
|
find msodbcsql -type f -name 'libmsodbcsql-18*.so*' -exec cp -Lf {} "$ODBC/" \;
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
rm -rf "$TMP"
|
rm -rf "$TMP"
|
||||||
echo " Bibliotecas ODBC en $ODBC"
|
|
||||||
|
# Stack de dependencias del driver MS ODBC 18, JUNTO al driver, para que un
|
||||||
|
# servidor pelado (sin unixODBC/krb5/openssl) pueda cargarlo. El driver se
|
||||||
|
# patchelf-ea a RPATH=$ORIGIN (más abajo) para resolverlas en su propio dir.
|
||||||
|
ODBC_DEP_PKGS=(
|
||||||
|
libltdl7 libkrb5-3 libgssapi-krb5-2 libk5crypto3 libkrb5support0
|
||||||
|
libcom-err2 libkeyutils1 libssl3 libodbc2 libodbcinst2
|
||||||
|
)
|
||||||
|
fetch_pkg_libs "$ODBC" "${ODBC_DEP_PKGS[@]}" || true
|
||||||
|
fetch_pkg_libs "$ODBC" unixodbc || true
|
||||||
|
echo " Bibliotecas ODBC + deps en $ODBC"
|
||||||
else
|
else
|
||||||
echo " ODBC ya existe"
|
echo " ODBC ya existe"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# RPATH=$ORIGIN en TODAS las .so del cluster ODBC: así el driver y sus deps
|
||||||
|
# (libkrb5 -> libk5crypto/libkrb5support/libkeyutils, libgssapi, libltdl,
|
||||||
|
# libodbcinst, openssl) se resuelven entre sí en su propio directorio una vez
|
||||||
|
# copiados a config/odbc/lib, sin depender de LD_LIBRARY_PATH.
|
||||||
|
if command -v patchelf >/dev/null 2>&1; then
|
||||||
|
for so in "$ODBC"/*.so*; do
|
||||||
|
[[ -f "$so" && ! -L "$so" ]] || continue
|
||||||
|
patchelf --set-rpath '$ORIGIN' "$so" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
echo " RPATH=\$ORIGIN aplicado a todo el cluster ODBC"
|
||||||
|
else
|
||||||
|
echo " ADVERTENCIA: patchelf no disponible; el driver ODBC podría no resolver" >&2
|
||||||
|
echo " sus deps en un servidor sin unixODBC/krb5. Instálalo en el entorno de build." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Libs de sistema Qt (cluster xcb/X11 + EGL) para la GUI con display"
|
||||||
|
if [[ -z "$(ls -A "$QTLIB" 2>/dev/null || true)" ]]; then
|
||||||
|
# Plataforma Qt xcb + su cierre X11/xcb + EGL. Van junto a las libs Qt en el
|
||||||
|
# onefile (PySide6/Qt/lib) para que libQt6XcbQpa las encuentre por $ORIGIN.
|
||||||
|
QT_XCB_PKGS=(
|
||||||
|
libxcb-cursor0 libxcb-icccm4 libxcb-util1 libxcb-image0 libxcb-keysyms1
|
||||||
|
libxcb-render-util0 libxcb-render0 libxcb-shape0 libxcb-xkb1
|
||||||
|
libxkbcommon-x11-0 libxkbcommon0 libxtst6 libxcomposite1 libxdamage1
|
||||||
|
libxrandr2 libxkbfile1 libxcb1 libx11-6 libx11-xcb1 libxext6 libxrender1
|
||||||
|
libxi6 libxfixes3 libxcb-glx0 libxcb-randr0 libxcb-shm0 libxcb-sync1
|
||||||
|
libxcb-present0 libxcb-dri2-0 libxcb-dri3-0 libxcb-xfixes0 libxau6
|
||||||
|
libxdmcp6 libbsd0 libsm6 libice6
|
||||||
|
)
|
||||||
|
QT_GL_PKGS=(libglvnd0 libegl1 libgl1 libopengl0 libglx0 libgles2)
|
||||||
|
fetch_pkg_libs "$QTLIB" "${QT_XCB_PKGS[@]}"
|
||||||
|
fetch_pkg_libs "$QTLIB" "${QT_GL_PKGS[@]}"
|
||||||
|
COUNT=$(find "$QTLIB" -type f -name '*.so*' 2>/dev/null | wc -l)
|
||||||
|
echo " $COUNT libs Qt embebidas en $QTLIB"
|
||||||
|
if [[ "$COUNT" -eq 0 ]]; then
|
||||||
|
echo " ADVERTENCIA: no se embebió ninguna lib Qt. El entorno de build" >&2
|
||||||
|
echo " (docker-build-linux.sh) debe traerlas para que PyInstaller las incluya." >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Libs Qt ya existen ($(find "$QTLIB" -type f -name '*.so*' | wc -l) archivos)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Listo: $BUNDLED"
|
echo "Listo: $BUNDLED"
|
||||||
|
|||||||
36
packaging/scripts/package-release.sh
Normal file → Executable file
36
packaging/scripts/package-release.sh
Normal file → Executable file
@@ -9,18 +9,38 @@ mkdir -p "$RELEASE"
|
|||||||
[[ -f "$ROOT/dist/CloudRestoreAS" ]] && cp -f "$ROOT/dist/CloudRestoreAS" "$RELEASE/CloudRestoreAS-linux" && chmod +x "$RELEASE/CloudRestoreAS-linux"
|
[[ -f "$ROOT/dist/CloudRestoreAS" ]] && cp -f "$ROOT/dist/CloudRestoreAS" "$RELEASE/CloudRestoreAS-linux" && chmod +x "$RELEASE/CloudRestoreAS-linux"
|
||||||
cp -f "$ROOT/packaging/LEEME.txt" "$RELEASE/"
|
cp -f "$ROOT/packaging/LEEME.txt" "$RELEASE/"
|
||||||
|
|
||||||
python3 <<'PY'
|
# Paquete Windows: carpeta raíz CloudRestoreAS/ con .exe + install.ps1 + LEEME
|
||||||
import zipfile
|
# (paridad con el .tar.gz de Linux).
|
||||||
|
if [[ -f "$RELEASE/CloudRestoreAS.exe" ]]; then
|
||||||
|
ROOT="$ROOT" RELEASE="$RELEASE" python3 <<'PY'
|
||||||
|
import os, zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
release = Path("dist/release")
|
root = Path(os.environ["ROOT"]); release = Path(os.environ["RELEASE"])
|
||||||
if (release / "CloudRestoreAS.exe").exists():
|
items = [(release / "CloudRestoreAS.exe", "CloudRestoreAS/CloudRestoreAS.exe"),
|
||||||
with zipfile.ZipFile(release / "CloudRestoreAS-win.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
(root / "packaging" / "LEEME.txt", "CloudRestoreAS/LEEME.txt")]
|
||||||
z.write(release / "CloudRestoreAS.exe", "CloudRestoreAS.exe")
|
if (root / "install.ps1").exists():
|
||||||
z.write(release / "LEEME.txt", "LEEME.txt")
|
items.append((root / "install.ps1", "CloudRestoreAS/install.ps1"))
|
||||||
|
with zipfile.ZipFile(release / "CloudRestoreAS-win.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
|
for src, arc in items:
|
||||||
|
z.write(src, arc)
|
||||||
|
print(" zip Windows:", release / "CloudRestoreAS-win.zip")
|
||||||
PY
|
PY
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -f "$RELEASE/CloudRestoreAS-linux" ]]; then
|
if [[ -f "$RELEASE/CloudRestoreAS-linux" ]]; then
|
||||||
tar czf "$RELEASE/CloudRestoreAS-linux.tar.gz" -C "$RELEASE" CloudRestoreAS-linux LEEME.txt
|
# Paquete de despliegue completo: binario + instalador + unit systemd + LEEME,
|
||||||
|
# bajo una carpeta raíz para que extraiga limpio. install.sh encuentra el binario
|
||||||
|
# (CloudRestoreAS-linux) y la unit (packaging/linux/) relativos a su ubicación.
|
||||||
|
STAGE="$RELEASE/CloudRestoreAS-linux"
|
||||||
|
PKG="$RELEASE/pkg-linux/CloudRestoreAS"
|
||||||
|
rm -rf "$RELEASE/pkg-linux"
|
||||||
|
mkdir -p "$PKG/packaging/linux"
|
||||||
|
cp -f "$STAGE" "$PKG/CloudRestoreAS-linux"; chmod +x "$PKG/CloudRestoreAS-linux"
|
||||||
|
cp -f "$ROOT/install.sh" "$PKG/install.sh"; chmod +x "$PKG/install.sh"
|
||||||
|
cp -f "$ROOT/packaging/linux/cloudrestoreas.service" "$PKG/packaging/linux/"
|
||||||
|
cp -f "$ROOT/packaging/LEEME.txt" "$PKG/LEEME.txt"
|
||||||
|
tar czf "$RELEASE/CloudRestoreAS-linux.tar.gz" -C "$RELEASE/pkg-linux" CloudRestoreAS
|
||||||
|
rm -rf "$RELEASE/pkg-linux"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Release en: $RELEASE"
|
echo "Release en: $RELEASE"
|
||||||
|
|||||||
34
runner.py
34
runner.py
@@ -23,9 +23,34 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Inicia el motor de restauración automáticamente",
|
help="Inicia el motor de restauración automáticamente",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--headless",
|
||||||
|
action="store_true",
|
||||||
|
help="Fuerza el modo sin interfaz (Qt offscreen) para servidores sin display",
|
||||||
|
)
|
||||||
return parser.parse_args(argv)
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_qt_platform(headless: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Selecciona el plugin de plataforma Qt en Linux. En un servidor headless (sin
|
||||||
|
DISPLAY/WAYLAND_DISPLAY) o con --headless, usa 'offscreen' para que la app
|
||||||
|
arranque y el motor trabaje sin X; con display usa el default ('xcb'). No toca
|
||||||
|
nada si el usuario ya fijó QT_QPA_PLATFORM, ni en Windows/macOS.
|
||||||
|
|
||||||
|
Devuelve la plataforma forzada ("offscreen") o "" si se deja el default.
|
||||||
|
"""
|
||||||
|
if sys.platform in ("win32", "darwin"):
|
||||||
|
return ""
|
||||||
|
if os.environ.get("QT_QPA_PLATFORM"):
|
||||||
|
return os.environ["QT_QPA_PLATFORM"]
|
||||||
|
has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
|
||||||
|
if headless or not has_display:
|
||||||
|
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
||||||
|
return "offscreen"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _crash_log_targets() -> list[Path]:
|
def _crash_log_targets() -> list[Path]:
|
||||||
"""Ubicaciones candidatas para el crash log, de más a menos accesible."""
|
"""Ubicaciones candidatas para el crash log, de más a menos accesible."""
|
||||||
targets: list[Path] = []
|
targets: list[Path] = []
|
||||||
@@ -64,6 +89,9 @@ def _show_fatal(exc: BaseException) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
|
# Sin display, forzar offscreen para que crear la QApplication no falle
|
||||||
|
# también aquí (el diálogo no se verá, pero el crash log ya quedó escrito).
|
||||||
|
_ensure_qt_platform()
|
||||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
app = QApplication.instance() or QApplication(sys.argv)
|
app = QApplication.instance() or QApplication(sys.argv)
|
||||||
@@ -100,6 +128,7 @@ def _run() -> int:
|
|||||||
from app.utils.logger import app_logger
|
from app.utils.logger import app_logger
|
||||||
|
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
qt_platform = _ensure_qt_platform(headless=args.headless)
|
||||||
launch = get_launch_options()
|
launch = get_launch_options()
|
||||||
initialize_env()
|
initialize_env()
|
||||||
|
|
||||||
@@ -109,7 +138,10 @@ def _run() -> int:
|
|||||||
app_logger.info("=" * 80)
|
app_logger.info("=" * 80)
|
||||||
app_logger.info("CloudRestoreAS - Iniciando aplicación")
|
app_logger.info("CloudRestoreAS - Iniciando aplicación")
|
||||||
app_logger.info(f"Directorio app: {ROOT_DIR}")
|
app_logger.info(f"Directorio app: {ROOT_DIR}")
|
||||||
app_logger.info("Modo: ventana siempre visible; cerrar minimiza a bandeja")
|
if qt_platform == "offscreen":
|
||||||
|
app_logger.info("Modo: sin display → plataforma Qt 'offscreen' (motor headless)")
|
||||||
|
else:
|
||||||
|
app_logger.info("Modo: ventana siempre visible; cerrar minimiza a bandeja")
|
||||||
if start_engine:
|
if start_engine:
|
||||||
app_logger.info("Modo: motor auto-inicio")
|
app_logger.info("Modo: motor auto-inicio")
|
||||||
if not panel_ok:
|
if not panel_ok:
|
||||||
|
|||||||
Reference in New Issue
Block a user