feature/generador-instaladores-linux-windows
This commit is contained in:
@@ -1,14 +1,18 @@
|
||||
"""Creación automática de config/ y carpetas de trabajo."""
|
||||
|
||||
import hashlib
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .. import __version__
|
||||
from ..constants import (
|
||||
APP_DIR,
|
||||
BUNDLE_DIR,
|
||||
BUNDLED_SOURCE_7ZIP,
|
||||
BUNDLED_SOURCE_ODBC,
|
||||
BUNDLED_STAMP_PATH,
|
||||
CONFIG_DIR,
|
||||
DATA_DIR,
|
||||
DIR_ENTRADA,
|
||||
@@ -20,19 +24,100 @@ from ..constants import (
|
||||
LOGS_DIR,
|
||||
ODBC_DIR,
|
||||
SEVEN_ZIP_DIR,
|
||||
VERSION_PATH,
|
||||
)
|
||||
from ..db.database import DatabaseManager
|
||||
from .env_loader import render_env_template
|
||||
|
||||
|
||||
def _copy_tree_if_missing(src: Path, dest: Path) -> None:
|
||||
def _bundled_resource(rel_path: str) -> Optional[Path]:
|
||||
"""
|
||||
Recurso empaquetado: primero dentro del onefile (BUNDLE_DIR), luego junto al código
|
||||
en desarrollo. Mismo orden de preferencia que usa _write_env_if_missing.
|
||||
"""
|
||||
for base in (BUNDLE_DIR, APP_DIR):
|
||||
candidate = base / rel_path
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _read_stamp(path: Path) -> str:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def _write_stamp(path: Path, content: str) -> None:
|
||||
"""Escribe un sello solo si cambió, para no tocar disco en cada arranque."""
|
||||
value = content.strip()
|
||||
if _read_stamp(path) == value:
|
||||
return
|
||||
try:
|
||||
path.write_text(value, encoding="utf-8")
|
||||
except OSError:
|
||||
# Los sellos son informativos: si config/ no es escribible, el arranque sigue.
|
||||
# La consecuencia es re-copiar las deps embebidas en el próximo arranque.
|
||||
pass
|
||||
|
||||
|
||||
def _bundled_deps_stamp() -> str:
|
||||
"""
|
||||
Huella de las dependencias embebidas en ESTE build: el sha256 de
|
||||
packaging/bundled-versions.json, que es el archivo que fija las versiones de 7-Zip,
|
||||
del driver ODBC y de unixODBC. Si no viene empaquetado, cae a la versión de la app.
|
||||
"""
|
||||
manifest = _bundled_resource("packaging/bundled-versions.json")
|
||||
if manifest is not None:
|
||||
try:
|
||||
return "sha256:" + hashlib.sha256(manifest.read_bytes()).hexdigest()
|
||||
except OSError:
|
||||
pass
|
||||
return f"app:{__version__}"
|
||||
|
||||
|
||||
def _backup_before_refresh(dest: Path) -> None:
|
||||
"""
|
||||
Aparta el contenido actual a `<dest>.bak` antes de re-copiarlo.
|
||||
|
||||
El re-despliegue usa copytree(dirs_exist_ok=True), que SOBRESCRIBE los archivos: si el
|
||||
operador editó a mano algo como odbcinst.ini, se perdería en la primera ejecución tras
|
||||
actualizar y sin aviso. Esa edición no se puede reconstruir, así que se conserva.
|
||||
|
||||
Un solo respaldo rotatorio, sin fecha: estas carpetas viven en servidores que almacenan
|
||||
respaldos de bases y no conviene acumular una copia del driver ODBC por cada actualización.
|
||||
"""
|
||||
backup = dest.with_name(f"{dest.name}.bak")
|
||||
try:
|
||||
if backup.exists():
|
||||
shutil.rmtree(backup, ignore_errors=True)
|
||||
shutil.copytree(dest, backup, dirs_exist_ok=True)
|
||||
except OSError:
|
||||
# El respaldo es una red de seguridad, no un requisito: si el disco no da o los
|
||||
# permisos no alcanzan, el refresco debe seguir su curso.
|
||||
pass
|
||||
|
||||
|
||||
def _copy_bundled_tree(src: Path, dest: Path, refresh: bool) -> None:
|
||||
"""
|
||||
Despliega 7-Zip/ODBC del bundle a config/. Copia si el destino está vacío y RE-copia
|
||||
cuando refresh es True, es decir cuando este build trae otras versiones embebidas:
|
||||
una actualización con driver ODBC nuevo debe reemplazar el viejo, no conservarlo.
|
||||
|
||||
Antes de re-copiar sobre contenido existente se aparta una copia a `<dest>.bak`, porque
|
||||
el copytree sobrescribe y las ediciones manuales del operador no son reconstruibles.
|
||||
|
||||
dest puede existir pero VACÍO: ensure_runtime_layout crea ODBC_DIR/SEVEN_ZIP_DIR
|
||||
antes de llamar aquí, así que la condición mira el contenido, no la existencia.
|
||||
"""
|
||||
if not src.is_dir():
|
||||
return
|
||||
# dest puede existir pero VACÍO: ensure_runtime_layout crea ODBC_DIR/SEVEN_ZIP_DIR
|
||||
# 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()):
|
||||
has_content = dest.exists() and any(dest.iterdir())
|
||||
if has_content and not refresh:
|
||||
return
|
||||
if has_content and refresh:
|
||||
_backup_before_refresh(dest)
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(src, dest, dirs_exist_ok=True)
|
||||
|
||||
@@ -92,9 +177,25 @@ def ensure_runtime_layout() -> None:
|
||||
|
||||
_write_env_if_missing()
|
||||
|
||||
_copy_tree_if_missing(BUNDLED_SOURCE_7ZIP, SEVEN_ZIP_DIR)
|
||||
_copy_tree_if_missing(BUNDLED_SOURCE_ODBC, ODBC_DIR)
|
||||
# Los sellos se ubican bajo el CONFIG_DIR vigente (no la ruta absoluta precalculada)
|
||||
# para que respeten el monkeypatch de las pruebas y no escriban en el config/ real.
|
||||
deps_stamp_path = CONFIG_DIR / BUNDLED_STAMP_PATH.name
|
||||
version_stamp_path = CONFIG_DIR / VERSION_PATH.name
|
||||
|
||||
# Las deps embebidas se re-despliegan cuando el build trae otras versiones. El sello
|
||||
# se escribe DESPUÉS de copiar: si la copia falla a medias, el próximo arranque lo
|
||||
# reintenta en lugar de quedar marcado como al día.
|
||||
deps_stamp = _bundled_deps_stamp()
|
||||
refresh_deps = _read_stamp(deps_stamp_path) != deps_stamp
|
||||
_copy_bundled_tree(BUNDLED_SOURCE_7ZIP, SEVEN_ZIP_DIR, refresh_deps)
|
||||
_copy_bundled_tree(BUNDLED_SOURCE_ODBC, ODBC_DIR, refresh_deps)
|
||||
_ensure_seven_zip_executable()
|
||||
if refresh_deps:
|
||||
_write_stamp(deps_stamp_path, deps_stamp)
|
||||
|
||||
# Sello de versión: lo lee el instalador remoto del PANEL por SFTP para verificar el
|
||||
# despliegue (en Windows el .exe es console=False y no tiene stdout confiable).
|
||||
_write_stamp(version_stamp_path, __version__)
|
||||
|
||||
from ..constants import DB_PATH
|
||||
|
||||
|
||||
Reference in New Issue
Block a user