128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
"""Compilar y ejecutar un juego vía subprocess."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
|
|
from .config import Game
|
|
from .paths import repo_dir
|
|
|
|
LogFn = Callable[[str], None]
|
|
|
|
# Windows: evita que la compilación/ejecución (shell + make/gcc, apps de consola)
|
|
# abra ventanas negras cuando el lanzador corre como GUI sin consola (el .exe).
|
|
# En el resto de SO es 0 (sin efecto).
|
|
_NO_WINDOW = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
|
|
|
# Directorios habituales de herramientas de línea de comandos (Homebrew, MacPorts…).
|
|
# Una .app de macOS lanzada desde Finder/Dock NO hereda el PATH del shell de login,
|
|
# así que herramientas como cmake, instaladas con Homebrew en /opt/homebrew/bin
|
|
# (Apple Silicon) o /usr/local/bin (Intel), quedan fuera del PATH y `make` no las
|
|
# encuentra. Las añadimos explícitamente. En Linux estos paths no existen y se
|
|
# filtran solos.
|
|
_EXTRA_PATH_DIRS = (
|
|
"/opt/homebrew/bin",
|
|
"/opt/homebrew/sbin",
|
|
"/usr/local/bin",
|
|
"/usr/local/sbin",
|
|
"/opt/local/bin",
|
|
)
|
|
|
|
# Prefijos de Homebrew/MacPorts. Una .app lanzada desde Finder/Dock tampoco
|
|
# hereda CPATH/LIBRARY_PATH/PKG_CONFIG_PATH del shell, así que el compilador no
|
|
# encuentra las cabeceras (p.ej. <SDL3/SDL_filesystem.h>) ni las librerías
|
|
# instaladas con `brew`. Añadimos sus include/lib explícitamente. En Linux estos
|
|
# paths no existen y se filtran solos.
|
|
_EXTRA_PREFIXES = (
|
|
"/opt/homebrew",
|
|
"/usr/local",
|
|
"/opt/local",
|
|
)
|
|
|
|
|
|
def _noop(_: str) -> None:
|
|
pass
|
|
|
|
|
|
def _launch_env() -> dict[str, str]:
|
|
"""Entorno para los subprocesos con un PATH que incluye los directorios de
|
|
herramientas habituales aunque la app se lance desde Finder/Dock."""
|
|
env = os.environ.copy()
|
|
parts = env.get("PATH", "").split(os.pathsep)
|
|
parts = [p for p in parts if p]
|
|
for d in _EXTRA_PATH_DIRS:
|
|
if d not in parts and os.path.isdir(d):
|
|
parts.append(d)
|
|
env["PATH"] = os.pathsep.join(parts)
|
|
|
|
# Rutas de cabeceras (CPATH), librerías (LIBRARY_PATH) y pkg-config para que
|
|
# el compilador encuentre dependencias instaladas con brew/MacPorts.
|
|
includes, libs, pkgconfigs = [], [], []
|
|
for prefix in _EXTRA_PREFIXES:
|
|
inc, lib = f"{prefix}/include", f"{prefix}/lib"
|
|
if os.path.isdir(inc):
|
|
includes.append(inc)
|
|
if os.path.isdir(lib):
|
|
libs.append(lib)
|
|
pkgconfigs.append(f"{lib}/pkgconfig")
|
|
_prepend(env, "CPATH", includes)
|
|
_prepend(env, "LIBRARY_PATH", libs)
|
|
_prepend(env, "PKG_CONFIG_PATH", pkgconfigs)
|
|
return env
|
|
|
|
|
|
def _prepend(env: dict[str, str], var: str, dirs: list[str]) -> None:
|
|
"""Antepone dirs al valor de env[var] (lista separada por os.pathsep), sin
|
|
duplicar entradas ya presentes."""
|
|
existing = [p for p in env.get(var, "").split(os.pathsep) if p]
|
|
new = [d for d in dirs if d not in existing]
|
|
if new:
|
|
env[var] = os.pathsep.join(new + existing)
|
|
|
|
|
|
def _stream(cmd: str, cwd: Path, log: LogFn) -> int:
|
|
"""Ejecuta un comando de shell en cwd, retransmitiendo stdout/err línea a línea."""
|
|
log(f"$ {cmd} (cwd={cwd})")
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
cwd=str(cwd),
|
|
shell=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
bufsize=1,
|
|
env=_launch_env(),
|
|
creationflags=_NO_WINDOW,
|
|
)
|
|
assert proc.stdout is not None
|
|
for line in proc.stdout:
|
|
log(line.rstrip())
|
|
return proc.wait()
|
|
|
|
|
|
def run_game(root: Path, game: Game, log: LogFn = _noop) -> int:
|
|
"""Compila (si hay build_cmd) y ejecuta el juego. Devuelve el código de salida.
|
|
|
|
Si build_cmd falla, aborta sin ejecutar. Lanza FileNotFoundError si el repo
|
|
no está clonado.
|
|
"""
|
|
repo = repo_dir(root, game.id)
|
|
if not (repo / ".git").exists():
|
|
raise FileNotFoundError(
|
|
f"{game.name} no està descarregat. Prem Descarrega primer."
|
|
)
|
|
|
|
if game.build_cmd.strip():
|
|
log(f"Compilant {game.name}…")
|
|
code = _stream(game.build_cmd, repo, log)
|
|
if code != 0:
|
|
log(f"La compilació ha fallat (codi {code}). No s'executa.")
|
|
return code
|
|
|
|
log(f"Executant {game.name}…")
|
|
return _stream(game.run_cmd, repo, log)
|