"""Resolución de rutas: dónde está games.toml y dónde guardar los datos. Compilado con Nuitka, ``__compiled__`` existe. En modo ``--onefile`` la carpeta del binario real la expone ``NUITKA_ONEFILE_DIRECTORY`` (Nuitka 4.x); con versiones que usan ``NUITKA_ONEFILE_BINARY`` tomamos su carpeta; si no, ``sys.executable`` (standalone). Ejecutando desde fuente usamos la raíz del proyecto (la carpeta que contiene ``jlauncher``). """ from __future__ import annotations import os import shutil import sys from pathlib import Path CONFIG_NAME = "games.toml" # Carpeta de soporte en macOS cuando corremos como .app: ~/Library/Application Support/… APP_SUPPORT_VENDOR = "jailgames" APP_SUPPORT_APP = "jlauncher" def is_compiled() -> bool: """True si corremos como binario Nuitka (define ``__compiled__`` en cada módulo).""" return "__compiled__" in globals() def base_dir() -> Path: """Carpeta base junto a la que viven games.toml y jlauncher_data.""" if is_compiled(): directory = os.environ.get("NUITKA_ONEFILE_DIRECTORY") if directory: return Path(directory).resolve() binary = os.environ.get("NUITKA_ONEFILE_BINARY") if binary: return Path(binary).resolve().parent return Path(sys.executable).resolve().parent # Desde fuente: raíz del proyecto = padre del paquete jlauncher/ return Path(__file__).resolve().parent.parent def macos_app_bundle() -> Path | None: """Ruta del .app si corremos como bundle de macOS (``…/jlauncher.app``); si no, None. En un app bundle el ejecutable vive en ``…/jlauncher.app/Contents/MacOS/jlauncher``, dentro de un árbol no escribible al instalarse en ``/Applications``. Detectarlo nos deja redirigir datos y settings a ``~/Library/Application Support``. """ if not is_compiled() or sys.platform != "darwin": return None exe = Path(sys.executable).resolve() for parent in exe.parents: if parent.suffix == ".app": return parent return None def app_icon_path() -> Path | None: """Ruta al PNG del icono para la UI (icono de ventana/Dock y diálogo «Quant a»). En .app vive en ``Contents/Resources/icon.png``; desde fuente, en ``assets/icon.png``. Devuelve None si no se encuentra. """ bundle = macos_app_bundle() if bundle is not None: candidate = bundle / "Contents" / "Resources" / "icon.png" else: candidate = base_dir() / "assets" / "icon.png" return candidate if candidate.exists() else None def support_dir() -> Path: """Carpeta de soporte escribible en macOS (.app); se crea si no existe.""" root = ( Path.home() / "Library" / "Application Support" / APP_SUPPORT_VENDOR / APP_SUPPORT_APP ) root.mkdir(parents=True, exist_ok=True) return root def writable_base() -> Path: """Base donde escribir datos/settings: Application Support si .app, si no base_dir().""" if macos_app_bundle() is not None: return support_dir() return base_dir() def config_file() -> Path: """Ruta a games.toml. En .app vive en Application Support (editable y persistente); se siembra la primera vez desde ``Contents/Resources/games.toml`` del bundle. Si no, junto al ejecutable / raíz. """ bundle = macos_app_bundle() if bundle is not None: target = writable_base() / CONFIG_NAME if not target.exists(): seed = bundle / "Contents" / "Resources" / CONFIG_NAME if seed.exists(): shutil.copy2(seed, target) return target return base_dir() / CONFIG_NAME def data_root(data_dir: str = "jlauncher_data") -> Path: """Carpeta raíz de datos; se crea si no existe.""" root = writable_base() / data_dir root.mkdir(parents=True, exist_ok=True) return root def game_dir(root: Path, game_id: str) -> Path: """Carpeta de un juego: //.""" return root / game_id def repo_dir(root: Path, game_id: str) -> Path: """Carpeta del clone git: //repo/.""" return game_dir(root, game_id) / "repo" def metadata_dir(root: Path, game_id: str) -> Path: """Carpeta de metadata cacheada: //metadata/.""" return game_dir(root, game_id) / "metadata"