"""Assemble le zip exporté d'un jeu (bouton "Publier", voir routes/publish/publish_game.py) : un dossier autonome contenant Python portable + Flask (vendorés une fois, voir vendor_runtime.py), une copie figée du jeu (screens/db/filters/core minimal + templates/play.html + le sous-ensemble utile de static/), sa base SQLite et ses fichiers envoyés (uploads/), et les scripts de lancement (player_app.py/run.bat). Ne modifie JAMAIS les fichiers réels de l'app (tout se passe dans un dossier de travail temporaire, supprimé après envoi de la réponse) ni la vraie base de données du jeu (copiée, jamais déplacée).""" import os import shutil import tempfile import zipfile import db from . import vendor_runtime _PUBLISH_DIR = os.path.dirname(os.path.abspath(__file__)) _BASE_DIR = os.path.dirname(_PUBLISH_DIR) # Sous-ensemble de static/ réellement nécessaire au mode jouable (voir # templates/play.html) — jamais les assets propres à l'éditeur seul # (icônes de l'interface de l'éditeur mises à part, déjà dans icons/). _STATIC_ITEMS = ["style.css", "csrf_fetch.js", "icons", "branding", "vendor/fonts", "vendor/animate.min.css", "js/play", "characters"] _SLUG_PLACEHOLDER = "_SLUG_PLACEHOLDER" def _copy_engine_sources(staging_dir): """Copie la partie du moteur nécessaire au mode jouable SEUL (jamais l'éditeur ni l'authentification — voir le constat d'exploration dans le plan : screens/ et db/ sont déjà totalement découplés de auth// routes/, core/flask_app.py et core/jinja_filters.py sont les 2 SEULS fichiers de core/ dont ils dépendent réellement).""" for pkg in ("screens", "db", "filters"): shutil.copytree(os.path.join(_BASE_DIR, pkg), os.path.join(staging_dir, pkg), ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) core_dir = os.path.join(staging_dir, "core") os.makedirs(core_dir, exist_ok=True) for fname in ("__init__.py", "flask_app.py", "jinja_filters.py"): shutil.copy2(os.path.join(_BASE_DIR, "core", fname), os.path.join(core_dir, fname)) templates_dir = os.path.join(staging_dir, "templates") os.makedirs(templates_dir, exist_ok=True) shutil.copy2(os.path.join(_BASE_DIR, "templates", "play.html"), os.path.join(templates_dir, "play.html")) static_dir = os.path.join(staging_dir, "static") for item in _STATIC_ITEMS: src = os.path.join(_BASE_DIR, "static", item) dst = os.path.join(static_dir, item) os.makedirs(os.path.dirname(dst), exist_ok=True) if os.path.isdir(src): shutil.copytree(src, dst) else: shutil.copy2(src, dst) def _copy_game_data(staging_dir, slug): """Copie le dossier RÉEL du jeu (game.db + uploads/, voir db.game_dir) tel quel — jamais déplacé, jamais modifié.""" src = db.game_dir(slug) dst = os.path.join(staging_dir, "projects", slug) shutil.copytree(src, dst, ignore=shutil.ignore_patterns("index.html", "index.css", "index.js")) def _write_player_app(staging_dir, slug): template_path = os.path.join(_PUBLISH_DIR, "player_app_template.py") with open(template_path, encoding="utf-8") as f: content = f.read() content = content.replace(_SLUG_PLACEHOLDER, slug) with open(os.path.join(staging_dir, "player_app.py"), "w", encoding="utf-8") as f: f.write(content) def _write_run_bat(staging_dir): src = os.path.join(_PUBLISH_DIR, "run_bat_template.bat") shutil.copy2(src, os.path.join(staging_dir, "run.bat")) def _zip_dir(staging_dir, zip_path): with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for root, _dirs, files in os.walk(staging_dir): for name in files: full = os.path.join(root, name) rel = os.path.relpath(full, staging_dir) zf.write(full, rel) def build_game_zip(slug): """Construit le zip et renvoie son chemin (dans un dossier temporaire — à l'appelant de le supprimer une fois la réponse HTTP envoyée, voir routes/publish/publish_game.py).""" game = db.game_meta(slug) python_dir, pylibs_dir = vendor_runtime.ensure_vendor_ready() staging_dir = tempfile.mkdtemp(prefix="forge_publish_") try: shutil.copytree(python_dir, os.path.join(staging_dir, "python-embed")) shutil.copytree(pylibs_dir, os.path.join(staging_dir, "python-embed", "pylibs")) _copy_engine_sources(staging_dir) _copy_game_data(staging_dir, slug) _write_player_app(staging_dir, slug) _write_run_bat(staging_dir) zip_fd, zip_path = tempfile.mkstemp(prefix="forge_publish_", suffix=".zip") os.close(zip_fd) _zip_dir(staging_dir, zip_path) finally: shutil.rmtree(staging_dir, ignore_errors=True) return zip_path, game["name"]