You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

261 lines
7.8 KiB

#!/usr/bin/env python3
import argparse
import importlib.util
import os
import shutil
import stat
import tempfile
from pathlib import Path
from typing import List
DEFAULT_EXCLUDES = {
".git",
".venv",
"venv",
"__pycache__",
".pytest_cache",
".mypy_cache",
".idea",
".vscode",
"dist",
"build",
}
DEFAULT_EXT_EXCLUDES = {
".pyc",
".pyo",
".pyd",
".log",
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Exporte un projet Pygame vers un package compatible Batocera"
)
parser.add_argument("--project-dir", default=".", help="Répertoire du projet à exporter (défaut: .)")
parser.add_argument("--entrypoint", default="main.py", help="Script Python principal à lancer (défaut: main.py)")
parser.add_argument("--name", default="duploGame", help="Nom du jeu/package (défaut: duploGame)")
parser.add_argument("--output-dir", default="dist", help="Dossier de sortie (défaut: dist)")
parser.add_argument(
"--mode",
choices=["folder", "single-file"],
default="folder",
help="Format d'export: folder (style retrotrivia) ou single-file (défaut: folder)",
)
parser.add_argument(
"--vendor-module",
action="append",
dest="vendor_modules",
default=None,
help="Module Python à embarquer (répétable). Par défaut: duplo_controller + bleak + dbus_fast",
)
parser.add_argument(
"--allow-missing-vendor",
action="store_true",
help="N'échoue pas si un module vendor demandé est introuvable",
)
return parser.parse_args()
def should_skip(path: Path, project_root: Path) -> bool:
rel = path.relative_to(project_root)
parts = set(rel.parts)
if parts & DEFAULT_EXCLUDES:
return True
if path.suffix.lower() in DEFAULT_EXT_EXCLUDES:
return True
return False
def copy_project(src_root: Path, dst_root: Path) -> None:
for root, dirs, files in os.walk(src_root):
root_path = Path(root)
dirs[:] = [d for d in dirs if not should_skip(root_path / d, src_root)]
for filename in files:
src_file = root_path / filename
if should_skip(src_file, src_root):
continue
rel = src_file.relative_to(src_root)
dst_file = dst_root / rel
dst_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_file, dst_file)
def _copy_path(src: Path, dst: Path) -> None:
if src.is_dir():
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def vendor_modules(package_root: Path, module_names: List[str], allow_missing: bool) -> List[str]:
vendor_root = package_root / "vendor"
vendor_root.mkdir(parents=True, exist_ok=True)
vendored = []
for module_name in module_names:
spec = importlib.util.find_spec(module_name)
if spec is None:
message = f"Module introuvable pour vendor: {module_name}"
if allow_missing:
print(f"[WARN] {message}")
continue
raise SystemExit(message)
if spec.submodule_search_locations:
src_path = Path(list(spec.submodule_search_locations)[0]).resolve()
dst_path = vendor_root / src_path.name
_copy_path(src_path, dst_path)
elif spec.origin:
src_file = Path(spec.origin).resolve()
dst_file = vendor_root / src_file.name
_copy_path(src_file, dst_file)
else:
message = f"Impossible de résoudre la source du module: {module_name}"
if allow_missing:
print(f"[WARN] {message}")
continue
raise SystemExit(message)
vendored.append(module_name)
return vendored
def build_folder_launcher(entrypoint: str) -> str:
return f'''#!/usr/bin/env python
# Auto-generated Batocera .pygame launcher (folder mode)
import os
import runpy
import sys
from pathlib import Path
def main():
game_dir = Path(__file__).resolve().parent
vendor_dir = game_dir / "vendor"
if vendor_dir.exists():
sys.path.insert(0, str(vendor_dir))
sys.path.insert(0, str(game_dir))
os.chdir(game_dir)
os.environ.setdefault("PYGAME_HIDE_SUPPORT_PROMPT", "1")
entry = game_dir / {entrypoint!r}
if not entry.exists():
raise SystemExit(f"Entrypoint introuvable: {{entry}}")
runpy.run_path(str(entry), run_name="__main__")
if __name__ == "__main__":
main()
'''
def build_single_file_launcher(game_name: str, entrypoint: str) -> str:
return f'''#!/usr/bin/env python
# Auto-generated Batocera .pygame launcher (single-file mode)
import os
import runpy
import shutil
import sys
import tempfile
from pathlib import Path
def main():
script_path = Path(__file__).resolve()
payload_dir = script_path.with_suffix("")
if not payload_dir.exists():
raise SystemExit(f"Dossier payload introuvable: {{payload_dir}}")
root = Path(tempfile.gettempdir()) / {('batocera_pygame_' + game_name)!r}
if root.exists():
shutil.rmtree(root, ignore_errors=True)
shutil.copytree(payload_dir, root)
vendor_dir = root / "vendor"
if vendor_dir.exists():
sys.path.insert(0, str(vendor_dir))
sys.path.insert(0, str(root))
os.chdir(root)
os.environ.setdefault("PYGAME_HIDE_SUPPORT_PROMPT", "1")
entry = root / {entrypoint!r}
if not entry.exists():
raise SystemExit(f"Entrypoint introuvable: {{entry}}")
runpy.run_path(str(entry), run_name="__main__")
if __name__ == "__main__":
main()
'''
def make_executable(path: Path) -> None:
mode = path.stat().st_mode
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
def main() -> None:
args = parse_args()
project_dir = Path(args.project_dir).resolve()
entrypoint = args.entrypoint
game_name = args.name
output_dir = Path(args.output_dir).resolve()
if not project_dir.exists():
raise SystemExit(f"Projet introuvable: {project_dir}")
if not (project_dir / entrypoint).exists():
raise SystemExit(f"Entrypoint introuvable: {entrypoint}")
vendor_modules_list = args.vendor_modules or ["duplo_controller", "bleak", "dbus_fast"]
output_path = output_dir / f"{game_name}.pygame"
with tempfile.TemporaryDirectory(prefix="batocera_pygame_export_") as tmp:
temp_root = Path(tmp)
package_root = temp_root / game_name
package_root.mkdir(parents=True, exist_ok=True)
copy_project(project_dir, package_root)
vendored = vendor_modules(
package_root,
module_names=vendor_modules_list,
allow_missing=args.allow_missing_vendor,
)
output_dir.mkdir(parents=True, exist_ok=True)
if args.mode == "folder":
game_output_dir = output_dir / game_name
if game_output_dir.exists():
shutil.rmtree(game_output_dir)
shutil.copytree(package_root, game_output_dir)
launcher_path = game_output_dir / f"{game_name}.pygame"
launcher_path.write_text(build_folder_launcher(entrypoint=entrypoint), encoding="utf-8")
make_executable(launcher_path)
output_path = launcher_path
else:
output_path.write_text(build_single_file_launcher(game_name=game_name, entrypoint=entrypoint), encoding="utf-8")
make_executable(output_path)
payload_dir = output_dir / game_name
if payload_dir.exists():
shutil.rmtree(payload_dir)
shutil.copytree(package_root, payload_dir)
print(f"Export termine: {output_path}")
if vendored:
print(f"Modules vendor inclus: {', '.join(vendored)}")
if __name__ == "__main__":
main()