2cfc7112fc
scripts/termux-preflight.py runs the docs/termux-operator.md §0.1-0.5 checks on the phone and prints a PASS/FAIL report: python>=3.11, requests/rich/websockets import, cryptography+Fernet round-trip (version floor treated as soft), srp C-ext OR the pure shim, that client/operator import without the server stack, and an AF_UNIX bind under the $TMPDIR runtime root. Makes the one remaining on-device Phase-0 step a single command. Exits non-zero on any hard failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
163 lines
5.6 KiB
Python
Executable File
163 lines
5.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""On-device Phase-0 preflight for a hack-house operator (Termux / aarch64).
|
|
|
|
Run this ON THE PHONE after installing the operator deps. It cannot be run
|
|
authoritatively from an x86_64/glibc dev box — the whole point is to exercise
|
|
the aarch64 + Android-bionic runtime (wheel availability, Termux package
|
|
versions, the $TMPDIR socket path). See docs/termux-operator.md §2.
|
|
|
|
Usage (in Termux, from the repo root):
|
|
|
|
pkg install python # 3.11+ recommended
|
|
pip install -r requirements-operator.txt # srp is best-effort (shim covers it)
|
|
python scripts/termux-preflight.py
|
|
|
|
It prints a PASS/FAIL line per check and exits non-zero if any hard check fails.
|
|
Copy the whole report back to the operator/dev session to decide next steps.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import os
|
|
import platform
|
|
import socket
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
# Repo root on sys.path so `cmd_chat.*` imports resolve when run in-tree.
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
_hard_failures = 0
|
|
_soft_notes = 0
|
|
|
|
|
|
def _line(status: str, name: str, detail: str = "") -> None:
|
|
tail = f" — {detail}" if detail else ""
|
|
print(f"[{status:^4}] {name}{tail}")
|
|
|
|
|
|
def hard(name: str, ok: bool, detail: str = "") -> bool:
|
|
global _hard_failures
|
|
if ok:
|
|
_line("PASS", name, detail)
|
|
else:
|
|
_hard_failures += 1
|
|
_line("FAIL", name, detail)
|
|
return ok
|
|
|
|
|
|
def soft(name: str, ok: bool, detail: str = "") -> None:
|
|
"""A non-blocking observation (won't fail the run, but worth reporting)."""
|
|
global _soft_notes
|
|
if ok:
|
|
_line("PASS", name, detail)
|
|
else:
|
|
_soft_notes += 1
|
|
_line("WARN", name, detail)
|
|
|
|
|
|
def _try_import(mod: str) -> tuple[bool, str]:
|
|
try:
|
|
m = importlib.import_module(mod)
|
|
return True, getattr(m, "__version__", "?")
|
|
except Exception as e: # ImportError, build/ABI errors, etc.
|
|
return False, f"{type(e).__name__}: {e}"
|
|
|
|
|
|
def main() -> int:
|
|
print("=" * 60)
|
|
print("hack-house operator — Termux Phase-0 preflight")
|
|
print(f"platform : {platform.platform()}")
|
|
print(f"machine : {platform.machine()}")
|
|
print(f"python : {sys.version.split()[0]} ({sys.executable})")
|
|
print("=" * 60)
|
|
|
|
# 0.1 — Python version (need 3.11+ for stdlib tomllib used by models.toml).
|
|
hard(
|
|
"python >= 3.11 (tomllib for models.toml)",
|
|
sys.version_info >= (3, 11),
|
|
detail=f"got {sys.version_info.major}.{sys.version_info.minor}",
|
|
)
|
|
|
|
# 0.2 — easy pure-Python / wheeled deps.
|
|
for mod in ("requests", "rich", "websockets"):
|
|
ok, ver = _try_import(mod)
|
|
hard(f"import {mod}", ok, ver)
|
|
|
|
# 0.4 — cryptography: the operator only uses Fernet; version floor is soft.
|
|
ok, ver = _try_import("cryptography")
|
|
hard("import cryptography", ok, ver)
|
|
if ok:
|
|
try:
|
|
from cryptography.fernet import Fernet
|
|
|
|
Fernet(Fernet.generate_key()).encrypt(b"ok")
|
|
hard("cryptography.fernet.Fernet round-trip", True)
|
|
except Exception as e:
|
|
hard("cryptography.fernet.Fernet round-trip", False, str(e))
|
|
# The server pins >=46, but the operator only needs Fernet; flag if lower
|
|
# so we consciously accept it (§0.4), don't hard-fail.
|
|
try:
|
|
major = int(str(ver).split(".")[0])
|
|
soft(
|
|
"cryptography >= 46 (server pin; operator needs only Fernet)",
|
|
major >= 46,
|
|
f"have {ver}; Fernet works on far older — acceptable",
|
|
)
|
|
except ValueError:
|
|
pass
|
|
|
|
# 0.5 — srp: real C-ext if it built, else the pure-Python shim MUST cover it.
|
|
ok_real, ver = _try_import("srp")
|
|
if ok_real:
|
|
soft("import srp (C-ext built)", True, ver)
|
|
else:
|
|
soft("import srp (C-ext)", False, "not built — using pure-Python shim")
|
|
ok_shim, detail = _try_import("cmd_chat.client._srp_pure")
|
|
hard("import cmd_chat.client._srp_pure (fallback)", ok_shim, detail)
|
|
|
|
# Client binds *some* srp (real or shim) without error.
|
|
ok_client, detail = _try_import("cmd_chat.client.client")
|
|
hard("import cmd_chat.client.client", ok_client, detail)
|
|
if ok_client:
|
|
import cmd_chat.client.client as c
|
|
|
|
_line("INFO", "client.srp backend", c.srp.__name__)
|
|
|
|
# Operator package imports without the server stack (sanic/pydantic).
|
|
ok_op, detail = _try_import("cmd_chat.operator")
|
|
hard("import cmd_chat.operator (no server deps needed)", ok_op, detail)
|
|
|
|
# Phase-1 dependency: AF_UNIX control socket under the runtime root ($TMPDIR
|
|
# on Termux). Prove we can actually bind one there.
|
|
try:
|
|
from cmd_chat.operator.session import runtime_root
|
|
|
|
root = runtime_root()
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
sp = root / "preflight.sock"
|
|
if sp.exists():
|
|
sp.unlink()
|
|
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
srv.bind(str(sp))
|
|
srv.close()
|
|
sp.unlink()
|
|
hard("AF_UNIX control socket under runtime root", True, str(root))
|
|
except Exception as e:
|
|
hard("AF_UNIX control socket under runtime root", False, str(e))
|
|
_line("INFO", "runtime root (should be writable)",
|
|
os.environ.get("TMPDIR", tempfile.gettempdir()))
|
|
|
|
print("=" * 60)
|
|
if _hard_failures == 0:
|
|
print(f"RESULT: PASS ({_soft_notes} warning(s)) — operator deps OK on-device")
|
|
return 0
|
|
print(f"RESULT: FAIL ({_hard_failures} hard failure(s), {_soft_notes} warning(s))")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|