feature/generador-instaladores-linux-windows

This commit is contained in:
2026-07-30 07:34:17 -06:00
parent cafe3f1b87
commit c3f1d70e23
34 changed files with 3498 additions and 252 deletions

View File

@@ -1,5 +1,6 @@
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec onefile portable (ejecutar en Windows o Linux según destino)."""
import re
import sys
from pathlib import Path
@@ -8,9 +9,27 @@ ROOT = Path(SPECPATH).resolve().parent
platform = "windows" if sys.platform == "win32" else "linux"
bundled = ROOT / "packaging" / "bundled" / platform
def _read_app_version() -> str:
"""
Lee __version__ de app/__init__.py sin importar el paquete: el spec corre en el
intérprete de PyInstaller y no debe cargar las dependencias de la app.
"""
text = (ROOT / "app" / "__init__.py").read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', text, re.MULTILINE)
if not match:
raise SystemExit("No se pudo leer __version__ de app/__init__.py")
return match.group(1)
APP_VERSION = _read_app_version()
_assets = ROOT / "packaging" / "assets"
datas = [
(str(ROOT / "packaging" / "templates" / "env.default"), "packaging/templates"),
# bundled-versions.json va embebido para que el bootstrap sepa qué versiones de
# 7-Zip/ODBC trae este build y re-despliegue config/ cuando cambien.
(str(ROOT / "packaging" / "bundled-versions.json"), "packaging"),
]
if _assets.is_dir():
for _icon in _assets.iterdir():
@@ -89,6 +108,82 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
_icon_file = ROOT / "packaging" / "assets" / "tray-icon.ico"
_icon = str(_icon_file) if _icon_file.is_file() else None
def _version_quad(version: str) -> tuple:
"""VSVersionInfo exige exactamente 4 enteros; se rellena o recorta lo que haga falta."""
parts = []
for chunk in version.split(".")[:4]:
digits = "".join(c for c in chunk if c.isdigit())
parts.append(int(digits) if digits else 0)
while len(parts) < 4:
parts.append(0)
return tuple(parts)
def _version_info_dir():
"""
Directorio donde escribir el recurso de versión.
Se prefiere `workpath`, el directorio de trabajo que PyInstaller inyecta en el namespace
del spec, por dos razones concretas de este proyecto:
1. build.ps1 lo redirige a %LOCALAPPDATA%\\Temp cuando el repo está en una ruta UNC
(\\\\wsl.localhost\\...), que es el caso al compilar Windows desde WSL.
2. `build/` en la raíz del repo lo crea el build de Linux DENTRO DE DOCKER, así que
queda propiedad de root; el Python de Windows escribe por SMB como el usuario
normal y recibe PermissionError.
"""
candidate = globals().get("workpath")
if candidate:
return Path(candidate)
return ROOT / "build"
def _write_version_info(version: str) -> str:
"""
Genera el recurso de versión de Windows. Sin esto el .exe sale sin metadatos y las
Propiedades del archivo no muestran nada, lo que complica auditar qué versión está
instalada en un servidor.
"""
quad = _version_quad(version)
out = _version_info_dir() / "version_info.txt"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(
f"""VSVersionInfo(
ffi=FixedFileInfo(
filevers={quad},
prodvers={quad},
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0),
),
kids=[
StringFileInfo([
StringTable('080904B0', [
StringStruct('CompanyName', 'Aduanasoft'),
StringStruct('FileDescription', 'CloudRestoreAS - Restauracion automatica SQL Server'),
StringStruct('FileVersion', '{version}'),
StringStruct('InternalName', 'CloudRestoreAS'),
StringStruct('LegalCopyright', 'Aduanasoft'),
StringStruct('OriginalFilename', 'CloudRestoreAS.exe'),
StringStruct('ProductName', 'CloudRestoreAS'),
StringStruct('ProductVersion', '{version}'),
])
]),
VarFileInfo([VarStruct('Translation', [0x0809, 1200])]),
],
)
""",
encoding="utf-8",
)
return str(out)
# El recurso de versión solo existe en PE/Windows; en Linux PyInstaller lo ignoraría.
_version_info = _write_version_info(APP_VERSION) if platform == "windows" else None
exe = EXE(
pyz,
a.scripts,
@@ -98,6 +193,7 @@ exe = EXE(
[],
name="CloudRestoreAS",
icon=_icon,
version=_version_info,
debug=False,
bootloader_ignore_signals=False,
strip=False,