Files
Nomarchy/tools/check-install-safety.py
Bernardo Magri 11d6a3df0f
Some checks failed
Check / eval (push) Has been cancelled
feat(install): #61 review Source line tracks offline/online
Gate the Review panel Source wording on the same OFFLINE flag as the
cache.nixos.org probe (ISO-only vs may use network binary caches).
Extend installer-safety pure contract so both phrases and $SOURCE_NET
cannot regress.

Also promote Bernardo-accepted A/B/C batch → NEXT #55–#63 and close #61.

Verified: V0 (check-install-safety + bash -n + checks.installer-safety).
2026-07-10 08:28:20 +01:00

169 lines
5.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Installer safety guards contract (flake check `installer-safety`).
Usage: check-install-safety.py <nomarchy-install.sh>
Locks the BACKLOG #54 safety guards (+ #61 review Source line) against
silent regression:
1. Offline compose-lock failure fail-closes (no network `flake lock`).
2. Disk signature warn for NTFS / BitLocker / Microsoft / LUKS.
3. HM pre-activate failure drops NOMARCHY-DESKTOP-NOT-THEMED.txt.
4. Review panel Source line tracks the same OFFLINE probe as the install path.
Also re-checks the password ≥8 interactive floors (V0-complete).
Pure — no VM, no ISO.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
script = Path(sys.argv[1]).read_text(encoding="utf-8")
def check(cond: bool, msg: str) -> None:
if not cond:
sys.exit(f"installer-safety: {msg}")
# ── 1. Offline compose-lock fail-closed ─────────────────────────────────
# The offline branch must fail hard; `nix flake lock` only on the online path.
check(
"Offline lock composition failed and there is no network to fall back to"
in script,
"missing offline compose-lock fail-closed message",
)
# Fail-closed is gated on OFFLINE=true.
check(
re.search(
r'if \[\[ "\$OFFLINE" == true \]\]; then\s*\n'
r'\s*fail "Offline lock composition failed',
script,
),
"offline fail-closed is not gated on OFFLINE=true",
)
# Network flake lock must sit in the non-offline fallback, not the offline arm.
flake_lock = script.find('nix --extra-experimental-features "nix-command flakes" flake lock')
offline_fail = script.find("Offline lock composition failed and there is no network")
check(flake_lock > 0 and offline_fail > 0, "compose-lock / flake lock markers missing")
check(
flake_lock > offline_fail,
"network `flake lock` appears before the offline fail-closed arm",
)
# Between offline fail and flake lock there must not be an unguarded lock call
# earlier in the file for this recovery path.
check(
"Offline lock composition failed — resolving over the network." in script,
"missing online-only compose-lock fallback warn",
)
# ── 2. Disk signature warn ──────────────────────────────────────────────
sig_re = re.search(
r"grep -qiE '([^']+)'\s*<<<\s*\"\$existing_sig\"",
script,
)
check(sig_re is not None, "disk signature grep missing")
pat = sig_re.group(1)
check(
all(tok in pat for tok in ("ntfs", "bitlocker", "microsoft", "crypto_luks")),
f"signature pattern incomplete: {pat!r}",
)
check(
"Windows/BitLocker/NTFS or LUKS" in script,
"missing user-facing disk-signature warn text",
)
# Pure behavioural: the same regex the installer uses.
rx = re.compile(pat, re.I)
samples_hit = [
"ntfs\n",
"TYPE=\"ntfs\"",
"BitLocker",
"Microsoft reserved",
"crypto_LUKS",
"TYPE=\"crypto_LUKS\"",
]
samples_miss = [
"",
"ext4\n",
"btrfs\nvfat",
"TYPE=\"xfs\"",
]
for s in samples_hit:
check(bool(rx.search(s)), f"signature regex should match {s!r}")
for s in samples_miss:
check(not rx.search(s), f"signature regex should NOT match {s!r}")
# ── 3. Pre-activate recovery hint ───────────────────────────────────────
check(
"NOMARCHY-DESKTOP-NOT-THEMED.txt" in script,
"missing pre-activate hint filename",
)
check(
"home-manager switch --flake ~/.nomarchy -b bak" in script,
"hint must tell the user the recovery command",
)
check(
'hint_file="/mnt/home/$USERNAME/NOMARCHY-DESKTOP-NOT-THEMED.txt"' in script
or 'hint_file="/mnt/home/$USERNAME/NOMARCHY-DESKTOP-NOT-THEMED.txt"'
in script.replace(" ", ""),
"hint must land on the target home under /mnt",
)
# Hint is only written on the failure arm (else of nixos-enter activate).
activate_if = script.find("if nixos-enter --root /mnt -- bash /root/nomarchy-hm-activate.sh")
hint_pos = script.find("NOMARCHY-DESKTOP-NOT-THEMED.txt")
check(activate_if > 0 and hint_pos > activate_if, "hint is not on the activate-failure arm")
# ── Password floors (V0-complete, keep the floor) ───────────────────────
check(
re.search(r"password for \$USERNAME \(min 8 chars\)", script) is not None,
"account password min-8 prompt missing",
)
check(
re.search(r"LUKS passphrase \(min 8 chars\)", script) is not None,
"LUKS passphrase min-8 prompt missing",
)
check(
script.count("[[ ${#") >= 2 and " -ge 8 ]]" in script,
"min-8 length checks missing",
)
# ── 4. Review Source line tracks OFFLINE (BACKLOG #61) ──────────────────
check(
"pinned into the ISO, no network needed" in script,
"missing offline Review Source wording",
)
check(
"may use network binary caches" in script,
"missing online Review Source wording",
)
check(
re.search(
r'if \[\[ "\$OFFLINE" == true \]\]; then\s*\n'
r'\s*SOURCE_NET="pinned into the ISO, no network needed"',
script,
),
"offline Source wording is not gated on OFFLINE=true via SOURCE_NET",
)
check(
'SOURCE_NET="pinned into the ISO; may use network binary caches"' in script,
"online SOURCE_NET assignment missing",
)
# Source gum line must expand $SOURCE_NET (not hardcode offline-only text).
# The line embeds nested quotes, so match from Source: through SOURCE_NET.
check(
re.search(r'"Source:\s+nomarchy.*\$SOURCE_NET"', script, re.DOTALL)
is not None,
"Source gum line missing or does not expand $SOURCE_NET",
)
# The only "no network needed" occurrence must be the offline SOURCE_NET assign.
check(
script.count("no network needed") == 1,
"offline Source phrase should appear exactly once (SOURCE_NET assign)",
)
print("installer-safety: all contracts hold")