feat: mode application de bureau + packaging Windows autonome
Permet de distribuer l'application en executable Windows double-clic, sans Python/Node/terminal pour l'utilisateur final. - `desktop.py` : demarre le serveur sur un port libre dans un thread et ouvre une fenetre native (pywebview). `PLESNA_DEBUG=1` active les DevTools. Commande `plesna-gerance desktop` (import paresseux). - `paths.py` : resolution centralisee des chemins selon le contexte (dev, executable PyInstaller, surcharge par variables d'env). Les donnees vont dans un dossier utilisateur inscriptible (%APPDATA%), les ressources dans le bundle. connection.py/storage.py utilisent `get_data_dir()`, app.py `resource_path()`. - app.py : type MIME `.mjs` force (modules ES / worker PDF.js sous Windows). - Groupes de deps optionnels desktop / desktop-linux / build. - packaging/ : spec PyInstaller, build_windows.ps1, installeur Inno Setup. Workflow GitHub build-windows.yml (runner Windows). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
9
packaging/app_entry.py
Normal file
9
packaging/app_entry.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Point d'entrée de l'exécutable empaqueté (PyInstaller).
|
||||
|
||||
Lance l'application en mode bureau (fenêtre native).
|
||||
"""
|
||||
|
||||
from plesna_gerance.desktop import run
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
53
packaging/build_windows.ps1
Normal file
53
packaging/build_windows.ps1
Normal file
@@ -0,0 +1,53 @@
|
||||
# Build de l'application Windows autonome (Plesna Gérance).
|
||||
#
|
||||
# À lancer depuis la racine du projet, dans PowerShell, sur une machine Windows :
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File packaging\build_windows.ps1
|
||||
#
|
||||
# Prérequis sur la machine de build (PAS sur la machine de l'utilisateur final) :
|
||||
# - Python >= 3.10 et uv (https://docs.astral.sh/uv/)
|
||||
# - Node.js >= 22 (pour builder le frontend)
|
||||
# - (optionnel) Inno Setup 6 pour produire l'installeur .exe
|
||||
#
|
||||
# Résultat :
|
||||
# dist\PlesnaGerance.exe (exécutable autonome)
|
||||
# dist\PlesnaGerance-Setup.exe (installeur, si Inno Setup présent)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $Root
|
||||
|
||||
Write-Host "==> 1/4 Build du frontend" -ForegroundColor Cyan
|
||||
Set-Location (Join-Path $Root "frontend")
|
||||
npm ci
|
||||
npm run build
|
||||
Set-Location $Root
|
||||
|
||||
Write-Host "==> 2/4 Installation des dependances Python (desktop + build)" -ForegroundColor Cyan
|
||||
uv sync --group desktop --group build
|
||||
|
||||
Write-Host "==> 3/4 Empaquetage PyInstaller" -ForegroundColor Cyan
|
||||
uv run pyinstaller packaging\plesna_gerance.spec --noconfirm --clean
|
||||
|
||||
$Exe = Join-Path $Root "dist\PlesnaGerance.exe"
|
||||
if (-not (Test-Path $Exe)) {
|
||||
throw "Echec : $Exe introuvable."
|
||||
}
|
||||
Write-Host " OK -> $Exe" -ForegroundColor Green
|
||||
|
||||
Write-Host "==> 4/4 Construction de l'installeur (Inno Setup)" -ForegroundColor Cyan
|
||||
$IsccCandidates = @(
|
||||
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
|
||||
"${env:ProgramFiles}\Inno Setup 6\ISCC.exe"
|
||||
)
|
||||
$Iscc = $IsccCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
|
||||
if ($Iscc) {
|
||||
& $Iscc "packaging\installer.iss"
|
||||
Write-Host " OK -> dist\PlesnaGerance-Setup.exe" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Inno Setup non trouve : etape installeur ignoree." -ForegroundColor Yellow
|
||||
Write-Host " (L'executable dist\PlesnaGerance.exe est utilisable tel quel.)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "`nTermine." -ForegroundColor Green
|
||||
45
packaging/installer.iss
Normal file
45
packaging/installer.iss
Normal file
@@ -0,0 +1,45 @@
|
||||
; Script Inno Setup pour Plesna Gérance.
|
||||
; Compile avec : ISCC.exe packaging\installer.iss
|
||||
;
|
||||
; Installation par utilisateur (pas de droits administrateur requis) :
|
||||
; idéal pour un poste personnel. Crée un raccourci Bureau et menu Démarrer.
|
||||
|
||||
#define AppName "Plesna Gérance"
|
||||
#define AppVersion "0.1.0"
|
||||
#define AppPublisher "Plesna"
|
||||
#define AppExeName "PlesnaGerance.exe"
|
||||
|
||||
[Setup]
|
||||
AppId={{B2F8C0E2-7A3D-4C1E-9E2A-PLESNAGERANCE}}
|
||||
AppName={#AppName}
|
||||
AppVersion={#AppVersion}
|
||||
AppPublisher={#AppPublisher}
|
||||
DefaultDirName={autopf}\PlesnaGerance
|
||||
DefaultGroupName={#AppName}
|
||||
DisableProgramGroupPage=yes
|
||||
; Installation par utilisateur courant -> aucun prompt UAC administrateur.
|
||||
PrivilegesRequired=lowest
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=PlesnaGerance-Setup
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
; Données utilisateur (DB, documents) -> %APPDATA%\PlesnaGerance, conservées
|
||||
; à la désinstallation (voir paths.get_data_dir).
|
||||
|
||||
[Languages]
|
||||
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "Créer un raccourci sur le Bureau"; GroupDescription: "Raccourcis :"; Flags: checkedonce
|
||||
|
||||
[Files]
|
||||
Source: "..\dist\PlesnaGerance.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
|
||||
Name: "{group}\Désinstaller {#AppName}"; Filename: "{uninstallexe}"
|
||||
Name: "{userdesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#AppExeName}"; Description: "Lancer {#AppName}"; Flags: nowait postinstall skipifsilent
|
||||
83
packaging/plesna_gerance.spec
Normal file
83
packaging/plesna_gerance.spec
Normal file
@@ -0,0 +1,83 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
"""Spec PyInstaller pour Plesna Gérance (application de bureau autonome).
|
||||
|
||||
À lancer depuis la racine du projet :
|
||||
|
||||
pyinstaller packaging/plesna_gerance.spec
|
||||
|
||||
Produit un exécutable unique (onefile, sans console) qui embarque :
|
||||
- le code Python et ses dépendances (FastAPI, uvicorn, pdfplumber, pywebview) ;
|
||||
- le frontend déjà buildé (``frontend/dist``).
|
||||
|
||||
Le frontend doit être buildé AVANT (``cd frontend && npm run build``).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PyInstaller.utils.hooks import collect_all, collect_submodules
|
||||
|
||||
# Racine du projet (le .spec est dans packaging/).
|
||||
ROOT = Path(SPECPATH).resolve().parent
|
||||
|
||||
# --- Ressources embarquées ------------------------------------------------
|
||||
datas = [
|
||||
# Frontend buildé -> extrait dans _MEIPASS/frontend/dist (cf. paths.resource_path)
|
||||
(str(ROOT / "frontend" / "dist"), "frontend/dist"),
|
||||
]
|
||||
binaries = []
|
||||
hiddenimports = []
|
||||
|
||||
# Dépendances à embarquer en totalité (modules dynamiques mal détectés).
|
||||
for pkg in ("uvicorn", "pdfplumber", "pdfminer", "pypdfium2"):
|
||||
pkg_datas, pkg_binaries, pkg_hidden = collect_all(pkg)
|
||||
datas += pkg_datas
|
||||
binaries += pkg_binaries
|
||||
hiddenimports += pkg_hidden
|
||||
|
||||
# uvicorn charge ses protocoles/boucles par import dynamique.
|
||||
hiddenimports += collect_submodules("uvicorn")
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
[str(ROOT / "packaging" / "app_entry.py")],
|
||||
pathex=[str(ROOT / "src")],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=["tkinter"],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name="PlesnaGerance",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False, # application fenêtrée, pas de console
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=str(ROOT / "packaging" / "icon.ico")
|
||||
if (ROOT / "packaging" / "icon.ico").exists()
|
||||
else None,
|
||||
)
|
||||
Reference in New Issue
Block a user