zonza/launcher/stub.c
Ralph Mayola 2236f2d11f fix: icône barre de menu sur Tahoe — stub libpython + abandon de rumps
Sur macOS Tahoe (26.5), le NSStatusItem n'est jamais placé (fenêtre h=0) si :
1. l'exécutable du processus n'appartient pas au bundle lancé (le python
   framework se ré-exécute → identité dépareillée) → stub C qui dlopen
   libpython et exécute app.py DANS le processus du bundle ;
2. l'item est créé par rumps (cause interne non isolée, l'API native
   fonctionne) → app.py réécrit en pyobjc pur (NSStatusItem + NSMenu).

Le stub transmet argv à python quand il y en a (workers multiprocessing
de MLX) — sinon bombe à fork (9 instances observées). PYTHONUNBUFFERED
pour que les print atteignent ~/Library/Logs/Zonza.log. Auto-diagnostic
du placement de l'icône au démarrage. rumps retiré des dépendances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:54:44 +02:00

63 lines
2.7 KiB
C

/* 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 <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
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);
}