diff --git a/README.md b/README.md index 6a684b1..49009cd 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,33 @@ Una entrada `[[game]]` por juego. Campos: | `info_url` | no | API Gitea del repo (def. derivada de `clone_url`) | | `icon_rel` | no | ruta del icono dentro del repo (def. `release/icons/icon.png`) | -## Compilar a binario (Nuitka) +## Compilar a binario (Nuitka, onefile) + +`build.sh` lo hace todo: crea el `.venv`, instala dependencias (PySide6 + Nuitka + +zstandard) y compila un único ejecutable comprimido, empaquetándolo en +`dist/jlauncher-v--.tar.gz` junto a `games.toml`. ```bash -pip install nuitka PySide6 ./build.sh -# binario en dist/jlauncher.dist/jlauncher +# binario: dist/jlauncher (+ dist/games.toml) ``` + +El binario crea `jlauncher_data/` y `settings.json` **junto a sí mismo** (resuelto vía +`NUITKA_ONEFILE_DIRECTORY`). El punto de entrada para empaquetar es `app.py` (desde +fuente se ejecuta con `python -m jlauncher`). + +### Prerequisitos del sistema (no los instala el script) + +- **Python 3.11+** (usa `tomllib`). +- Un **compilador C**: + - Linux: `gcc` y `patchelf` (p. ej. `apt install build-essential patchelf python3-dev`). + - macOS: **Xcode Command Line Tools** (`xcode-select --install`); aquí *no* hace falta + patchelf (Nuitka usa `install_name_tool`). +- `git` en el PATH. + +### macOS + +Compila en el propio Mac (Nuitka no compila cruzado): `./build.sh` genera +`jlauncher-v…-darwin-arm64.tar.gz`. Como el binario no va firmado, la primera vez quizá +debas hacer `xattr -dr com.apple.quarantine jlauncher` o abrirlo con clic derecho → Abrir. +Lánzalo desde terminal (`./jlauncher`). diff --git a/app.py b/app.py new file mode 100644 index 0000000..9d94731 --- /dev/null +++ b/app.py @@ -0,0 +1,11 @@ +"""Punto de entrada para empaquetar con Nuitka. + +Importa el paquete ``jlauncher`` con imports absolutos para que los imports relativos +internos (``from .config import …``) resuelvan correctamente al compilar. Para ejecutar +desde fuente sigue valiendo ``python -m jlauncher``. +""" + +from jlauncher.__main__ import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 566b746..031f813 --- a/build.sh +++ b/build.sh @@ -1,19 +1,63 @@ #!/usr/bin/env bash -# Compila jlauncher a un binario nativo (C, vía Nuitka). -# Requiere: pip install nuitka PySide6 +# Compila jlauncher a un binario standalone con Nuitka y empaqueta un tar.gz de release. +# Requisitos del sistema: python3-dev, gcc, patchelf (ver README). set -euo pipefail -cd "$(dirname "$0")" +HERE="$(cd "$(dirname "$0")" && pwd)" +cd "$HERE" -python -m nuitka \ - --standalone \ - --assume-yes-for-downloads \ - --enable-plugin=pyside6 \ - --output-dir=dist \ - --output-filename=jlauncher \ - --include-data-files=games.toml=games.toml \ - jlauncher +VERSION="$(sed -n 's/^__version__[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' jlauncher/__init__.py | head -n1)" +if [ -z "$VERSION" ]; then + echo "[build] no se pudo leer __version__ de jlauncher/__init__.py" >&2 + exit 1 +fi +ARCH="$(uname -m)" +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +RELEASE_NAME="jlauncher-v${VERSION}-${OS}-${ARCH}" -echo -echo "Listo. Binario en: dist/jlauncher.dist/jlauncher" -echo "games.toml se incluye junto al binario; jlauncher_data/ se creará al lado al ejecutar." +if [ ! -d .venv ]; then + echo "[build] creando venv…" + python3 -m venv .venv + .venv/bin/pip install --quiet --upgrade pip +fi + +echo "[build] sincronizando dependencias…" +.venv/bin/pip install --quiet -r requirements.txt + +if ! .venv/bin/python -c "import nuitka" 2>/dev/null; then + echo "[build] instalando nuitka en el venv…" + .venv/bin/pip install --quiet "nuitka[onefile]" +fi + +# zstandard habilita la compresión del onefile (binario mucho más pequeño). +if ! .venv/bin/python -c "import zstandard" 2>/dev/null; then + echo "[build] instalando zstandard (compresión onefile)…" + .venv/bin/pip install --quiet zstandard +fi + +echo "[build] versión: v${VERSION}" +echo "[build] limpiando artefactos previos…" +rm -rf dist build app.build app.dist app.onefile-build + +echo "[build] compilando (PySide6 onefile; puede tardar varios minutos)…" +.venv/bin/python -m nuitka \ + --onefile \ + --assume-yes-for-downloads \ + --enable-plugin=pyside6 \ + --include-package=jlauncher \ + --output-dir=dist \ + --output-filename=jlauncher \ + --remove-output \ + --lto=yes \ + app.py + +echo "[build] copiando games.toml junto al binario…" +cp games.toml dist/games.toml + +echo "[build] empaquetando release ${RELEASE_NAME}.tar.gz…" +tar -czf "dist/${RELEASE_NAME}.tar.gz" -C dist jlauncher games.toml + +echo "[build] hecho:" +ls -lh "dist/jlauncher" "dist/games.toml" "dist/${RELEASE_NAME}.tar.gz" +echo "[build] el binario crea jlauncher_data/ y settings.json junto a sí mismo." +echo "[build] distribuir: descomprimir el tar.gz (jlauncher + games.toml juntos)." diff --git a/jlauncher/paths.py b/jlauncher/paths.py index 7b628bc..0ae48bb 100644 --- a/jlauncher/paths.py +++ b/jlauncher/paths.py @@ -1,12 +1,14 @@ """Resolución de rutas: dónde está games.toml y dónde guardar los datos. -Cuando se compila con Nuitka (``--standalone``) el atributo global ``__compiled__`` -existe, así que usamos la carpeta del ejecutable. Ejecutando desde fuente usamos la -raíz del proyecto (la carpeta que contiene el paquete ``jlauncher``). +Compilado con Nuitka, ``__compiled__`` existe. En modo ``--onefile`` la carpeta del +binario real la expone ``NUITKA_ONEFILE_DIRECTORY`` (Nuitka 4.x); con versiones que usan +``NUITKA_ONEFILE_BINARY`` tomamos su carpeta; si no, ``sys.executable`` (standalone). +Ejecutando desde fuente usamos la raíz del proyecto (la carpeta que contiene ``jlauncher``). """ from __future__ import annotations +import os import sys from pathlib import Path @@ -21,6 +23,12 @@ def is_compiled() -> bool: def base_dir() -> Path: """Carpeta base junto a la que viven games.toml y jlauncher_data.""" if is_compiled(): + directory = os.environ.get("NUITKA_ONEFILE_DIRECTORY") + if directory: + return Path(directory).resolve() + binary = os.environ.get("NUITKA_ONEFILE_BINARY") + if binary: + return Path(binary).resolve().parent return Path(sys.executable).resolve().parent # Desde fuente: raíz del proyecto = padre del paquete jlauncher/ return Path(__file__).resolve().parent.parent