diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index d037708..86a9a76 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -1,12 +1,12 @@ name: Desktop installers -# Produit les installeurs de BUREAU (.msi Windows pour l'instant) et les publie +# Produit les installeurs de BUREAU (.msi Windows + AppImage Linux) et les publie # en tant qu'assets d'une GitHub Release, sur tag `v*`. # # Complementaire au pipeline Gitea Actions (.gitea/workflows/release.yml) qui, -# lui, build et pousse les IMAGES Docker. Ici on est sur GitHub car jpackage et -# PyInstaller ne savent PAS cross-compiler : le .msi DOIT etre construit sur un -# runner Windows, et GitHub en fournit gratuitement (windows-latest). +# lui, build et pousse les IMAGES Docker. Ici on est sur GitHub car jpackage ne +# sait PAS cross-compiler : le .msi DOIT etre construit sur un runner Windows et +# l'AppImage sur un runner Linux — GitHub fournit les deux gratuitement. # # Prerequis : le depot Gitea doit etre mirrore vers GitHub (push mirror, tags # inclus) pour que le tag declenche ce workflow. @@ -191,6 +191,68 @@ jobs: path: core/target/dist-out/*.msi retention-days: 90 - # TODO (plus tard) : job `linux` sur ubuntu-latest produisant un AppImage - # (jpackage --type app-image + appimagetool) + PyInstaller Linux du Brain, - # attache a la MEME release. Reutilise la meme matrice / les memes etapes. + # LINUX : AppImage (1 fichier, toutes distros) attache a la MEME release. + # Tourne sur ubuntu-latest (jpackage ne cross-compile pas -> build natif Linux). + # Brain empaquete via python-build-standalone (pas de Python embeddable Linux + # officiel ; pas de PyInstaller -> pas de faux positif AV). Memes regles de + # publication que windows : stable -> release publique ; beta -> artefact prive. + linux: + needs: tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && format('v{0}', inputs.version) || github.ref }} + + # Apporte jpackage (empaquetage natif) dans le PATH. + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + # Pas de setup-python : le Brain utilise SON python-build-standalone + # (telecharge par le script, avec son propre pip). Le host n'en a pas besoin. + + # Version (numerique X.Y.Z) + tag + isbeta, comme le job windows. + - name: Derive version + id: ver + run: | + if [ '${{ github.event_name }}' = 'workflow_dispatch' ]; then + raw='${{ inputs.version }}' + else + raw='${{ github.ref_name }}' + fi + raw="${raw#v}" # 0.15.0 ou 0.15.0-beta + num="${raw%%-*}" # 0.15.0 + case "$raw" in *-beta*) isbeta=true ;; *) isbeta=false ;; esac + echo "version=$num" >> "$GITHUB_OUTPUT" + echo "tag=v$raw" >> "$GITHUB_OUTPUT" + echo "isbeta=$isbeta" >> "$GITHUB_OUTPUT" + + - name: Build Linux AppImage + run: bash ./installers/desktop/build-linux.sh --version ${{ steps.ver.outputs.version }} + + # STABLE uniquement : attache l'AppImage a la release publique (meme tag que + # le .msi -> les deux installeurs sur la meme release). + - name: Publish AppImage to GitHub Release (stable) + if: ${{ steps.ver.outputs.isbeta == 'false' }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.ver.outputs.tag }} + files: core/target/dist-out/*.AppImage + fail_on_unmatched_files: true + + # BETA uniquement : artefact PRIVE (pas de release publique). + - name: Upload AppImage as private artifact (beta) + if: ${{ steps.ver.outputs.isbeta == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: loremind-beta-${{ steps.ver.outputs.version }}-appimage + path: core/target/dist-out/*.AppImage + retention-days: 90 diff --git a/brain/app/main.py b/brain/app/main.py index 38b3e58..772b34b 100644 --- a/brain/app/main.py +++ b/brain/app/main.py @@ -26,7 +26,7 @@ from app.infrastructure.ollama_model_installer import ensure_ollama_embedding_mo app = FastAPI( title="LoreMind Brain", description="Backend IA pour la génération de contenu narratif.", - version="0.17.0", + version="0.17.1-beta", ) logger = logging.getLogger(__name__) diff --git a/brain/run_local.py b/brain/run_local.py index c0fa422..341d441 100644 --- a/brain/run_local.py +++ b/brain/run_local.py @@ -23,7 +23,11 @@ sys.path.insert(0, _HERE) # AVANT que l'app n'importe le pdf_extractor (qui détecte la version au chargement). # tessdata (fra+eng) est embarqué dans tesseract/tessdata. Sans ce bloc, l'OCR # reste désactivé en dégradation gracieuse (PDF born-digital OK, scans signalés). -_TESS = os.path.join(_HERE, "tesseract", "tesseract.exe") +# Binaire selon l'OS : tesseract.exe (Windows embeddable) ou tesseract (Linux/Mac). +# Si aucun Tesseract n'est bundlé (cas Linux/AppImage par défaut), le bloc est +# sauté et pytesseract retombe sur le tesseract SYSTÈME du PATH (ex. apt install +# tesseract-ocr) — sinon OCR désactivé en dégradation gracieuse. +_TESS = os.path.join(_HERE, "tesseract", "tesseract.exe" if os.name == "nt" else "tesseract") if os.path.exists(_TESS): os.environ.setdefault("TESSDATA_PREFIX", os.path.join(_HERE, "tesseract")) try: diff --git a/core/pom.xml b/core/pom.xml index df90bbe..8c51ab8 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -14,7 +14,7 @@ com.loremind loremind-core - 0.17.0 + 0.17.1-beta LoreMind Core Backend Core - Architecture Hexagonale diff --git a/installers/desktop/app-icon.png b/installers/desktop/app-icon.png new file mode 100644 index 0000000..7fcbd2a Binary files /dev/null and b/installers/desktop/app-icon.png differ diff --git a/installers/desktop/build-linux.sh b/installers/desktop/build-linux.sh new file mode 100644 index 0000000..52d7f0b --- /dev/null +++ b/installers/desktop/build-linux.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# +# Construit l'application de BUREAU Linux de DM Loremind sous forme d'AppImage, +# sans Docker. Equivalent Linux de installers/desktop/build-windows.ps1. +# +# Pipeline complet "local-first" : +# 1. Build du front Angular (web/ -> web/dist/web) +# 2. Prep du Brain (Python standalone) (brain/ -> dist-embed-linux : python relocatable + deps + sources) +# 3. Build du Core en fat jar + front (core/ -> target/*.jar, profil Maven "desktop") +# 4. Assemblage de la charge utile (jar + brain) dans un dossier d'entree jpackage +# 5. jpackage --type app-image -> image applicative + JRE embarque +# 6. appimagetool -> DM_Loremind-x86_64.AppImage (1 fichier, toutes distros) +# +# L'app resultante se lance d'un double-clic (chmod +x puis ./*.AppImage) : le Core +# demarre en profil Spring "local" (H2 fichier + stockage filesystem) et lance lui-meme +# le Brain en sidecar. Aucune dependance externe (ni Docker, ni Java, ni Python). +# +# PREREQUIS (machine de BUILD Linux uniquement, PAS chez l'utilisateur final) : +# - JDK 21+ avec jpackage dans le PATH (Temurin OK). +# - Node.js + npm (build Angular) ; Maven via le wrapper du repo. +# - curl, tar, et FUSE *ou* (sur CI sans FUSE) on lance appimagetool en +# --appimage-extract-and-run (gere automatiquement ci-dessous). +# - Internet (telecharge python-build-standalone + appimagetool). +# +# IMPORTANT : jpackage NE cross-compile PAS. Ce script DOIT tourner sur Linux +# (machine, WSL, ou runner ubuntu-latest). Il ne produira rien d'utile sous Windows. +# +# Projet : DM Loremind — assistant pour Maitres de Jeu de JDR — Licence AGPL-3.0 +set -euo pipefail + +# --- Args ------------------------------------------------------------------ +VERSION="" +SKIP_FRONT=0; SKIP_BRAIN=0; SKIP_JAR=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --version) VERSION="$2"; shift 2 ;; + --skip-front) SKIP_FRONT=1; shift ;; + --skip-brain) SKIP_BRAIN=1; shift ;; + --skip-jar) SKIP_JAR=1; shift ;; + *) echo "Option inconnue : $1" >&2; exit 2 ;; + esac +done + +step() { printf '\033[36m==> %s\033[0m\n' "$1"; } +ok() { printf '\033[32m OK %s\033[0m\n' "$1"; } +err() { printf '\033[31m XX %s\033[0m\n' "$1" >&2; } + +# --- Chemins --------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +WEB_DIR="$REPO_ROOT/web" +BRAIN_DIR="$REPO_ROOT/brain" +CORE_DIR="$REPO_ROOT/core" +STAGE_DIR="$CORE_DIR/target/dist-input" # charge utile jpackage +OUT_DIR="$CORE_DIR/target/dist-out" # image applicative + AppImage produits +BRAIN_EMBED="$BRAIN_DIR/dist-embed-linux" # staging du brain empaquete (Linux) +ICON_PNG="$SCRIPT_DIR/app-icon.png" # icone de l'app (jpackage Linux exige un PNG) +APP_NAME="DM Loremind" + +# python-build-standalone (Astral) : equivalent Linux du Python embeddable de +# python.org (qui n'existe que sous Windows). Build "install_only" = Python complet +# RELOCATABLE (pip inclus), donc on installe les deps directement dedans. +PY_MINOR="3.12" # doit matcher python:3.12 du Docker + +# --- Version (numerique) --------------------------------------------------- +if [[ -z "$VERSION" ]]; then + # Viser la version DU PROJET (artifactId loremind-core), pas le du parent. + VERSION="$(grep -oPz 'loremind-core\s*\K[0-9]+\.[0-9]+\.[0-9]+' \ + "$CORE_DIR/pom.xml" | tr -d '\0' | head -1 || true)" + [[ -z "$VERSION" ]] && VERSION="0.0.0" +fi + +echo +printf '\033[35m============================================================\033[0m\n' +printf '\033[35m DM Loremind - Build AppImage Linux (v%s)\033[0m\n' "$VERSION" +printf '\033[35m============================================================\033[0m\n' + +# --- Verif outils ---------------------------------------------------------- +command -v jpackage >/dev/null 2>&1 || { err "jpackage introuvable dans le PATH. Installez un JDK 21+ (Temurin)."; exit 1; } + +# --- 1. Front Angular ------------------------------------------------------ +if [[ $SKIP_FRONT -eq 0 ]]; then + step "Build du front Angular" + ( cd "$WEB_DIR" + [[ -d node_modules ]] || npm ci + npm run build ) + ok "Front construit (web/dist/web)" +else step "Front : saute (--skip-front)"; fi + +# --- 2. Brain (Python standalone relocatable, PAS d'exe gele) -------------- +if [[ $SKIP_BRAIN -eq 0 ]]; then + step "Preparation du Brain (python-build-standalone $PY_MINOR)" + rm -rf "$BRAIN_EMBED"; mkdir -p "$BRAIN_EMBED" + + # a) Resoudre l'asset install_only linux-gnu pour $PY_MINOR dans la derniere + # release PBS (la date du tag change a chaque release -> on la decouvre). + step " Resolution de l'archive python-build-standalone" + asset_url="$(curl -fsSL https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest \ + | grep -oE "https://[^\"]*cpython-${PY_MINOR//./\\.}\.[0-9]+\+[0-9]+-x86_64-unknown-linux-gnu-install_only\.tar\.gz" \ + | head -1)" + [[ -z "$asset_url" ]] && { err "Archive python-build-standalone introuvable pour $PY_MINOR."; exit 1; } + echo " Telechargement $asset_url" + curl -fsSL "$asset_url" -o "$BRAIN_EMBED/python.tar.gz" + # L'archive install_only s'extrait en un dossier 'python/' (bin/python3, lib/...). + tar -xzf "$BRAIN_EMBED/python.tar.gz" -C "$BRAIN_EMBED" + rm -f "$BRAIN_EMBED/python.tar.gz" + PYBIN="$BRAIN_EMBED/python/bin/python3" + [[ -x "$PYBIN" ]] || { err "python3 introuvable apres extraction ($PYBIN)."; exit 1; } + + # b) Installer les deps DANS le python standalone (site-packages interne). + # Python complet => pip fonctionne directement, pas de bricolage ._pth/--target. + "$PYBIN" -m pip install --upgrade pip >/dev/null + "$PYBIN" -m pip install -r "$BRAIN_DIR/requirements.txt" + + # c) Copier les sources du Brain + le point d'entree. + cp -r "$BRAIN_DIR/app" "$BRAIN_EMBED/app" + cp "$BRAIN_DIR/run_local.py" "$BRAIN_EMBED/run_local.py" + + # d) OCR (Tesseract) : pas bundle pour l'instant sous Linux (libs partagees + # lourdes a embarquer proprement). Degradation gracieuse : si l'utilisateur a + # 'tesseract-ocr' (apt/dnf), pytesseract le trouve sur le PATH et l'OCR des + # scans marche ; sinon PDF born-digital OK, scans signales. Cf. run_local.py. + ok "Brain prepare (brain/dist-embed-linux : python standalone + deps + sources)" +else step "Brain : saute (--skip-brain)"; fi + +# --- 3. Core (fat jar avec front embarque) --------------------------------- +if [[ $SKIP_JAR -eq 0 ]]; then + step "Build du Core (fat jar, profil desktop)" + ( cd "$CORE_DIR" + chmod +x ./mvnw + # -Pdesktop : copie web/dist/web dans le jar (classpath:/static). + ./mvnw -q -Pdesktop -DskipTests clean package ) + ok "Core construit (core/target)" +else step "Core : saute (--skip-jar)"; fi + +# --- 4. Assemblage de la charge utile -------------------------------------- +step "Assemblage de la charge utile jpackage" +rm -rf "$STAGE_DIR"; mkdir -p "$STAGE_DIR" + +# Le jar repackage Spring Boot (executable) — on ignore le *.jar.original. +jar="$(find "$CORE_DIR/target" -maxdepth 1 -name 'loremind-core-*.jar' ! -name '*.original' | head -1)" +[[ -z "$jar" ]] && { err "Jar introuvable dans core/target. Relancez sans --skip-jar."; exit 1; } +cp "$jar" "$STAGE_DIR/loremind-core.jar" + +# Le Brain (python standalone + deps + sources) -> stage/brain/ +[[ -d "$BRAIN_EMBED/python" ]] || { err "Brain introuvable (brain/dist-embed-linux). Relancez sans --skip-brain."; exit 1; } +mkdir -p "$STAGE_DIR/brain" +cp -r "$BRAIN_EMBED/." "$STAGE_DIR/brain/" +ok "Charge utile prete ($STAGE_DIR)" + +# --- 5. jpackage -> app-image ---------------------------------------------- +step "Generation de l'image applicative (app-image) via jpackage" +[[ -f "$ICON_PNG" ]] || { err "Icone manquante : $ICON_PNG (PNG requis sous Linux)."; exit 1; } +rm -rf "$OUT_DIR"; mkdir -p "$OUT_DIR" + +# $APPDIR : substitue par jpackage AU LANCEMENT par le dossier applicatif +# (lib/app), qui contient le jar ET le dossier brain copie depuis --input. +# Le Brain se lance via python3 + run_local.py. brain.sidecar.command est une +# LISTE : les deux chemins separes par une virgule (aucun chemin Linux n'en +# contient) sont bindes en List par Spring. NB: $APPDIR doit rester +# LITTERAL ici (quote simple) — c'est jpackage, pas bash, qui le resout. +BRAIN_CMD='$APPDIR/brain/python/bin/python3,$APPDIR/brain/run_local.py' + +jpackage \ + --type app-image \ + --name "$APP_NAME" \ + --app-version "$VERSION" \ + --vendor 'IGML Creation' \ + --icon "$ICON_PNG" \ + --input "$STAGE_DIR" \ + --main-jar loremind-core.jar \ + --main-class org.springframework.boot.loader.launch.JarLauncher \ + --dest "$OUT_DIR" \ + --java-options '-Dspring.profiles.active=local' \ + --java-options "-Dbrain.sidecar.command=$BRAIN_CMD" + +APPIMG_SRC="$OUT_DIR/$APP_NAME" # dossier produit par jpackage (avec espace) +[[ -d "$APPIMG_SRC" ]] || { err "app-image jpackage introuvable ($APPIMG_SRC)."; exit 1; } +ok "Image applicative prete ($APPIMG_SRC)" + +# --- 6. AppImage via appimagetool ------------------------------------------ +step "Empaquetage AppImage (toutes distros)" +APPDIR="$OUT_DIR/AppDir" +rm -rf "$APPDIR"; mkdir -p "$APPDIR/usr" + +# a) L'app-image jpackage va sous AppDir/usr/ (le lanceur bin/* resout lib/ en +# relatif via ../lib -> reste coherent). +cp -r "$APPIMG_SRC/." "$APPDIR/usr/" + +# b) Icone a la racine de l'AppDir (nom = Icon= du .desktop, SANS extension). +cp "$ICON_PNG" "$APPDIR/dm-loremind.png" +mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps" +cp "$ICON_PNG" "$APPDIR/usr/share/icons/hicolor/256x256/apps/dm-loremind.png" + +# c) Fichier .desktop (a la racine + copie standard). +cat > "$APPDIR/dm-loremind.desktop" < lance le binaire jpackage. +cat > "$APPDIR/AppRun" <<'EOF' +#!/bin/sh +HERE="$(dirname "$(readlink -f "$0")")" +exec "$HERE/usr/bin/DM Loremind" "$@" +EOF +chmod +x "$APPDIR/AppRun" + +# e) appimagetool (telecharge si absent). --appimage-extract-and-run : evite +# d'exiger FUSE (absent des runners CI). +TOOL="$OUT_DIR/appimagetool-x86_64.AppImage" +if [[ ! -x "$TOOL" ]]; then + curl -fsSL -o "$TOOL" \ + https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + chmod +x "$TOOL" +fi + +# APPIMAGE_EXTRACT_AND_RUN : permet d'EXECUTER l'AppImage appimagetool SANS FUSE +# (absent des runners CI) — il se self-extrait au lieu de se monter via fuse. +OUT_FILE="$OUT_DIR/DM_Loremind-${VERSION}-x86_64.AppImage" +ARCH=x86_64 APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --appimage-extract-and-run "$APPDIR" "$OUT_FILE" +chmod +x "$OUT_FILE" + +echo +printf '\033[32m============================================================\033[0m\n' +printf '\033[32m AppImage genere !\033[0m\n' +printf '\033[32m %s\033[0m\n' "$OUT_FILE" +printf '\033[32m============================================================\033[0m\n' diff --git a/web/package-lock.json b/web/package-lock.json index 84dcf3f..6406328 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "loremind-web", - "version": "0.17.0", + "version": "0.17.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "loremind-web", - "version": "0.17.0", + "version": "0.17.1-beta", "dependencies": { "@angular/animations": "^21.2.16", "@angular/common": "^21.2.16", diff --git a/web/package.json b/web/package.json index 55e01d4..50671f1 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "loremind-web", - "version": "0.17.0", + "version": "0.17.1-beta", "description": "LoreMind Frontend - Angular", "scripts": { "ng": "ng",