Téléverser les fichiers vers "/"
This commit is contained in:
parent
f95bce07d7
commit
5ee99efaff
|
|
@ -0,0 +1,851 @@
|
|||
#!/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()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/.venv/bin/activate"
|
||||
exec python "$SCRIPT_DIR/transcribe_station.py" "$@"
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue