"""Prépare, une seule fois par poste, tout ce qu'il faut pour empaqueter un jeu en exécutable autonome : Python portable (embeddable, python.org) et les paquets pip nécessaires (Flask + ses dépendances), mis en cache localement sous data/publish_vendor/ (non commité, voir .gitignore — même dossier que data/secret_key/data/users.db, déjà runtime-only). La toute PREMIÈRE publication d'un jeu sur un poste donné télécharge ces ~15 Mo (connexion internet nécessaire, une seule fois) ; toutes les publications suivantes réutilisent le cache, hors-ligne. Le JOUEUR qui reçoit le zip final, lui, n'a jamais besoin d'internet — ce cache ne concerne que la machine qui PUBLIE. Fonctions séparées et mockables (download_python_embeddable/ vendor_flask) pour que les tests ne déclenchent jamais un vrai téléchargement.""" import os import subprocess import sys import urllib.request import zipfile _BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) VENDOR_DIR = os.environ.get("FORGE_PUBLISH_VENDOR_DIR") or os.path.join(_BASE_DIR, "data", "publish_vendor") PYTHON_VERSION = "3.11.9" PYTHON_EMBED_URL = f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip" PYTHON_EMBED_DIR = os.path.join(VENDOR_DIR, "python-embed-amd64") PYLIBS_DIR = os.path.join(VENDOR_DIR, "pylibs") # Mêmes versions que requirements.txt — seul Flask est listé explicitement, # ses dépendances (Werkzeug/Jinja2/MarkupSafe/click/itsdangerous/blinker) # suivent automatiquement via pip. FLASK_SPEC = "Flask==3.0.3" def download_python_embeddable(): """Télécharge et extrait le ZIP Python embeddable officiel si absent du cache — no-op si déjà présent (idempotent, sûr à rappeler à chaque publication).""" marker = os.path.join(PYTHON_EMBED_DIR, "python.exe") if os.path.isfile(marker): return PYTHON_EMBED_DIR os.makedirs(PYTHON_EMBED_DIR, exist_ok=True) zip_path = os.path.join(VENDOR_DIR, "python-embed-amd64.zip") urllib.request.urlretrieve(PYTHON_EMBED_URL, zip_path) with zipfile.ZipFile(zip_path) as zf: zf.extractall(PYTHON_EMBED_DIR) os.remove(zip_path) _enable_pylibs_path(PYTHON_EMBED_DIR) return PYTHON_EMBED_DIR def _enable_pylibs_path(python_embed_dir): """Le ZIP embeddable désactive site-packages par défaut (python311._pth ne contient que "python311.zip" et "."). On ajoute une ligne "pylibs" (chemin relatif à python.exe) plutôt que de réactiver tout le mécanisme "site" — les paquets vendorés (Flask...) sont de simples dossiers purement Python, aucun besoin d'aller plus loin.""" pth_files = [f for f in os.listdir(python_embed_dir) if f.endswith("._pth")] if not pth_files: return pth_path = os.path.join(python_embed_dir, pth_files[0]) with open(pth_path, encoding="utf-8") as f: content = f.read() if "pylibs" not in content: with open(pth_path, "a", encoding="utf-8") as f: f.write("\npylibs\n") def vendor_flask(): """pip install --target=... : installe Flask (paquets purs Python, indépendants de la version exacte de l'interpréteur embeddable) dans le cache, réutilisé par tous les exports suivants — no-op si déjà fait.""" if os.path.isdir(PYLIBS_DIR) and os.listdir(PYLIBS_DIR): return PYLIBS_DIR os.makedirs(PYLIBS_DIR, exist_ok=True) subprocess.run( [sys.executable, "-m", "pip", "install", "--target", PYLIBS_DIR, "--no-compile", FLASK_SPEC], check=True, ) return PYLIBS_DIR def ensure_vendor_ready(): """Point d'entrée unique appelé par build_package.py — télécharge/ installe ce qui manque, puis renvoie (dossier_python_embed, dossier_pylibs).""" python_dir = download_python_embeddable() pylibs_dir = vendor_flask() return python_dir, pylibs_dir