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",