feature/generador-instaladores-linux-windows
This commit is contained in:
@@ -38,6 +38,17 @@ if odbc_dir.is_dir():
|
||||
dest = "bundled/odbc"
|
||||
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 = [
|
||||
"PySide6",
|
||||
"pyodbc",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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.
|
||||
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,
|
||||
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
|
||||
# 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
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
RELEASE="$ROOT/dist/release"
|
||||
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"
|
||||
exec bash "$ROOT/build-all.sh" "$@"
|
||||
|
||||
@@ -9,10 +9,19 @@ docker run --rm \
|
||||
set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
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 \
|
||||
python3 python3-venv python3-pip curl dpkg-dev binutils unixodbc \
|
||||
libglib2.0-0 libdbus-1-3 libxkbcommon0 libfontconfig1 libxcb1 libx11-6 \
|
||||
libxcb-xkb1 libegl1 libgl1 libxi6 libxrender1 libxext6 \
|
||||
python3 python3-venv python3-pip curl dpkg-dev binutils apt-utils patchelf \
|
||||
unixodbc libodbcinst2 libltdl7 libkrb5-3 libgssapi-krb5-2 libssl3 \
|
||||
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
|
||||
chmod +x build.sh packaging/scripts/download-bundled-deps.sh
|
||||
./build.sh
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#!/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
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
@@ -7,17 +13,55 @@ VERSIONS="$ROOT/packaging/bundled-versions.json"
|
||||
BUNDLED="$ROOT/packaging/bundled/linux"
|
||||
SEVEN="$BUNDLED/7zip"
|
||||
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() {
|
||||
python3 -c "import json; print(json.load(open('$VERSIONS'))['$1'])"
|
||||
}
|
||||
|
||||
LINUX_7Z_URL="$(read_url seven_zip_linux_x64)"
|
||||
MSODBC_DEB="$(read_url msodbcsql18_deb)"
|
||||
# Descarga uno o más paquetes .deb (y NO sus dependencias), extrae sus .so y los
|
||||
# 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)"
|
||||
LINUX_7Z_URL="$(read_url seven_zip_linux_x64)"
|
||||
if [[ ! -f "$SEVEN/7zz" ]]; then
|
||||
TMP=$(mktemp -d)
|
||||
curl -fsSL -o "$TMP/7z.tar.xz" "$LINUX_7Z_URL"
|
||||
@@ -31,6 +75,7 @@ else
|
||||
fi
|
||||
|
||||
echo "==> ODBC Driver 18 + unixODBC"
|
||||
MSODBC_DEB="$(read_url msodbcsql18_deb)"
|
||||
if [[ -z "$(ls -A "$ODBC" 2>/dev/null || true)" ]]; then
|
||||
TMP=$(mktemp -d)
|
||||
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
|
||||
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
|
||||
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
|
||||
find /usr/lib -name 'libodbcinst*.so*' -exec cp -P {} "$ODBC/" \; 2>/dev/null || true
|
||||
# -L: dereferencia symlinks para no dejar enlaces colgantes en el destino.
|
||||
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
|
||||
|
||||
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"
|
||||
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
|
||||
echo " ODBC ya existe"
|
||||
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"
|
||||
|
||||
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"
|
||||
cp -f "$ROOT/packaging/LEEME.txt" "$RELEASE/"
|
||||
|
||||
python3 <<'PY'
|
||||
import zipfile
|
||||
# Paquete Windows: carpeta raíz CloudRestoreAS/ con .exe + install.ps1 + LEEME
|
||||
# (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
|
||||
release = Path("dist/release")
|
||||
if (release / "CloudRestoreAS.exe").exists():
|
||||
with zipfile.ZipFile(release / "CloudRestoreAS-win.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
||||
z.write(release / "CloudRestoreAS.exe", "CloudRestoreAS.exe")
|
||||
z.write(release / "LEEME.txt", "LEEME.txt")
|
||||
root = Path(os.environ["ROOT"]); release = Path(os.environ["RELEASE"])
|
||||
items = [(release / "CloudRestoreAS.exe", "CloudRestoreAS/CloudRestoreAS.exe"),
|
||||
(root / "packaging" / "LEEME.txt", "CloudRestoreAS/LEEME.txt")]
|
||||
if (root / "install.ps1").exists():
|
||||
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
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
echo "Release en: $RELEASE"
|
||||
|
||||
Reference in New Issue
Block a user