784 lines
32 KiB
Bash
784 lines
32 KiB
Bash
#!/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 "$@"
|