diff --git a/reticulum-daemon/build.sh b/reticulum-daemon/build.sh index ad5964ee..f99642d3 100755 --- a/reticulum-daemon/build.sh +++ b/reticulum-daemon/build.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# Build the PyInstaller single-binary for the OTA (plan Phase 1 packaging). -# Output: dist/archy-reticulum-daemon — drop next to /usr/local/bin/archipelago. +# Build the PyInstaller single-binaries for the OTA (plan Phase 1 packaging). +# Outputs: dist/archy-reticulum-daemon, dist/archy-rnodeconf — drop both next +# to /usr/local/bin/archipelago. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" @@ -9,7 +10,7 @@ if [ ! -d .venv ]; then fi .venv/bin/pip install -q -r requirements.txt -r requirements-build.txt -rm -rf build dist archy-reticulum-daemon.spec +rm -rf build dist archy-reticulum-daemon.spec archy-rnodeconf.spec # --collect-submodules: RNS/LXMF load most of their own internals dynamically # (interface drivers, transport backends), which PyInstaller's static import @@ -30,3 +31,30 @@ rm -rf build dist archy-reticulum-daemon.spec reticulum_daemon.py echo "Built dist/archy-reticulum-daemon ($(du -h dist/archy-reticulum-daemon | cut -f1))" + +# archy-rnodeconf: RNS's own official RNode config/diagnostic tool +# (RNS.Utilities.rnodeconf — reads/sets frequency, bandwidth, spreading +# factor, coding rate, TX power on an attached RNode; also verifies firmware +# signatures and can flash/bootstrap a board). Shipped alongside the daemon +# so every node can inspect and reconfigure its own radio without needing a +# full RNS/Python dev environment set up by hand — see the checkpoint in +# docs/RETICULUM-TRANSPORT-PROGRESS.md for the incident (two nodes silently +# running at different spreading factors, invisible without this tool) that +# 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)" +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 \ + --collect-submodules RNS \ + --collect-data RNS \ + --runtime-hook pyi_rthook_exit_builtins.py \ + -d noarchive \ + "$RNODECONF_SRC" + echo "Built dist/archy-rnodeconf ($(du -h dist/archy-rnodeconf | cut -f1))" +else + echo "WARNING: rnodeconf.py not found at $RNODECONF_SRC (RNS version mismatch?) — skipping archy-rnodeconf build" >&2 +fi diff --git a/reticulum-daemon/pyi_rthook_exit_builtins.py b/reticulum-daemon/pyi_rthook_exit_builtins.py new file mode 100644 index 00000000..0b999229 --- /dev/null +++ b/reticulum-daemon/pyi_rthook_exit_builtins.py @@ -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 diff --git a/scripts/deploy-to-target.sh b/scripts/deploy-to-target.sh index eb95c29b..c93f3a50 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -257,6 +257,13 @@ ssh $SSH_OPTS "$TARGET_HOST" ' NEED_INSTALL="" command -v rsync >/dev/null 2>&1 || NEED_INSTALL="$NEED_INSTALL rsync" command -v python3 >/dev/null 2>&1 || NEED_INSTALL="$NEED_INSTALL python3" + # python3 -m venv exists but cannot bootstrap pip without the matching + # .-venv package on Debian — needed for reticulum-daemon/ + # build.sh (archy-reticulum-daemon / archy-rnodeconf packaging). + if command -v python3 >/dev/null 2>&1 && ! python3 -c "import ensurepip" >/dev/null 2>&1; then + PYVER=$(python3 -c "import sys; print(f\"{sys.version_info.major}.{sys.version_info.minor}\")") + NEED_INSTALL="$NEED_INSTALL python3.${PYVER#*.}-venv" + fi if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then echo " Node.js/npm not found — installing..." curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - 2>&1 | tail -3 @@ -585,6 +592,20 @@ else echo " ⚠️ Rust not installed on target, skipping backend build" fi +# reticulum-daemon tools (archy-reticulum-daemon, archy-rnodeconf) — skip with +# --frontend-only. Non-fatal on failure: these are supplementary mesh/radio +# tools, not required for the rest of the deploy to succeed, and build.sh +# handles its own venv/pip setup so a first run here is slower than later ones. +if [ "$FRONTEND_ONLY" = true ]; then + echo " Skipping reticulum-daemon tools build (--frontend-only)" +else + progress "Building reticulum-daemon tools (archy-reticulum-daemon, archy-rnodeconf)" + section_start + ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/reticulum-daemon && ./build.sh 2>&1" | sed 's/^/ /' \ + || echo " ⚠️ reticulum-daemon tools build failed — continuing without updating them" + section_end +fi + if [ "$LIVE" = true ]; then # Create rollback backup before deploying @@ -608,6 +629,26 @@ if [ "$LIVE" = true ]; then ssh $SSH_OPTS "$TARGET_HOST" "sudo cp $TARGET_DIR/core/target/release/archipelago /usr/local/bin/" fi + # Deploy reticulum-daemon tools (archy-reticulum-daemon, archy-rnodeconf) — + # skip with --frontend-only. Non-fatal: archipelago falls back to its dev + # venv path (ARCHY_RETICULUM_DAEMON_PY/_SCRIPT) if the packaged binary + # isn't present, so a missing/failed build here degrades rather than + # breaks mesh. archipelago is already stopped from the backend-binary + # step above, so this is a clean window to swap both binaries. + if [ "$FRONTEND_ONLY" = true ]; then + echo " Skipping reticulum-daemon tools deploy (--frontend-only)" + else + progress "Deploying reticulum-daemon tools" + for tool in archy-reticulum-daemon archy-rnodeconf; do + if ssh $SSH_OPTS "$TARGET_HOST" "[ -f $TARGET_DIR/reticulum-daemon/dist/$tool ]" 2>/dev/null; then + ssh $SSH_OPTS "$TARGET_HOST" "sudo cp $TARGET_DIR/reticulum-daemon/dist/$tool /usr/local/bin/ && sudo chmod +x /usr/local/bin/$tool" + echo " $tool deployed" + else + echo " ⚠️ $tool not built — leaving existing /usr/local/bin/$tool (if any) in place" + fi + done + fi + # Deploy frontend (preserve aiui/ and claude-login.html — they are NOT part of the neode-ui build) progress "Deploying frontend" ssh $SSH_OPTS "$TARGET_HOST" "sudo find /opt/archipelago/web-ui -mindepth 1 -maxdepth 1 ! -name 'aiui' ! -name 'claude-login.html' -exec rm -rf {} +"