fix(mesh): fix archy-rnodeconf exiting 1 on success (frozen exit() gotcha)

Confirmed on the just-packaged binary: `archy-rnodeconf --info` printed
everything correctly, then crashed with NameError: name 'exit' is not
defined and returned exit code 1. rnodeconf.py's own graceful_exit() calls
the bare exit() builtin, which is only ever defined by site.py for
interactive Python — a frozen PyInstaller app skips that init, so any
bundled script relying on it hits this the moment it tries to quit cleanly,
after the real work already succeeded. Classic, well-documented PyInstaller
gotcha; the standard fix is a runtime hook pre-defining exit/quit as
sys.exit before the bundled script's own code runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ssmithx 2026-07-04 14:52:28 +00:00
parent 3925843455
commit c692a8b52f
2 changed files with 24 additions and 0 deletions

View File

@ -43,9 +43,15 @@ echo "Built dist/archy-reticulum-daemon ($(du -h dist/archy-reticulum-daemon | c
# motivated shipping it as a first-class OS tool rather than an ad hoc script. # motivated shipping it as a first-class OS tool rather than an ad hoc script.
RNODECONF_SRC="$(find .venv/lib -maxdepth 5 -path '*/site-packages/RNS/Utilities/rnodeconf.py' -print -quit)" RNODECONF_SRC="$(find .venv/lib -maxdepth 5 -path '*/site-packages/RNS/Utilities/rnodeconf.py' -print -quit)"
if [ -n "$RNODECONF_SRC" ] && [ -f "$RNODECONF_SRC" ]; then if [ -n "$RNODECONF_SRC" ] && [ -f "$RNODECONF_SRC" ]; then
# --runtime-hook: rnodeconf's own graceful_exit() calls the bare
# exit()/quit() builtins, which only exist in interactive Python (site.py
# injects them) — a frozen app hits NameError right as it tries to quit
# cleanly, after all the real work already succeeded. See
# pyi_rthook_exit_builtins.py.
.venv/bin/pyinstaller --onefile --name archy-rnodeconf --clean --noconfirm \ .venv/bin/pyinstaller --onefile --name archy-rnodeconf --clean --noconfirm \
--collect-submodules RNS \ --collect-submodules RNS \
--collect-data RNS \ --collect-data RNS \
--runtime-hook pyi_rthook_exit_builtins.py \
-d noarchive \ -d noarchive \
"$RNODECONF_SRC" "$RNODECONF_SRC"
echo "Built dist/archy-rnodeconf ($(du -h dist/archy-rnodeconf | cut -f1))" echo "Built dist/archy-rnodeconf ($(du -h dist/archy-rnodeconf | cut -f1))"

View File

@ -0,0 +1,18 @@
# PyInstaller runtime hook — see build.sh.
#
# `exit()`/`quit()` aren't part of the language; they're `site.Quitter`
# instances the interactive interpreter injects into builtins at startup
# (site.py). A frozen PyInstaller app never runs that interactive-mode
# init, so any bundled script that calls bare `exit()` (RNS's own
# rnodeconf.py does, in its graceful_exit() cleanup path) hits
# `NameError: name 'exit' is not defined` right as it tries to quit
# cleanly — the real work above it already completed, but the process
# still exits 1, which is a foot-gun for anything scripting off the exit
# code. Pre-define both as sys.exit so that path is a no-op crash-wise.
import builtins
import sys
if not hasattr(builtins, "exit"):
builtins.exit = sys.exit
if not hasattr(builtins, "quit"):
builtins.quit = sys.exit