TranscriptStation/build.sh

784 lines
32 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# ╔══════════════════════════════════════════════════════════════════════════╗
# ║ build.sh Compilation + installation de TranscribeStation ║
# ║ JT-Tools by Johnny · H3Campus · Mars 2026 ║
# ╚══════════════════════════════════════════════════════════════════════════╝
#
# Usage :
# ./build.sh Build Linux + raccourci GNOME (défaut)
# ./build.sh --linux Build Linux uniquement
# ./build.sh --windows Génère build_windows.bat (cross depuis Linux)
# ./build.sh --both Linux + Windows
# ./build.sh --no-install Build sans créer de raccourcis
# ./build.sh --install-only Raccourci seulement (binaire déjà compilé)
# ./build.sh --uninstall Supprime le raccourci GNOME
# ./build.sh --clean Supprime build/ dist/ icon.*
# ./build.sh --help Affiche cette aide
#
set -euo pipefail
IFS=$'\n\t'
# ── Constantes ─────────────────────────────────────────────────────────────────
APP_NAME="TranscribeStation"
APP_VERSION="1.0.0"
APP_COMMENT="Gestionnaire de transcription audio JT-Tools by Johnny"
APP_COMMENT_WIN="Gestionnaire de transcription audio - JT-Tools by Johnny"
APP_CATS="Audio;AudioVideo;Office;"
APP_KEYWORDS="transcription;audio;dictée;pédalier;olympus;"
MAIN_SCRIPT="transcribe_station.py"
ICON_PNG="icon.png"
ICON_ICO="icon.ico"
DIST_LINUX="dist/linux"
DIST_WINDOWS="dist/windows"
VENV_DIR=".venv"
HIDDEN_IMPORTS=(
"PyQt6.QtMultimedia"
"PyQt6.QtCore"
"PyQt6.QtGui"
"PyQt6.QtWidgets"
"soundfile"
"numpy"
"hid"
"cffi"
"_cffi_backend"
)
# ── Couleurs ───────────────────────────────────────────────────────────────────
R='\033[0;31m' G='\033[0;32m' Y='\033[1;33m'
C='\033[0;36m' B='\033[1m' N='\033[0m'
ok() { echo -e "${G}[OK]${N} $*"; }
info() { echo -e "${C}[..]${N} $*"; }
warn() { echo -e "${Y}[!!]${N} $*"; }
err() { echo -e "${R}[XX]${N} $*" >&2; }
step() { echo -e "\n${B}══ $* ══${N}"; }
die() { err "$*"; exit 1; }
# ── Détection Python / venv ────────────────────────────────────────────────────
find_python() {
if [[ -f "$VENV_DIR/bin/python" ]]; then
echo "$VENV_DIR/bin/python"
elif command -v python3 &>/dev/null; then
echo "$(command -v python3)"
else
die "python3 introuvable. Installez Python 3.10+."
fi
}
find_pip() {
local py; py=$(find_python)
echo "$py -m pip"
}
# ── Validation PyQt6 dans Wine ────────────────────────────────────────────────
check_wine_pyqt_runtime() {
local wine_cmd="$1"
local wine_python="$2"
local wine_run=("env" "WINEDEBUG=-all,err+all" "$wine_cmd")
local probe_output
local rc
probe_output=$("${wine_run[@]}" "$wine_python" -c $'from pathlib import Path\nimport importlib.metadata as md\nimport PyQt6\nqt_dir = Path(PyQt6.__file__).resolve().parent / "Qt6" / "bin"\nmissing = [name for name in ("icuuc.dll", "icuin.dll", "icudt.dll") if not (qt_dir / name).exists()]\nprint("PYQT6_VERSION=" + md.version("PyQt6"))\ntry:\n print("PYQT6_QT6_VERSION=" + md.version("PyQt6-Qt6"))\nexcept Exception:\n print("PYQT6_QT6_VERSION=?")\nprint("QT_BIN=" + str(qt_dir).replace(chr(92), "/"))\nprint("MISSING_DLLS=" + ",".join(missing))\nfrom PyQt6 import QtCore\nprint("QTCORE_OK=1")' 2>&1)
rc=$?
while IFS= read -r line; do
[[ -n "$line" ]] && info "Wine Qt probe : $line"
done <<< "$probe_output"
if [[ $rc -ne 0 ]]; then
local missing_dlls=""
missing_dlls=$(printf '%s\n' "$probe_output" | sed -n 's/^MISSING_DLLS=//p' | tail -n1)
err "PyQt6 est installé dans Wine, mais QtCore ne se charge pas."
[[ -n "$missing_dlls" ]] && err "DLL Qt manquantes dans le wheel Windows : $missing_dlls"
err "Le binaire Windows généré via Wine serait invalide."
err "Utilisez dist/windows/build_windows.bat sur Windows natif."
return 1
fi
}
# ── Génération des icônes via Python/Pillow ────────────────────────────────────
generate_icons() {
step "Génération des icônes"
local py; py=$(find_python)
if ! $py -c "import PIL" 2>/dev/null; then
info "Installation de Pillow..."
$py -m pip install --quiet Pillow
fi
info "Création de $ICON_PNG et $ICON_ICO..."
$py - << 'PYICON'
from PIL import Image, ImageDraw
import sys
def draw_logo(draw, sz):
draw.rounded_rectangle(
[0, 0, sz - 1, sz - 1],
radius=int(sz * 0.18),
fill=(59, 110, 220, 255),
)
bars = [0.25, 0.45, 0.70, 0.90, 0.65, 1.0, 0.55, 0.80, 0.40, 0.20]
n, mg = len(bars), sz * 0.13
bw = (sz - 2 * mg) / (n * 1.7)
gap = (sz - 2 * mg) / n
mid = sz / 2
for i, h in enumerate(bars):
x = mg + i * gap + (gap - bw) / 2
bh = h * sz * 0.36
draw.rounded_rectangle(
[x, mid - bh, x + bw, mid + bh],
radius=bw / 2,
fill=(255, 255, 255, 215),
)
sizes = [16, 32, 48, 64, 128, 256]
images = []
for sz in sizes:
img = Image.new("RGBA", (sz, sz), (0, 0, 0, 0))
draw_logo(ImageDraw.Draw(img), sz)
images.append(img)
images[-1].save("icon.png")
images[0].save("icon.ico", format="ICO",
sizes=[(s, s) for s in sizes],
append_images=images[1:])
print("OK")
PYICON
[[ -f "$ICON_PNG" ]] && ok "$ICON_PNG généré" || warn "$ICON_PNG absent (Pillow échoué ?)"
[[ -f "$ICON_ICO" ]] && ok "$ICON_ICO généré" || warn "$ICON_ICO absent"
}
# ── Build PyInstaller Linux ────────────────────────────────────────────────────
build_linux() {
step "Build Linux (PyInstaller --onefile)"
local py; py=$(find_python)
# Vérifier / installer PyInstaller
if ! $py -m PyInstaller --version &>/dev/null; then
info "Installation de PyInstaller..."
$py -m pip install --quiet pyinstaller
fi
generate_icons
# Construire les arguments hidden-import
local hi_args=()
for h in "${HIDDEN_IMPORTS[@]}"; do
hi_args+=("--hidden-import=$h")
done
# Argument icône (optionnel)
local icon_arg=()
[[ -f "$ICON_ICO" ]] && icon_arg=("--icon=$ICON_ICO")
info "Lancement de PyInstaller..."
$py -m PyInstaller \
--onefile \
--clean \
--noconfirm \
--name="$APP_NAME" \
--distpath="$DIST_LINUX" \
--workpath="build/linux" \
--specpath="build/linux" \
--log-level=WARN \
"${hi_args[@]}" \
"${icon_arg[@]}" \
"$MAIN_SCRIPT"
local binary="$DIST_LINUX/$APP_NAME"
if [[ -f "$binary" ]]; then
chmod +x "$binary"
local size
size=$(du -m "$binary" | cut -f1)
ok "Binaire : $binary (${size} Mo)"
# Copier l'icône PNG à côté du binaire (chemin absolu dans le .desktop)
if [[ -f "$ICON_PNG" ]]; then
cp "$ICON_PNG" "$DIST_LINUX/$APP_NAME.png"
ok "Icône copiée : $DIST_LINUX/$APP_NAME.png"
fi
else
die "Binaire introuvable dans $DIST_LINUX/"
fi
}
# ── Installation raccourci GNOME ───────────────────────────────────────────────
install_gnome_shortcut() {
step "Installation raccourci GNOME"
local binary
binary="$(pwd)/$DIST_LINUX/$APP_NAME"
[[ -f "$binary" ]] || die "Binaire introuvable : $binary (lancez d'abord --linux)"
# 1. Icône XDG hicolor (prioritaire) + fallback chemin absolu
local icon_dir="$HOME/.local/share/icons/hicolor/256x256/apps"
local icon_ref="$APP_NAME" # référence symbolique XDG (défaut)
local icon_sizes=("16x16" "32x32" "48x48" "64x64" "128x128" "256x256")
mkdir -p "$icon_dir"
# Chercher l'icône : priorité au dist/linux/ puis au répertoire courant
local icon_src=""
for candidate in "$DIST_LINUX/$APP_NAME.png" "$ICON_PNG"; do
[[ -f "$candidate" ]] && icon_src="$candidate" && break
done
if [[ -n "$icon_src" ]]; then
# Installer dans tous les formats XDG disponibles
for sz in "${icon_sizes[@]}"; do
local sz_dir="$HOME/.local/share/icons/hicolor/$sz/apps"
mkdir -p "$sz_dir"
done
cp "$icon_src" "$icon_dir/$APP_NAME.png"
ok "Icône XDG installée : $icon_dir/$APP_NAME.png"
# Fallback : chemin absolu si XDG n'est pas disponible
icon_ref="$icon_dir/$APP_NAME.png"
else
warn "Aucune icône trouvée — raccourci sans icône"
warn "Relancez après --linux pour générer $ICON_PNG"
fi
# 2. Fichier .desktop
local apps_dir="$HOME/.local/share/applications"
mkdir -p "$apps_dir"
local desktop="$apps_dir/$APP_NAME.desktop"
cat > "$desktop" << DESKTOP
[Desktop Entry]
Version=1.0
Type=Application
Name=$APP_NAME
GenericName=Transcription Audio
Comment=$APP_COMMENT
Exec=$binary
Icon=$icon_ref
Terminal=false
Categories=$APP_CATS
StartupNotify=true
StartupWMClass=$APP_NAME
Keywords=$APP_KEYWORDS
DESKTOP
chmod 755 "$desktop"
ok ".desktop installé : $desktop"
# 3. Raccourci bureau (Bureau / Desktop / Escritorio)
local placed=false
for bureau_name in "Bureau" "Desktop" "Escritorio"; do
local bureau_dir="$HOME/$bureau_name"
if [[ -d "$bureau_dir" ]]; then
cp "$desktop" "$bureau_dir/$APP_NAME.desktop"
chmod 755 "$bureau_dir/$APP_NAME.desktop"
ok "Raccourci bureau : $bureau_dir/$APP_NAME.desktop"
placed=true
break
fi
done
$placed || warn "Dossier Bureau/Desktop introuvable — raccourci bureau ignoré"
# 4. Mise à jour des bases de données
for cmd_args in \
"update-desktop-database $apps_dir" \
"gtk-update-icon-cache -f -t $HOME/.local/share/icons/hicolor" \
"xdg-desktop-menu forceupdate"
do
local cmd; cmd=$(echo "$cmd_args" | cut -d' ' -f1)
if command -v "$cmd" &>/dev/null; then
# shellcheck disable=SC2086
$cmd_args 2>/dev/null && info "Mise à jour : $cmd" || true
fi
done
ok "Raccourci GNOME installé — visible après reconnexion si nécessaire"
}
# ── Désinstallation raccourci GNOME ───────────────────────────────────────────
uninstall_gnome_shortcut() {
step "Désinstallation raccourci GNOME"
local removed=false
local targets=(
"$HOME/.local/share/applications/$APP_NAME.desktop"
"$HOME/.local/share/icons/hicolor/256x256/apps/$APP_NAME.png"
"$HOME/Bureau/$APP_NAME.desktop"
"$HOME/Desktop/$APP_NAME.desktop"
"$HOME/Escritorio/$APP_NAME.desktop"
)
for t in "${targets[@]}"; do
if [[ -e "$t" ]]; then
rm -f "$t"
ok "Supprimé : $t"
removed=true
fi
done
$removed || info "Aucun fichier à supprimer"
if command -v update-desktop-database &>/dev/null; then
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
fi
}
# ── Génération build_windows.bat ──────────────────────────────────────────────
build_windows() {
step "Génération du script Windows"
mkdir -p "$DIST_WINDOWS"
# Construire la liste des --hidden-import pour le .bat
local hi_bat=""
for h in "${HIDDEN_IMPORTS[@]}"; do
hi_bat+=" --hidden-import $h ^\n"
done
# Construire les arguments hidden-import pour le .bat
local hi_bat_args=""
for h in "${HIDDEN_IMPORTS[@]}"; do
hi_bat_args+=" --hidden-import $h ^\\"$'\n'
done
cat > "$DIST_WINDOWS/build_windows.bat" << WINBAT
@echo off
chcp 65001 > nul
setlocal EnableDelayedExpansion
REM ═══════════════════════════════════════════════════════════════
REM build_windows.bat $APP_NAME v$APP_VERSION
REM JT-Tools by Johnny H3Campus
REM Compatible : Windows natif ET Wine (Linux)
REM ═══════════════════════════════════════════════════════════════
echo.
echo *** Build $APP_NAME v$APP_VERSION ***
echo.
REM ── Localiser le dossier projet ───────────────────────────────────────
set "SCRIPT_DIR=%~dp0"
for %%I in ("%CD%") do set "PROJECT_ROOT=%%~fI"
if exist "%PROJECT_ROOT%\$MAIN_SCRIPT" goto :ROOT_FOUND
for %%I in ("%SCRIPT_DIR%\..\..") do set "PROJECT_ROOT=%%~fI"
if exist "%PROJECT_ROOT%\$MAIN_SCRIPT" goto :ROOT_FOUND
for %%I in ("%SCRIPT_DIR%") do set "PROJECT_ROOT=%%~fI"
if exist "%PROJECT_ROOT%\$MAIN_SCRIPT" goto :ROOT_FOUND
echo [XX] $MAIN_SCRIPT introuvable.
echo [XX] Placez build_windows.bat dans dist\windows\ du projet,
echo [XX] ou lancez-le depuis la racine contenant $MAIN_SCRIPT.
pause & exit /b 1
:ROOT_FOUND
cd /d "%PROJECT_ROOT%"
echo [OK] Dossier projet : %PROJECT_ROOT%
echo.
REM ── Localiser Python (cherche dans les emplacements standard) ─────────
set PYTHON_EXE=
REM Essayer d'abord "python" dans le PATH
python --version >nul 2>&1
if not errorlevel 1 ( set PYTHON_EXE=python & goto :PYTHON_FOUND )
REM Chercher dans les emplacements fixes courants
for %%P in (
"C:\Python312\python.exe"
"C:\Python311\python.exe"
"C:\Python310\python.exe"
"C:\Python39\python.exe"
"%LOCALAPPDATA%\Programs\Python\Python312\python.exe"
"%LOCALAPPDATA%\Programs\Python\Python311\python.exe"
"%LOCALAPPDATA%\Programs\Python\Python310\python.exe"
"%APPDATA%\..\Local\Programs\Python\Python312\python.exe"
) do (
if exist %%P ( set PYTHON_EXE=%%P & goto :PYTHON_FOUND )
)
REM Utiliser le launcher py si disponible
py -3.12 --version >nul 2>&1 && set PYTHON_EXE=py -3.12 & goto :PYTHON_FOUND
py -3.11 --version >nul 2>&1 && set PYTHON_EXE=py -3.11 & goto :PYTHON_FOUND
py --version >nul 2>&1 && set PYTHON_EXE=py & goto :PYTHON_FOUND
echo [XX] Python introuvable. Installez Python 3.12 :
echo https://www.python.org/ftp/python/3.12.10/python-3.12.10-amd64.exe
pause & exit /b 1
:PYTHON_FOUND
for /f "tokens=2" %%v in ('%PYTHON_EXE% --version 2^>^&1') do set PY_FOUND=%%v
echo [OK] Python trouve : %PYTHON_EXE% (%PY_FOUND%)
echo.
REM ── Ajouter Scripts/ au PATH pour que pip/pyinstaller soit accessible ─
for /f "delims=" %%d in ('%PYTHON_EXE% -c "import sysconfig; print(sysconfig.get_path(chr(115)+chr(99)+chr(114)+chr(105)+chr(112)+chr(116)+chr(115)))"') do set PY_SCRIPTS=%%d
if defined PY_SCRIPTS ( set PATH=%PY_SCRIPTS%;%PATH% )
REM ── [1/4] Installation des dependances ───────────────────────────────
echo [1/4] Installation des dependances Python...
%PYTHON_EXE% -m pip install --upgrade pip setuptools wheel --quiet
%PYTHON_EXE% -m pip install pyinstaller PyQt6 PyQt6-Qt6 numpy soundfile hid Pillow ^
--prefer-binary --quiet ^
--trusted-host pypi.org ^
--trusted-host pypi.python.org ^
--trusted-host files.pythonhosted.org
if errorlevel 1 (
echo [XX] Echec pip install. Nouvelle tentative sans --quiet...
%PYTHON_EXE% -m pip install pyinstaller PyQt6 PyQt6-Qt6 numpy soundfile hid Pillow ^
--prefer-binary ^
--trusted-host pypi.org ^
--trusted-host pypi.python.org ^
--trusted-host files.pythonhosted.org
if errorlevel 1 ( echo [XX] ERREUR installation paquets & pause & exit /b 1 )
)
echo [OK] Dependances installees.
%PYTHON_EXE% -c "from PyQt6 import QtCore"
if errorlevel 1 (
echo [XX] PyQt6 est installe mais QtCore ne se charge pas.
echo [XX] Verifiez PyQt6 / PyQt6-Qt6 ou essayez une autre version.
pause & exit /b 1
)
echo.
REM ── [2/4] Generation des icones (via fichier temp) ───────────────────
echo [2/4] Generation des icones...
REM Ecrire le script Python dans un fichier temp (evite les problemes de guillemets cmd)
set ICON_PY=%TEMP%\build_icon_%RANDOM%.py
(
echo from PIL import Image, ImageDraw
echo def logo(d, sz^):
echo d.rounded_rectangle([0,0,sz-1,sz-1], radius=int(sz*.18^), fill=(59,110,220,255^)^)
echo bars=[.25,.45,.7,.9,.65,1.,.55,.8,.4,.2]
echo n,mg=len(bars^),sz*.13; bw=(sz-2*mg^)/(n*1.7^); gap=(sz-2*mg^)/n; mid=sz/2
echo for i,h in enumerate(bars^):
echo x=mg+i*gap+(gap-bw^)/2; bh=h*sz*.36
echo d.rounded_rectangle([x,mid-bh,x+bw,mid+bh],radius=bw/2,fill=(255,255,255,215^)^)
echo sizes=[16,32,48,64,128,256]; imgs=[]
echo for sz in sizes:
echo img=Image.new('RGBA',(sz,sz^),(0,0,0,0^)^); logo(ImageDraw.Draw(img^),sz^); imgs.append(img^)
echo imgs[-1].save('icon.png'^)
echo imgs[0].save('icon.ico',format='ICO',sizes=[(s,s^) for s in sizes],append_images=imgs[1:]^)
echo print('[OK] Icones generees'^)
) > "%ICON_PY%"
%PYTHON_EXE% "%ICON_PY%"
del "%ICON_PY%" 2>nul
if not exist icon.ico ( echo [!!] Icone ignoree (Pillow absent ?^) ) else ( echo [OK] icon.ico et icon.png. )
echo.
REM ── [3/4] Compilation PyInstaller ────────────────────────────────────
echo [3/4] Compilation PyInstaller...
%PYTHON_EXE% -m PyInstaller ^
--onedir --windowed --clean --noconfirm ^
--name $APP_NAME ^
--distpath "%PROJECT_ROOT%\dist\windows" ^
--workpath "%PROJECT_ROOT%\build\windows" ^
--specpath "%PROJECT_ROOT%\build\windows" ^
--hidden-import PyQt6.QtMultimedia ^
--hidden-import PyQt6.QtCore ^
--hidden-import PyQt6.QtGui ^
--hidden-import PyQt6.QtWidgets ^
--hidden-import soundfile ^
--hidden-import numpy ^
--hidden-import hid ^
--hidden-import cffi ^
--hidden-import _cffi_backend ^
--icon "%PROJECT_ROOT%\icon.ico" ^
"%PROJECT_ROOT%\$MAIN_SCRIPT"
if errorlevel 1 ( echo [XX] ERREUR PyInstaller & pause & exit /b 1 )
echo [OK] Binaire : %PROJECT_ROOT%\dist\windows\$APP_NAME\$APP_NAME.exe
echo.
REM ── [4/4] Raccourcis Bureau + Menu Demarrer ───────────────────────────
echo [4/4] Installation des raccourcis...
if exist "%PROJECT_ROOT%\dist\windows\install_shortcut.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass ^
-File "%PROJECT_ROOT%\dist\windows\install_shortcut.ps1" ^
-ExePath "%PROJECT_ROOT%\dist\windows\$APP_NAME\$APP_NAME.exe"
) else (
echo [!!] Script PowerShell introuvable : %PROJECT_ROOT%\dist\windows\install_shortcut.ps1
)
echo.
echo *** Build termine ! ***
echo Binaire : %PROJECT_ROOT%\dist\windows\$APP_NAME\$APP_NAME.exe
echo Raccourcis : Bureau + Menu Demarrer
echo.
pause
WINBAT
# Script PowerShell standalone pour install raccourci seulement
cat > "$DIST_WINDOWS/install_shortcut.ps1" << PSSCRIPT
# install_shortcut.ps1 - Install shortcuts for $APP_NAME
# Usage : powershell -ExecutionPolicy Bypass -File install_shortcut.ps1
param([string]\$ExePath = "")
if (-not \$ExePath) {
\$here = Split-Path \$MyInvocation.MyCommand.Path
\$ExePath = Join-Path \$here "$APP_NAME\$APP_NAME.exe"
if (-not (Test-Path \$ExePath)) {
\$ExePath = Join-Path \$here "$APP_NAME.exe"
}
}
if (-not (Test-Path \$ExePath)) {
Write-Error "Executable introuvable : \$ExePath"
exit 1
}
\$exe = Resolve-Path \$ExePath
\$desc = "$APP_COMMENT_WIN"
\$dirs = @(
[Environment]::GetFolderPath("Desktop"),
"\$env:APPDATA\Microsoft\Windows\Start Menu\Programs"
)
foreach (\$dir in \$dirs) {
\$lnk = Join-Path \$dir "$APP_NAME.lnk"
\$wsh = New-Object -ComObject WScript.Shell
\$sc = \$wsh.CreateShortcut(\$lnk)
\$sc.TargetPath = \$exe
\$sc.IconLocation = "\$exe,0"
\$sc.Description = \$desc
\$sc.WorkingDirectory = Split-Path \$exe
\$sc.Save()
Write-Host "[OK] Raccourci : \$lnk"
}
Write-Host ""
Write-Host "[OK] Installation terminee."
PSSCRIPT
ok "Script Windows : $DIST_WINDOWS/build_windows.bat"
ok "Script PowerShell : $DIST_WINDOWS/install_shortcut.ps1"
echo ""
warn "Pour Windows natif : copiez dist/windows/ et double-cliquez build_windows.bat"
echo ""
# ── Compilation via Wine (appels directs, pas via cmd.exe) ───────────────
local wine_cmd
wine_cmd=$(command -v wine64 || command -v wine || true)
[[ -z "$wine_cmd" ]] && return
step "Compilation Windows via Wine"
info "Wine : $wine_cmd"
# Chercher python.exe dans le préfixe Wine
local wine_prefix="${WINEPREFIX:-$HOME/.wine}"
local wine_python=""
for pyver in Python312 Python311 Python310 Python39; do
local candidate="$wine_prefix/drive_c/$pyver/python.exe"
if [[ -f "$candidate" ]]; then
wine_python="$candidate"
break
fi
done
# Chercher aussi dans Program Files
if [[ -z "$wine_python" ]]; then
for base in "$wine_prefix/drive_c/users/$USER/AppData/Local/Programs" "$wine_prefix/drive_c/Program Files" "$wine_prefix/drive_c/Program Files (x86)"; do
for pyver in Python312 Python311 Python310; do
local candidate="$base/Python/$pyver/python.exe"
[[ -f "$candidate" ]] && wine_python="$candidate" && break 2
done
done
fi
if [[ -z "$wine_python" ]]; then
warn "Python Windows introuvable dans Wine ($wine_prefix)."
warn "Installez-le avec :"
info " wget https://www.python.org/ftp/python/3.12.10/python-3.12.10-amd64.exe"
info " wine python-3.12.10-amd64.exe /quiet InstallAllUsers=0 TargetDir=C:\\Python312"
info "Puis relancez : ./build.sh --windows"
return
fi
ok "Python Wine : $wine_python"
# Chemin Windows pour Wine (Z: = racine Linux sur Wine standard)
local wine_py_win
wine_py_win=$(winepath -w "$wine_python" 2>/dev/null || echo "$wine_python")
# Générer les icônes côté Linux avant la compilation
generate_icons
# ── Avertissement Wine + numpy ───────────────────────────────────────────────
# ucrtbase.dll.crealf n'est pas implémenté dans Wine → numpy crashe PyInstaller.
# PyInstaller découvre les hooks via les entry_points de TOUS les paquets installés.
# Si numpy est présent dans le Python Wine, son hook crashe Wine même avec --exclude-module.
# Solution : désinstaller numpy/soundfile AVANT PyInstaller, réinstaller si nécessaire.
warn "Build Wine : numpy/soundfile seront temporairement désinstallés du Python Wine"
warn " (ucrtbase.dll.crealf non implémenté → PyInstaller crasherait)."
warn " La waveform sera absente du .exe Wine."
warn " Pour un .exe complet → build_windows.bat sur Windows natif."
echo ""
# WINEDEBUG via variable d'environnement pour Wine lui-même (pas export bash)
# Utiliser WINEDEBUG dans la commande wine directement
local wine_run=("env" "WINEDEBUG=-all,err+all" "$wine_cmd")
local pip_trust=(
--trusted-host pypi.org
--trusted-host pypi.python.org
--trusted-host files.pythonhosted.org
)
info "Mise à jour de pip dans Wine..."
"${wine_run[@]}" "$wine_python" -m pip install --upgrade pip setuptools wheel --quiet "${pip_trust[@]}" 2>/dev/null || true
# ── Désinstaller numpy/soundfile/scipy du Python Wine ────────────────────────
# (ils peuvent rester d'une installation précédente)
info "Désinstallation de numpy/soundfile/scipy du Python Wine (anti-crash ucrtbase)..."
"${wine_run[@]}" "$wine_python" -m pip uninstall -y numpy soundfile scipy PyInstaller-hooks-contrib 2>/dev/null || true
# ── Installer les dépendances sans numpy/soundfile ────────────────────────────
info "Installation PyInstaller + PyQt6 (sans numpy/soundfile)..."
"${wine_run[@]}" "$wine_python" -m pip install pyinstaller PyQt6 PyQt6-Qt6 hid Pillow --prefer-binary --quiet "${pip_trust[@]}"
if [[ $? -ne 0 ]]; then
err "Échec pip install Wine."
return
fi
ok "Dépendances installées (PyQt6 + PyInstaller, sans numpy)."
check_wine_pyqt_runtime "$wine_cmd" "$wine_python" || return 1
# ── Vérifier que numpy est bien absent ───────────────────────────────────────
if "${wine_run[@]}" "$wine_python" -c "import numpy" 2>/dev/null; then
err "numpy est encore présent dans le Python Wine malgré la désinstallation."
err "Désinstallez-le manuellement : wine $wine_python -m pip uninstall -y numpy"
return
fi
ok "numpy absent du Python Wine — PyInstaller peut s'exécuter."
info "Lancement de PyInstaller (Windows via Wine)..."
# Hidden imports : exclure ce qui crashe Wine
local hi_args_wine=()
for h in "${HIDDEN_IMPORTS[@]}"; do
case "$h" in
numpy|soundfile|scipy|cffi|_cffi_backend) ;;
*) hi_args_wine+=(--hidden-import "$h") ;;
esac
done
local icon_arg_wine=()
[[ -f "$ICON_ICO" ]] && icon_arg_wine=(--icon "$(winepath -w "$(pwd)/$ICON_ICO" 2>/dev/null || echo "$ICON_ICO")")
local src_win; src_win=$(winepath -w "$(pwd)/$MAIN_SCRIPT" 2>/dev/null || echo "$MAIN_SCRIPT")
local dist_win; dist_win=$(winepath -w "$(pwd)/$DIST_WINDOWS" 2>/dev/null || echo "$DIST_WINDOWS")
local build_win; build_win=$(winepath -w "$(pwd)/build/windows" 2>/dev/null || echo "build/windows")
"${wine_run[@]}" "$wine_python" -m PyInstaller --onefile --windowed --clean --noconfirm --name="$APP_NAME" --distpath="$dist_win" --workpath="$build_win" --specpath="$build_win" --log-level=WARN --exclude-module numpy --exclude-module soundfile --exclude-module scipy --exclude-module _multiarray_umath "${hi_args_wine[@]}" "${icon_arg_wine[@]}" "$src_win" 2>&1 | grep -v "^0[0-9a-f]*:fixme:\|^0[0-9a-f]*:err:\|WineDbg\|Register dump\|Stack dump\|Backtrace\|Modules:\|Threads:\|System info"
local rc=${PIPESTATUS[0]}
local binary="$DIST_WINDOWS/$APP_NAME.exe"
if [[ -f "$binary" && $rc -eq 0 ]]; then
local size; size=$(du -m "$binary" | cut -f1)
ok "Binaire Windows : $binary (${size} Mo)"
warn "Ce binaire N'INCLUT PAS la waveform (numpy exclu pour compatibilité Wine)."
warn "Pour un .exe complet avec waveform :"
warn " Copiez build_windows.bat sur une machine Windows et exécutez-le."
else
err "PyInstaller échoué (code $rc)."
err "Solution recommandée : utilisez build_windows.bat sur Windows natif."
fi
}
# ── Nettoyage ──────────────────────────────────────────────────────────────────
clean() {
step "Nettoyage"
local removed=false
for target in build dist "$ICON_PNG" "$ICON_ICO" *.spec; do
if [[ -e "$target" ]]; then
rm -rf "$target"
ok "Supprimé : $target"
removed=true
fi
done
$removed || info "Rien à nettoyer"
}
# ── Aide ───────────────────────────────────────────────────────────────────────
usage() {
sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
# ── Résumé final ───────────────────────────────────────────────────────────────
summary() {
echo ""
echo -e "${B}${G}╔══════════════════════════════════════════╗${N}"
echo -e "${B}${G}$APP_NAME v$APP_VERSION Build OK ║${N}"
echo -e "${B}${G}╚══════════════════════════════════════════╝${N}"
echo ""
[[ -f "$DIST_LINUX/$APP_NAME" ]] && \
echo -e " Linux : ${C}$DIST_LINUX/$APP_NAME${N}"
[[ -f "$DIST_WINDOWS/build_windows.bat" ]] && \
echo -e " Windows: ${C}$DIST_WINDOWS/build_windows.bat${N}"
echo ""
}
# ── Point d'entrée ─────────────────────────────────────────────────────────────
main() {
# Aller dans le répertoire du script
cd "$(dirname "$(realpath "$0")")"
# Vérifications de base
[[ $EUID -eq 0 ]] && die "Ne pas exécuter en root."
[[ -f "$MAIN_SCRIPT" ]] || die "$MAIN_SCRIPT introuvable dans $(pwd)"
local do_linux=false
local do_windows=false
local do_install=true
local do_install_only=false
local do_uninstall=false
local do_clean=false
# Défaut : si aucun argument, build linux
[[ $# -eq 0 ]] && do_linux=true && do_install=true
while [[ $# -gt 0 ]]; do
case "$1" in
--linux) do_linux=true ;;
--windows) do_windows=true ;;
--both) do_linux=true; do_windows=true ;;
--no-install) do_install=false ;;
--install-only) do_install_only=true ;;
--uninstall) do_uninstall=true ;;
--clean) do_clean=true ;;
--help|-h) usage ;;
*) die "Option inconnue : $1 (--help pour l'aide)" ;;
esac
shift
done
# ── Actions exclusives ──────────────────────────────────────────────────
if $do_clean; then clean; exit 0; fi
if $do_uninstall; then uninstall_gnome_shortcut; exit 0; fi
if $do_install_only; then
generate_icons
install_gnome_shortcut
exit 0
fi
# ── Affichage de l'en-tête ─────────────────────────────────────────────
echo -e "\n${B}╔══════════════════════════════════════════════╗${N}"
echo -e "${B}$APP_NAME Build v$APP_VERSION${N}"
echo -e "${B}║ JT-Tools by Johnny · H3Campus ║${N}"
echo -e "${B}╚══════════════════════════════════════════════╝${N}"
# ── Build Linux ─────────────────────────────────────────────────────────
if $do_linux; then
build_linux
$do_install && install_gnome_shortcut
fi
# ── Build Windows ───────────────────────────────────────────────────────
if $do_windows; then
build_windows
fi
summary
}
main "$@"