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>
63 lines
2.7 KiB
C
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);
|
|
}
|