/* Stub de lancement Zonza — embarque libpython DANS le processus du bundle. * * Pourquoi pas un script bash qui exec python ? Sur macOS Tahoe (26), le * gestionnaire de barre de menu refuse de placer le NSStatusItem d'un processus * dont l'exécutable principal n'appartient pas au bundle lancé par LaunchServices * (le python framework se ré-exécute dans son Python.app interne → identité * dépareillée → icône créée mais jamais affichée, fenêtre de hauteur 0). * Vérifié par tests A/B/C/D le 2026-06-13 : seul un exécutable du bundle qui * garde son image (dlopen de libpython, pas d'exec) obtient un emplacement. * * Compilé par build_app.sh avec les chemins injectés en -D : * PYTHON_DYLIB — dylib du framework Python (Homebrew) * APP_PY — chemin absolu de app.py dans le dépôt * VENV_SITE — site-packages du venv du dépôt */ #include #include #include typedef int (*py_bytes_main_t)(int, char **); int main(int argc, char **argv) { /* logs de l'app → ~/Library/Logs/Zonza.log (le bundle n'a pas de terminal) */ const char *home = getenv("HOME"); if (home) { char logpath[1024]; snprintf(logpath, sizeof logpath, "%s/Library/Logs/Zonza.log", home); freopen(logpath, "a", stdout); freopen(logpath, "a", stderr); setvbuf(stdout, NULL, _IOLBF, 0); setvbuf(stderr, NULL, _IOLBF, 0); } /* le venv n'est pas détecté (pas de pyvenv.cfg à côté de notre binaire) : * on ajoute ses site-packages via PYTHONPATH avant l'init de python. */ setenv("PYTHONPATH", VENV_SITE, 1); /* sans tty, python bufferise stdout → les print n'atteindraient le log qu'à la * sortie du processus. */ setenv("PYTHONUNBUFFERED", "1", 1); void *lib = dlopen(PYTHON_DYLIB, RTLD_NOW | RTLD_GLOBAL); if (!lib) { fprintf(stderr, "zonza: dlopen libpython a échoué : %s\n", dlerror()); return 1; } py_bytes_main_t py_main = (py_bytes_main_t)dlsym(lib, "Py_BytesMain"); if (!py_main) { fprintf(stderr, "zonza: Py_BytesMain introuvable : %s\n", dlerror()); return 1; } /* CRITIQUE anti bombe-à-fork : multiprocessing (MLX/numpy) relance * sys.executable — c'est-à-dire CE stub — avec ses propres arguments * (-c … --multiprocessing-fork). Si on forçait app.py inconditionnellement, * chaque worker relancerait l'app entière (vérifié : 9 instances). * Donc : des arguments présents → on les transmet tels quels à python ; * aucun argument (lancement normal par l'utilisateur) → on lance app.py. */ if (argc > 1) { return py_main(argc, argv); } char *py_argv[] = {argv[0], APP_PY, NULL}; return py_main(2, py_argv); }