TranscriptStation/build.py

852 lines
36 KiB
Python
Raw 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 python3
"""
build.py Compilation + installation de TranscribeStation
Produit un binaire onefile PyInstaller et installe les raccourcis système.
Usage :
python build.py # build + install (plateforme courante)
python build.py --linux # build Linux
python build.py --windows # build Windows (Wine ou .bat natif)
python build.py --both # Linux + Windows
python build.py --install-only # installe sans recompiler
python build.py --clean # nettoyer build/ dist/ *.spec icon.*
Raccourcis créés :
Linux → ~/.local/share/applications/TranscribeStation.desktop
~/.local/share/icons/hicolor/256x256/apps/TranscribeStation.png
~/Bureau/TranscribeStation.desktop (si dossier existe)
Windows → %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\TranscribeStation.lnk
%USERPROFILE%\\Desktop\\TranscribeStation.lnk
"""
import sys
import os
import shutil
import platform
import subprocess
import argparse
import textwrap
import urllib.request
import zipfile
import tempfile
from pathlib import Path
# ─── Configuration ────────────────────────────────────────────────────────────
APP_NAME = "TranscribeStation"
APP_VERSION = "1.2.0"
APP_AUTHOR = "H3Campus"
APP_URL = "https://github.com/h3campus/transcribe-station"
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;"
MAIN_SCRIPT = "transcribe_station.py"
ICON_ICO = Path("icon.ico")
ICON_PNG = Path("icon.png")
BUILD_DIR = Path("build")
DIST_DIR = Path("dist")
HIDDEN_IMPORTS = [
"PySide6.QtMultimedia",
"PySide6.QtCore",
"PySide6.QtGui",
"PySide6.QtWidgets",
"soundfile",
"numpy",
"hid",
"cffi",
"_cffi_backend",
]
# --collect-all hid : inclut la DLL native hidapi.dll dans le bundle Windows
COLLECT_ALL = ["hid"]
ADD_DATA: list[str] = []
# hidapi.dll natif Windows (absent du package pip hid 1.x qui est un pur wrapper ctypes)
HIDAPI_VERSION = "0.14.0"
HIDAPI_DLL_URL = (
f"https://github.com/libusb/hidapi/releases/download/"
f"hidapi-{HIDAPI_VERSION}/hidapi-win.zip"
)
# ─── Couleurs terminal ─────────────────────────────────────────────────────────
R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'
C='\033[0;36m'; B='\033[1m'; N='\033[0m'
def ok(m): print(f"{G}[OK]{N} {m}")
def info(m):print(f"{C}[..]{N} {m}")
def warn(m):print(f"{Y}[!!]{N} {m}")
def err(m): print(f"{R}[XX]{N} {m}", file=sys.stderr)
def step(m):print(f"\n{B}{m}{N}")
# ─── hidapi.dll ───────────────────────────────────────────────────────────────
def _fetch_hidapi_dll() -> Path | None:
"""Télécharge hidapi.dll (x64) depuis les releases GitHub, mise en cache dans build/."""
dll_cache = BUILD_DIR / "hidapi.dll"
if dll_cache.exists():
ok(f"hidapi.dll en cache : {dll_cache}")
return dll_cache
info(f"Téléchargement hidapi.dll v{HIDAPI_VERSION} depuis GitHub...")
BUILD_DIR.mkdir(parents=True, exist_ok=True)
try:
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
tmp_path = tmp.name
urllib.request.urlretrieve(HIDAPI_DLL_URL, tmp_path)
with zipfile.ZipFile(tmp_path) as zf:
candidates = [n for n in zf.namelist()
if n.lower().endswith("hidapi.dll") and "x64" in n.lower()]
if not candidates:
candidates = [n for n in zf.namelist()
if n.lower().endswith("hidapi.dll")]
if candidates:
with zf.open(candidates[0]) as src, open(dll_cache, "wb") as dst:
dst.write(src.read())
ok(f"hidapi.dll extrait ({candidates[0]}) → {dll_cache}")
return dll_cache
warn("hidapi.dll introuvable dans l'archive")
except Exception as exc:
warn(f"Échec téléchargement hidapi.dll : {exc}")
finally:
try:
os.unlink(tmp_path)
except Exception:
pass
return None
def _install_hidapi_dll_onedir(internal_dir: Path) -> None:
"""Copie hidapi.dll dans _internal/ d'un bundle onedir."""
if not internal_dir.exists():
warn(f"Dossier _internal introuvable : {internal_dir}")
return
dll = _fetch_hidapi_dll()
if dll:
dest = internal_dir / "hidapi.dll"
shutil.copy2(dll, dest)
ok(f"hidapi.dll installé → {dest}")
else:
warn("hidapi.dll non disponible — le pédalier ne fonctionnera pas sans cette DLL.")
# ─── Helpers ──────────────────────────────────────────────────────────────────
def run(cmd: list, check=True) -> int:
info(" ".join(str(c) for c in cmd))
r = subprocess.run(cmd, check=False)
if check and r.returncode != 0:
err(f"Échec (code {r.returncode})")
sys.exit(r.returncode)
return r.returncode
def pip_install(pkgs: list, python=sys.executable):
run([python, "-m", "pip", "install", "--quiet"] + pkgs)
def validate_wine_pyside(wine: str, wine_python: str) -> bool:
probe = subprocess.run(
[
wine, wine_python, "-c",
(
"from pathlib import Path; "
"import importlib.metadata as md; "
"import PySide6; "
"qt_dir = Path(PySide6.__file__).resolve().parent; "
"missing = [name for name in ('Qt6Core.dll', 'Qt6Multimedia.dll') if not (qt_dir / name).exists()]; "
"print('PYSIDE6_VERSION=' + md.version('PySide6')); "
"print('QT_DIR=' + str(qt_dir).replace(chr(92), '/')); "
"print('MISSING_DLLS=' + ','.join(missing)); "
"from PySide6 import QtCore; "
"print('QTCORE_OK=1')"
),
],
check=False,
capture_output=True,
text=True,
)
output = (probe.stdout or "") + (probe.stderr or "")
for line in output.splitlines():
if line.strip():
info(f"Wine Qt probe : {line}")
if probe.returncode != 0:
missing = ""
for line in output.splitlines():
if line.startswith("MISSING_DLLS="):
missing = line.split("=", 1)[1].strip()
break
err("PySide6 est installé dans Wine, mais QtCore ne se charge pas.")
if missing:
err(f"DLL Qt manquantes dans le wheel Windows : {missing}")
err("Le binaire Windows généré via Wine serait invalide.")
err("Utilisez le script build_windows.bat sur Windows natif.")
return False
return True
# ─── Génération d'icônes ──────────────────────────────────────────────────────
def _draw_icon(draw, sz):
"""Dessine le logo waveform TranscribeStation."""
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),
)
def generate_icons() -> bool:
"""Génère icon.png (256px) et icon.ico (multi-tailles). Retourne True si OK."""
try:
from PIL import Image, ImageDraw
except ImportError:
warn("Pillow absent → icônes ignorées (pip install Pillow)")
return False
sizes_ico = [16, 32, 48, 64, 128, 256]
images = []
for sz in sizes_ico:
img = Image.new("RGBA", (sz, sz), (0, 0, 0, 0))
_draw_icon(ImageDraw.Draw(img), sz)
images.append(img)
# PNG 256 px pour Linux XDG
images[-1].save(str(ICON_PNG))
ok(f"Icône PNG : {ICON_PNG}")
# ICO multi-tailles pour Windows
images[0].save(
str(ICON_ICO), format="ICO",
sizes=[(s, s) for s in sizes_ico],
append_images=images[1:],
)
ok(f"Icône ICO : {ICON_ICO}")
return True
# ─── Raccourci GNOME / Linux ──────────────────────────────────────────────────
def install_linux_shortcut(binary: Path):
step("📌 Installation du raccourci GNOME...")
binary = binary.resolve()
# 1. Icône → XDG hicolor
xdg_icon_dir = Path.home() / ".local/share/icons/hicolor/256x256/apps"
xdg_icon_dir.mkdir(parents=True, exist_ok=True)
xdg_icon = xdg_icon_dir / f"{APP_NAME}.png"
if ICON_PNG.exists():
shutil.copy(ICON_PNG, xdg_icon)
ok(f"Icône installée : {xdg_icon}")
else:
warn("icon.png absent — raccourci sans icône")
# 2. Fichier .desktop → applications
apps_dir = Path.home() / ".local/share/applications"
apps_dir.mkdir(parents=True, exist_ok=True)
desktop_path = apps_dir / f"{APP_NAME}.desktop"
desktop_content = textwrap.dedent(f"""\
[Desktop Entry]
Version=1.0
Type=Application
Name={APP_NAME}
GenericName=Transcription Audio
Comment={APP_COMMENT}
Exec={binary}
Icon={APP_NAME}
Terminal=false
Categories={APP_CATS}
StartupNotify=true
StartupWMClass={APP_NAME}
Keywords=transcription;audio;dictée;pédalier;
""")
desktop_path.write_text(desktop_content, encoding="utf-8")
desktop_path.chmod(0o755)
ok(f".desktop installé : {desktop_path}")
# 3. Raccourci Bureau (Desktop)
for bureau in ["Bureau", "Desktop", "Escritorio"]:
bureau_dir = Path.home() / bureau
if bureau_dir.is_dir():
bureau_link = bureau_dir / f"{APP_NAME}.desktop"
shutil.copy(desktop_path, bureau_link)
bureau_link.chmod(0o755)
ok(f"Raccourci bureau : {bureau_link}")
break
# 4. Mettre à jour la base des .desktop
for cmd in [
["update-desktop-database", str(apps_dir)],
["gtk-update-icon-cache", "-f", "-t",
str(Path.home() / ".local/share/icons/hicolor")],
["xdg-desktop-menu", "forceupdate"],
]:
if shutil.which(cmd[0]):
run(cmd, check=False)
ok("Raccourci GNOME installé. Visible au prochain rechargement de session.")
def uninstall_linux_shortcut():
step("🗑 Désinstallation raccourci GNOME...")
targets = [
Path.home() / f".local/share/applications/{APP_NAME}.desktop",
Path.home() / f".local/share/icons/hicolor/256x256/apps/{APP_NAME}.png",
]
for bureau in ["Bureau", "Desktop", "Escritorio"]:
targets.append(Path.home() / bureau / f"{APP_NAME}.desktop")
for t in targets:
if t.exists():
t.unlink()
ok(f"Supprimé : {t}")
# ─── Raccourci Windows (depuis Windows natif) ─────────────────────────────────
def install_windows_shortcut(binary: Path):
"""Crée Menu Démarrer + Bureau via PowerShell (à appeler depuis Windows)."""
step("📌 Installation du raccourci Windows...")
binary = binary.resolve()
# param() en premiere ligne : ExePath est passe par le bat via -ExePath "..."
# Ne PAS hardcoder $ExePath dans le corps du script (serait ecrase par la ligne param)
ps_script = textwrap.dedent(f"""
param([string]$ExePath)
$AppName = "{APP_NAME}"
$Comment = "{APP_COMMENT_WIN}"
function New-Shortcut([string]$dest) {{
$wsh = New-Object -ComObject WScript.Shell
$lnk = $wsh.CreateShortcut($dest)
$lnk.TargetPath = $ExePath
$lnk.IconLocation = "$ExePath,0"
$lnk.Description = $Comment
$lnk.WorkingDirectory = [System.IO.Path]::GetDirectoryName($ExePath)
$lnk.Save()
Write-Host "[OK] Raccourci : $dest"
}}
$startMenu = "$env:APPDATA\\Microsoft\\Windows\\Start Menu\\Programs"
New-Shortcut "$startMenu\\$AppName.lnk"
$desktop = [Environment]::GetFolderPath("Desktop")
New-Shortcut "$desktop\\$AppName.lnk"
Write-Host ""
Write-Host "[OK] Shortcuts installed successfully."
""").strip()
ps_file = DIST_DIR / "windows" / "install_shortcut.ps1"
ps_file.parent.mkdir(parents=True, exist_ok=True)
ps_file.write_text(ps_script, encoding="utf-8-sig")
ok(f"Script PowerShell généré : {ps_file}")
if platform.system() == "Windows":
info("Exécution de PowerShell...")
run(["powershell", "-ExecutionPolicy", "Bypass",
"-File", str(ps_file)], check=False)
else:
warn("Cross-compilation : exécutez ce script sur la machine Windows cible :")
warn(f" powershell -ExecutionPolicy Bypass -File {ps_file}")
# ─── Build Linux ──────────────────────────────────────────────────────────────
def build_linux(install: bool = True):
step("🐧 Build Linux")
pip_install(["pyinstaller", "Pillow"])
generate_icons()
run([sys.executable, "-m", "PyInstaller",
"--onefile", "--clean", "--noconfirm",
f"--name={APP_NAME}",
"--distpath=dist/linux",
"--workpath=build/linux",
"--specpath=build/linux",
"--log-level=WARN",
*(f"--hidden-import={h}" for h in HIDDEN_IMPORTS),
*(f"--collect-all={m}" for m in COLLECT_ALL),
*(f"--add-data={d}" for d in ADD_DATA),
f"--icon={ICON_ICO}" if ICON_ICO.exists() else "",
MAIN_SCRIPT,
])
binary = Path(f"dist/linux/{APP_NAME}")
if binary.exists():
binary.chmod(0o755)
size = binary.stat().st_size // (1024 * 1024)
ok(f"Binaire Linux : {binary} ({size} Mo)")
if install:
install_linux_shortcut(binary)
else:
err("Binaire introuvable dans dist/linux/")
# ─── Build Windows ────────────────────────────────────────────────────────────
def build_windows(install: bool = True):
step("🪟 Build Windows")
# ── Sur machine Windows native ────────────────────────────────────────────
if platform.system() == "Windows":
pip_install(["pyinstaller", "Pillow"])
generate_icons()
icon_arg = f"--icon={ICON_ICO.resolve()}" if ICON_ICO.exists() else ""
main_script = str(Path(MAIN_SCRIPT).resolve())
run([sys.executable, "-m", "PyInstaller",
"--onedir", "--clean", "--noconfirm", "--windowed",
f"--name={APP_NAME}",
"--distpath=dist/windows",
"--workpath=build/windows",
"--specpath=build/windows",
"--log-level=WARN",
*(f"--hidden-import={h}" for h in HIDDEN_IMPORTS),
*(f"--collect-all={m}" for m in COLLECT_ALL),
icon_arg,
main_script,
])
binary = Path(f"dist/windows/{APP_NAME}/{APP_NAME}.exe")
if binary.exists():
ok(f"Binaire Windows : {binary}")
_install_hidapi_dll_onedir(Path(f"dist/windows/{APP_NAME}/_internal"))
if install:
install_windows_shortcut(binary)
else:
err("Binaire introuvable.")
return
# ── Cross-compilation depuis Linux via Wine ────────────────────────────────
wine = shutil.which("wine") or shutil.which("wine64")
wine_python = None
if wine:
for candidate in [
Path.home() / ".wine/drive_c/Python312/python.exe",
Path.home() / ".wine/drive_c/Python311/python.exe",
Path.home() / ".wine/drive_c/Python310/python.exe",
]:
if candidate.exists():
wine_python = candidate
break
if wine and wine_python:
info(f"Wine Python : {wine_python}")
pip_install(["pyinstaller", "PySide6", "numpy", "soundfile", "hid", "Pillow"],
python=str(wine_python))
generate_icons()
if not validate_wine_pyside(wine, str(wine_python)):
sys.exit(1)
# Récupérer hidapi.dll pour l'embarquer dans le onefile via --add-binary
hidapi_dll = _fetch_hidapi_dll()
add_binary_args = [f"--add-binary={hidapi_dll}:."] if hidapi_dll else []
if not hidapi_dll:
warn("hidapi.dll non disponible — le pédalier ne fonctionnera pas dans le .exe Wine")
run([wine, str(wine_python), "-m", "PyInstaller",
"--onefile", "--clean", "--noconfirm", "--windowed",
f"--name={APP_NAME}",
"--distpath=dist/windows",
"--workpath=build/windows",
"--specpath=build/windows",
"--log-level=WARN",
*(f"--hidden-import={h}" for h in HIDDEN_IMPORTS),
*(f"--collect-all={m}" for m in COLLECT_ALL),
*add_binary_args,
f"--icon={ICON_ICO}" if ICON_ICO.exists() else "",
MAIN_SCRIPT,
])
binary = Path(f"dist/windows/{APP_NAME}.exe")
if binary.exists():
size = binary.stat().st_size // (1024 * 1024)
ok(f"Binaire Windows : {binary} ({size} Mo)")
install_windows_shortcut(Path(f"dist/windows/{APP_NAME}.exe"))
else:
warn("Wine ou Python Windows introuvable → génération du script natif Windows")
_write_windows_bat(install=install)
def _write_windows_bat(install: bool = True):
"""Genere build_windows.bat + _build_icons.py dans dist/ pour execution native sur Windows."""
out_dir = Path("dist") / "windows"
out_dir.mkdir(parents=True, exist_ok=True)
bat_path = out_dir / "build_windows.bat"
icons_path = out_dir / "_build_icons.py"
# ── Script Python icones dans un fichier separe (evite l'interpretation batch) ──
icon_py = (
"from PIL import Image, ImageDraw\n"
"sizes = [16, 32, 48, 64, 128, 256]\n"
"imgs = []\n"
"for sz in sizes:\n"
" img = Image.new('RGBA', (sz, sz), (0, 0, 0, 0))\n"
" d = ImageDraw.Draw(img)\n"
" d.rounded_rectangle([0, 0, sz-1, sz-1], radius=int(sz*.18), fill=(59, 110, 220, 255))\n"
" bars = [.25, .45, .7, .9, .65, 1., .55, .8, .4, .2]\n"
" n, mg = len(bars), sz*.13\n"
" bw = (sz - 2*mg) / (n * 1.7)\n"
" gap = (sz - 2*mg) / n\n"
" mid = sz / 2\n"
" for i, h in enumerate(bars):\n"
" x = mg + i*gap + (gap - bw) / 2\n"
" bh = h * sz * .36\n"
" d.rounded_rectangle([x, mid-bh, x+bw, mid+bh], radius=bw/2, fill=(255, 255, 255, 215))\n"
" imgs.append(img)\n"
"imgs[-1].save('icon.png')\n"
"imgs[0].save('icon.ico', format='ICO', sizes=[(s,s) for s in sizes], append_images=imgs[1:])\n"
"print('[OK] Icones generees')\n"
)
icons_path.write_text(icon_py, encoding="utf-8")
ok(f"Script icones genere : {icons_path}")
# ── Arguments hidden-import + collect-all PyInstaller ──
hi_lines = " ^\n ".join(f"--hidden-import {h}" for h in HIDDEN_IMPORTS)
ca_lines = " ^\n ".join(f"--collect-all {m}" for m in COLLECT_ALL)
shortcut_block = ""
if install:
shortcut_block = (
"echo [5/5] Installation des raccourcis...\n"
"REM Raccourci vers binaire LOCAL (WScript.Shell refuse les chemins reseau/partages)\n"
f'set "LOCAL_EXE=%LOCAL_DIST%\\{APP_NAME}\\{APP_NAME}.exe"\n'
f'if exist "%PROJECT_ROOT%dist\\windows\\install_shortcut.ps1" (\n'
f' powershell -NoProfile -ExecutionPolicy Bypass ^\n'
f' -File "%PROJECT_ROOT%dist\\windows\\install_shortcut.ps1" ^\n'
f' -ExePath "%LOCAL_EXE%"\n'
f') else (\n'
f' echo [!!] Script PowerShell introuvable\n'
f')\n'
)
# Venv court pour contourner MAX_PATH avec PySide6 (Microsoft Store Python)
# C:\ts\.venv\Lib\site-packages\ = 30 chars vs ~140 chars pour AppData\Packages\...
VENV = "C:\\ts\\.venv"
# ASCII uniquement dans le bat
bat = (
"@echo off\n"
"setlocal EnableDelayedExpansion\n"
f"REM build_windows.bat -- {APP_NAME} v{APP_VERSION}\n"
"REM A executer sur une machine Windows avec Python 3.12+\n"
"echo.\n"
f"echo *** Build {APP_NAME} v{APP_VERSION} pour Windows ***\n"
"echo.\n"
"\n"
"REM -- Localisation du projet\n"
"set \"SCRIPT_DIR=%~dp0\"\n"
"for %%I in (\"%SCRIPT_DIR%\\..\") do set \"PROJECT_ROOT=%%~fI\\\"\n"
f"if exist \"%PROJECT_ROOT%{MAIN_SCRIPT}\" goto ROOT_FOUND\n"
"for %%I in (\"%SCRIPT_DIR%\\..\\..\") do set \"PROJECT_ROOT=%%~fI\\\"\n"
f"if exist \"%PROJECT_ROOT%{MAIN_SCRIPT}\" goto ROOT_FOUND\n"
f"echo [XX] {MAIN_SCRIPT} introuvable.\n"
"pause & exit /b 1\n"
"\n"
":ROOT_FOUND\n"
"echo [OK] Dossier projet : %PROJECT_ROOT%\n"
"echo.\n"
"\n"
"REM -- Localisation d'un vrai Python (evite le stub Windows Store)\n"
"set \"REAL_PYTHON=\"\n"
"\n"
"REM Essai 1 : py launcher (installe avec Python officiel)\n"
"where py >nul 2>&1\n"
"if not errorlevel 1 (\n"
" py -3 -c \"import sys; print(sys.executable)\" >nul 2>&1\n"
" if not errorlevel 1 (\n"
" for /f \"delims=\" %%P in ('py -3 -c \"import sys; print(sys.executable)\"') do set \"REAL_PYTHON=%%P\"\n"
" )\n"
")\n"
"\n"
"REM Essai 2 : emplacements standard Python 3.x\n"
"if not defined REAL_PYTHON (\n"
" for %%V in (313 312 311 310 39) do (\n"
" if not defined REAL_PYTHON (\n"
" if exist \"C:\\Python%%V\\python.exe\" set \"REAL_PYTHON=C:\\Python%%V\\python.exe\"\n"
" )\n"
" )\n"
")\n"
"REM Essai 3 : AppData\\Local\\Programs\\Python\n"
"if not defined REAL_PYTHON (\n"
" for %%V in (313 312 311 310 39) do (\n"
" if not defined REAL_PYTHON (\n"
" set \"_CANDIDATE=%LOCALAPPDATA%\\Programs\\Python\\Python%%V\\python.exe\"\n"
" if exist \"!_CANDIDATE!\" set \"REAL_PYTHON=!_CANDIDATE!\"\n"
" )\n"
" )\n"
")\n"
"\n"
"if not defined REAL_PYTHON (\n"
" echo [XX] Python 3 introuvable. Installez Python depuis https://www.python.org/downloads/\n"
" echo [XX] Assurez-vous de cocher \"Add python.exe to PATH\" lors de l'installation.\n"
" pause & exit /b 1\n"
")\n"
"echo [OK] Python trouve : %REAL_PYTHON%\n"
"\n"
"REM -- Venv a chemin court pour eviter MAX_PATH avec PySide6\n"
f"set \"VENV={VENV}\"\n"
"set \"VPYTHON=%VENV%\\Scripts\\python.exe\"\n"
"set \"VPIP=%VENV%\\Scripts\\pip.exe\"\n"
"echo [1/5] Preparation venv dans %VENV%...\n"
"if exist \"%VENV%\" (\n"
" \"%VPYTHON%\" -c \"import sys\" >nul 2>&1\n"
" if errorlevel 1 (\n"
" echo [!!] Venv existant invalide - recreation...\n"
" rmdir /S /Q \"%VENV%\"\n"
" )\n"
")\n"
"if not exist \"%VPYTHON%\" (\n"
" mkdir C:\\ts 2>nul\n"
" \"%REAL_PYTHON%\" -m venv \"%VENV%\"\n"
" if errorlevel 1 ( echo ERREUR venv & pause & exit /b 1 )\n"
")\n"
"echo [OK] Venv pret.\n"
"\n"
"echo [2/5] Installation des dependances Python...\n"
"\"%VPIP%\" install --quiet --upgrade pip\n"
"\"%VPIP%\" install --quiet pyinstaller PySide6 numpy soundfile hid Pillow\n"
"if errorlevel 1 ( echo ERREUR pip & pause & exit /b 1 )\n"
"\"%VPYTHON%\" -c \"from PySide6.QtMultimedia import QMediaPlayer; print('[OK] PySide6 + QtMultimedia OK')\"\n"
"if errorlevel 1 ( echo ERREUR PySide6.QtMultimedia & pause & exit /b 1 )\n"
"\n"
"echo [3/5] Generation des icones...\n"
"copy /Y \"%SCRIPT_DIR%_build_icons.py\" \"%PROJECT_ROOT%_build_icons.py\" > nul\n"
"\"%VPYTHON%\" \"%PROJECT_ROOT%_build_icons.py\"\n"
"if errorlevel 1 ( echo ERREUR icones & pause & exit /b 1 )\n"
"del \"%PROJECT_ROOT%_build_icons.py\" > nul 2>&1\n"
"\n"
"REM -- Build dans C:\\ts (chemin local court) pour eviter les erreurs reseau/MAX_PATH\n"
"set \"LOCAL_DIST=C:\\ts\\dist\"\n"
"set \"LOCAL_BUILD=C:\\ts\\build\"\n"
"\n"
"echo [4/5] Compilation PyInstaller (sortie locale C:\\ts\\dist)...\n"
"\"%VENV%\\Scripts\\pyinstaller.exe\" --onedir --windowed --clean --noconfirm ^\n"
f" --name {APP_NAME} ^\n"
" --distpath \"%LOCAL_DIST%\" ^\n"
" --workpath \"%LOCAL_BUILD%\" ^\n"
" --specpath \"%LOCAL_BUILD%\" ^\n"
" --icon \"%PROJECT_ROOT%icon.ico\" ^\n"
f" {hi_lines} ^\n"
f" {ca_lines} ^\n"
f" \"%PROJECT_ROOT%{MAIN_SCRIPT}\"\n"
"if errorlevel 1 ( echo ERREUR PyInstaller & pause & exit /b 1 )\n"
"\n"
"REM -- Ajout de hidapi.dll (absent du package pip hid)\n"
f"set \"HIDAPI_URL=https://github.com/libusb/hidapi/releases/download/hidapi-{HIDAPI_VERSION}/hidapi-win.zip\"\n"
f"set \"HIDAPI_ZIP=C:\\ts\\hidapi-win.zip\"\n"
f"set \"HIDAPI_DLL=%LOCAL_DIST%\\{APP_NAME}\\_internal\\hidapi.dll\"\n"
"if not exist \"%HIDAPI_DLL%\" (\n"
" echo Telechargement hidapi.dll...\n"
" powershell -NoProfile -Command \"Invoke-WebRequest -Uri '%HIDAPI_URL%' -OutFile '%HIDAPI_ZIP%'\"\n"
" if not errorlevel 1 (\n"
" powershell -NoProfile -Command \""
f"Expand-Archive -Force '%HIDAPI_ZIP%' 'C:\\\\ts\\\\hidapi_tmp'; "
f"$dll = Get-ChildItem 'C:\\\\ts\\\\hidapi_tmp' -Recurse -Filter hidapi.dll | "
f"Where-Object {{ $_.FullName -match 'x64' }} | Select-Object -First 1; "
f"if ($dll) {{ Copy-Item $dll.FullName '%HIDAPI_DLL%'; Write-Host '[OK] hidapi.dll installe' }} "
f"else {{ Write-Host '[!!] hidapi.dll x64 introuvable dans archive' }}; "
f"Remove-Item 'C:\\\\ts\\\\hidapi_tmp' -Recurse -Force\"\n"
" ) else (\n"
" echo [!!] Echec telechargement hidapi.dll - le pedalier ne fonctionnera pas.\n"
" )\n"
") else (\n"
" echo [OK] hidapi.dll deja present.\n"
")\n"
"\n"
"echo Copie du binaire vers le dossier projet...\n"
f"if exist \"%PROJECT_ROOT%dist\\windows\\{APP_NAME}\" rmdir /S /Q \"%PROJECT_ROOT%dist\\windows\\{APP_NAME}\"\n"
f"xcopy /E /I /Y \"%LOCAL_DIST%\\{APP_NAME}\" \"%PROJECT_ROOT%dist\\windows\\{APP_NAME}\\\"\n"
"if errorlevel 1 ( echo ERREUR copie xcopy & pause & exit /b 1 )\n"
f"echo [OK] Binaire copie dans %PROJECT_ROOT%dist\\windows\\{APP_NAME}\\\n"
"\n"
+ shortcut_block +
"\n"
"copy /Y \"%PROJECT_ROOT%icon.ico\" \"C:\\ts\\icon.ico\" > nul\n"
"\n"
"echo [6/6] Creation de l'installeur (Inno Setup)...\n"
"set \"ISCC_EXE=\"\n"
"if exist \"C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe\" "
"set \"ISCC_EXE=C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe\"\n"
"if exist \"C:\\Program Files\\Inno Setup 6\\ISCC.exe\" "
"set \"ISCC_EXE=C:\\Program Files\\Inno Setup 6\\ISCC.exe\"\n"
"if not defined ISCC_EXE (\n"
" echo [!!] Inno Setup 6 non installe - installeur ignore.\n"
" echo [!!] Telecharger gratuitement : https://jrsoftware.org/isdl.php\n"
" echo [!!] Puis relancez ce script pour generer l'installeur.\n"
" goto BUILD_DONE\n"
")\n"
f"mkdir C:\\ts\\installer 2>nul\n"
f"echo Compilation Inno Setup...\n"
f"\"%ISCC_EXE%\" \"%SCRIPT_DIR%{APP_NAME}.iss\"\n"
"if errorlevel 1 ( echo [!!] Erreur Inno Setup & goto BUILD_DONE )\n"
f"echo [OK] Installeur : C:\\ts\\installer\\{APP_NAME}_Setup_v{APP_VERSION}.exe\n"
"\n"
":BUILD_DONE\n"
"echo.\n"
f"echo *** Build termine ! ***\n"
f"echo Binaire : %PROJECT_ROOT%dist\\windows\\{APP_NAME}\\{APP_NAME}.exe\n"
f"echo Installeur : C:\\ts\\installer\\{APP_NAME}_Setup_v{APP_VERSION}.exe (si Inno Setup installe)\n"
"echo.\n"
"pause\n"
)
bat_crlf = bat.replace("\n", "\r\n")
bat_path.write_bytes(bat_crlf.encode("ascii", errors="replace"))
ok(f"Script Windows genere : {bat_path}")
ok(f"Script icones genere : {icons_path}")
generate_inno_script()
warn(f"Sur Windows : lancez dist\\windows\\build_windows.bat depuis la racine du projet")
warn(f"Inno Setup requis pour l'installeur : https://jrsoftware.org/isdl.php")
# ─── Installeur Inno Setup ────────────────────────────────────────────────────
def generate_inno_script():
"""
Génère TranscribeStation.iss pour Inno Setup 6.
Sortie : dist/windows/TranscribeStation.iss (CRLF + BOM UTF-8)
Compilation : ISCC.exe TranscribeStation.iss
Résultat : C:\\ts\\installer\\TranscribeStation_Setup_v{APP_VERSION}.exe
"""
# On utilise une string ordinaire pour éviter les conflits entre
# les accolades Inno Setup {app} et le f-string Python.
# N = APP_NAME, V = APP_VERSION, A = APP_AUTHOR, U = APP_URL
N, V, A, U = APP_NAME, APP_VERSION, APP_AUTHOR, APP_URL
iss = (
"[Setup]\n"
f"AppName={N}\n"
f"AppVersion={V}\n"
f"AppVerName={N} {V}\n"
f"AppPublisher={A}\n"
f"AppPublisherURL={U}\n"
f"AppSupportURL={U}\n"
f"DefaultDirName={{localappdata}}\\Programs\\{N}\n"
f"DefaultGroupName={N}\n"
"AllowNoIcons=yes\n"
"OutputDir=C:\\ts\\installer\n"
f"OutputBaseFilename={N}_Setup_v{V}\n"
"SetupIconFile=C:\\ts\\icon.ico\n"
"Compression=lzma2/ultra64\n"
"SolidCompression=yes\n"
"WizardStyle=modern\n"
"WizardSizePercent=120\n"
"PrivilegesRequired=lowest\n"
"ArchitecturesAllowed=x64compatible\n"
"ArchitecturesInstallIn64BitMode=x64compatible\n"
f"UninstallDisplayIcon={{app}}\\{N}.exe\n"
f"UninstallDisplayName={N} {V}\n"
"CloseApplications=yes\n"
"\n"
"[Languages]\n"
'Name: "french"; MessagesFile: "compiler:Languages\\French.isl"\n'
"\n"
"[Tasks]\n"
'Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; '
'GroupDescription: "{cm:AdditionalIcons}"\n'
"\n"
"[Files]\n"
f'Source: "C:\\ts\\dist\\{N}\\*"; '
'DestDir: "{app}"; '
"Flags: ignoreversion recursesubdirs createallsubdirs\n"
"\n"
"[Icons]\n"
f'Name: "{{group}}\\{N}"; Filename: "{{app}}\\{N}.exe"\n'
f'Name: "{{userdesktop}}\\{N}"; Filename: "{{app}}\\{N}.exe"; Tasks: desktopicon\n'
"\n"
"[Run]\n"
f'Filename: "{{app}}\\{N}.exe"; '
f'Description: "Lancer {N}"; '
"Flags: nowait postinstall skipifsilent\n"
"\n"
"[UninstallDelete]\n"
'Type: filesandordirs; Name: "{app}"\n'
"\n"
"[Code]\n"
"// Verifie que l'architecture est x64\n"
"function InitializeSetup(): Boolean;\n"
"begin\n"
" Result := True;\n"
" if not Is64BitInstallMode then begin\n"
f" MsgBox('{N} necessite Windows 64 bits.', mbError, MB_OK);\n"
" Result := False;\n"
" end;\n"
"end;\n"
)
out = DIST_DIR / "windows" / f"{APP_NAME}.iss"
out.parent.mkdir(parents=True, exist_ok=True)
# BOM UTF-8 + CRLF obligatoires pour Inno Setup
iss_crlf = iss.replace("\n", "\r\n")
out.write_bytes(b"\xef\xbb\xbf" + iss_crlf.encode("utf-8"))
ok(f"Script Inno Setup généré : {out}")
return out
# ─── Nettoyage ────────────────────────────────────────────────────────────────
def clean():
step("🧹 Nettoyage...")
for d in [BUILD_DIR, DIST_DIR]:
if d.exists():
shutil.rmtree(d); ok(f"Supprimé : {d}/")
for f in Path(".").glob("*.spec"):
f.unlink(); ok(f"Supprimé : {f}")
for icon in [ICON_ICO, ICON_PNG]:
if icon.exists():
icon.unlink(); ok(f"Supprimé : {icon}")
ok("Nettoyage terminé.")
# ─── Point d'entrée ───────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description=f"Build + install {APP_NAME}",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent("""
Exemples :
python build.py build + raccourcis (auto-detect OS)
python build.py --linux build Linux + raccourci GNOME
python build.py --windows build Windows + raccourci Menu Démarrer
python build.py --both les deux
python build.py --no-install build sans créer de raccourcis
python build.py --uninstall supprimer le raccourci GNOME
python build.py --clean supprimer artefacts build/dist/
"""),
)
parser.add_argument("--linux", action="store_true")
parser.add_argument("--windows", action="store_true")
parser.add_argument("--both", action="store_true")
parser.add_argument("--no-install", action="store_true",
help="Ne pas installer de raccourci")
parser.add_argument("--install-only", action="store_true",
help="Installer raccourci sans recompiler")
parser.add_argument("--uninstall", action="store_true",
help="Désinstaller raccourci GNOME")
parser.add_argument("--clean", action="store_true")
args = parser.parse_args()
os.chdir(Path(__file__).parent)
do_install = not args.no_install
if args.clean:
clean(); return
if args.uninstall:
uninstall_linux_shortcut(); return
if args.install_only:
binary = Path(f"dist/linux/{APP_NAME}")
if binary.exists():
generate_icons()
install_linux_shortcut(binary)
else:
err(f"Binaire introuvable : {binary}")
err("Lancez d'abord : python build.py --linux")
return
if args.both:
build_linux(install=do_install)
build_windows(install=do_install)
return
if args.windows:
build_windows(install=do_install); return
if args.linux or platform.system() == "Linux":
build_linux(install=do_install)
elif platform.system() == "Windows":
build_windows(install=do_install)
else:
err(f"Plateforme inconnue : {platform.system()}")
err("Utilisez --linux ou --windows")
if __name__ == "__main__":
main()