# Recette de construction du bundle autonome. Voir build.py, qui prépare # l'environnement isolé puis appelle PyInstaller sur ce fichier. # # Choix du mode : un dossier, pas un fichier unique. L'outil se relance # lui-même (la surveillance appelle le téléchargement, l'assemblage, puis # l'interface), et un exécutable « onefile » réextrait la centaine de méga- # octets du bundle à *chaque* lancement, y compris pour ces relances. Le # dossier se compresse tout aussi bien pour la diffusion, et démarre # instantanément. from __future__ import annotations # Python 3.8 (build Windows 7) : les annotations "X | None" ne s'évaluent qu'à l'écriture des chaînes, jamais à l'exécution. import os from pathlib import Path from PyInstaller.utils.hooks import collect_data_files from build_support import ecrire_version_info SRC = Path(SPECPATH) APP_ICON = SRC / "assets" / "blink2video.png" # Favicon de l'interface web (serve.py, route /favicon.ico) : embarqué sous # assets/ pour que le chemin lu au démarrage (runtime.resource_dir() / # "assets" / ...) soit le même, figé ou depuis les sources. FAVICON = SRC / "assets" / "blink2video.ico" # Marqueur absent des bundles officiels. L'édition Windows 7 s'en sert pour # s'identifier et pour refuser une mise à jour vers le runtime Python 3.12. WIN7_MARKER = SRC / "assets" / "windows7-build.txt" # CSS/JS de la page web (serve.py), extraits pour ne plus vivre en dur dans # un .py : lus par runtime.resource_dir() à la racine du bundle, comme # FFMPEG ci-dessous, jamais sous assets/ (contrairement à FAVICON). SERVE_STYLE = SRC / "serve_style.css" SERVE_APP_JS = SRC / "serve_app.js" # ffmpeg est propre à la plateforme : on demande le binaire à imageio_ffmpeg, # installé dans l'environnement de construction, qui fournit celui de l'hôte. # À défaut, on reprend la copie déposée dans _vendor. PyInstaller ne sachant # pas produire pour un autre système que le sien, ce choix se fait à la # construction et vaut pour la plateforme courante uniquement. def _ffmpeg() -> str: # build.py a déjà choisi un binaire sachant incruster du texte, et l'a # désigné ici : c'est lui qui fait autorité. import os choisi = os.environ.get("BLINK_FFMPEG") if choisi: return choisi try: import imageio_ffmpeg return imageio_ffmpeg.get_ffmpeg_exe() except Exception: import glob trouves = sorted(glob.glob("_vendor/imageio_ffmpeg/binaries/ffmpeg-*")) if not trouves: raise SystemExit( "ffmpeg introuvable : installez imageio-ffmpeg dans " "l'environnement de construction." ) return trouves[0] def _ffprobe() -> str | None: # Best-effort seulement (secours Linux de build.py, voir ffmpeg_utilisable) : # merge_daily._outil_validation_media() sait déjà s'en passer, en retombant # sur une vérification ffmpeg plus stricte. import os return os.environ.get("BLINK_FFPROBE") or None def _runtime(): import importlib.util charge = importlib.util.spec_from_file_location("runtime", "runtime.py") runtime = importlib.util.module_from_spec(charge) charge.loader.exec_module(runtime) return runtime def _modules(runtime) -> list: return sorted({verbe.module for verbe in runtime.VERBES.values()} - {runtime.ENTREE} | {"runtime", "tzdata"}) FFMPEG = _ffmpeg() FFPROBE = _ffprobe() RUNTIME = _runtime() analysis = Analysis( ["blink2video.py"], pathex=["."], # ffmpeg (et ffprobe, quand build.py en a trouvé un) voyagent à la racine # du bundle : merge_daily.find_ffmpeg/_outil_validation_media les y # cherchent en premier, avant le PATH de la machine cible. binaries=[(FFMPEG, ".")] + ([(FFPROBE, ".")] if FFPROBE else []), # Windows n'a pas de base de fuseaux horaires système : sans ces données, # ZoneInfo("Europe/Paris") échoue et tout l'horodatage avec. datas=(collect_data_files("tzdata", include_py_files=False) # Non optionnels (contrairement à APP_ICON/FAVICON ci-dessous) : # serve.py les lit sans condition au chargement du module, leur # absence du bundle serait un crash au démarrage, pas une # dégradation propre. + [(str(SERVE_STYLE), "."), (str(SERVE_APP_JS), ".")] + ([(str(APP_ICON), ".")] if APP_ICON.exists() else []) + ([(str(FAVICON), "assets")] if FAVICON.exists() else []) + ([(str(WIN7_MARKER), ".")] if os.environ.get("BLINK_BUILD_TARGET") == "windows7" else [])), # Les verbes sont résolus par importlib au moment de l'appel : l'analyse # statique de PyInstaller ne peut pas les voir, il faut les nommer. # Déduits de runtime.VERBES : les verbes sont résolus par importlib au # moment de l'appel, donc invisibles à l'analyse statique. Les énumérer ici # à la main créerait une liste parallèle de plus, et c'est exactement ce # qui a fait échouer la CI après le renommage de « review » en « serve ». hiddenimports=_modules(RUNTIME), hookspath=[], runtime_hooks=[], # imageio_ffmpeg est écarté : son seul intérêt est de fournir un binaire, # déjà copié à la racine ci-dessus. Le laisser entrer le ferait embarquer # une seconde fois, 84 Mo pour rien. find_ffmpeg trouve celui de la racine # avant même de tenter cet import. excludes=["tkinter", "PyInstaller", "pytest", "imageio_ffmpeg"], noarchive=False, ) pyz = PYZ(analysis.pure) executable = EXE( pyz, analysis.scripts, [], exclude_binaries=True, name="blink2video", debug=False, strip=False, upx=False, # Sans fenêtre, comme pythonw.exe (déjà substitué à python.exe pour # l'exécution depuis les sources, voir autostart.py) : avec console=True, # même l'entrée de démarrage automatique d'un exe installé ouvrait une # console à chaque connexion, ce que toute l'icône de zone de # notification est justement censée éviter (constaté en conditions # réelles sur la VM Windows 7, et signalé par un utilisateur sur # Reddit). print() ne casse rien sans console : runtime.py compte déjà # dessus pour tourner sous pythonw.exe, un chemin déjà éprouvé, pas # nouveau avec ce changement. console=False, # Même mécanisme que lidar2map : PNG source portable, converti par # PyInstaller en ressource native sur la plateforme de construction. icon=str(APP_ICON), version=str(ecrire_version_info(RUNTIME.VERSION, SRC / ".version_info.txt")), ) COLLECT( executable, analysis.binaries, analysis.datas, strip=False, upx=False, name="blink2video", )