fix(install): normalize empty keyboard variants
All checks were successful
Check / eval (push) Successful in 3m18s
All checks were successful
Check / eval (push) Successful in 3m18s
Treat the picker-only (none) row as an empty XKB variant at the shared input boundary and again in the template patcher. Guard the real downstream output and console keymap, and carry the sentinel through the offline installer VM. Verified: V0 full flake evaluation plus shell/Python/Nix/diff checks; V1 installer-keyboard, installer-safety, and template-SoT builds; V2 full KVM offline LUKS+swap install and themed first boot. No V3 required.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Installer safety guards contract (flake check `installer-safety`).
|
||||
|
||||
Usage: check-install-safety.py <nomarchy-install.sh>
|
||||
Usage: check-install-safety.py <nomarchy-install.sh> [<patch-template.py> <template>]
|
||||
|
||||
Locks the BACKLOG #54 safety guards (+ #61 review Source line) against
|
||||
silent regression:
|
||||
@@ -11,16 +11,27 @@ silent regression:
|
||||
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).
|
||||
Also re-checks the password ≥8 interactive floors (V0-complete) and, when
|
||||
given the patcher + template, the BACKLOG #91 no-keyboard-variant path.
|
||||
Pure — no VM, no ISO.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
if len(sys.argv) not in (2, 4):
|
||||
sys.exit(
|
||||
f"usage: {sys.argv[0]} <nomarchy-install.sh> "
|
||||
"[<patch-template.py> <template>]"
|
||||
)
|
||||
|
||||
script = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@@ -165,4 +176,84 @@ check(
|
||||
"offline Source phrase should appear exactly once (SOURCE_NET assign)",
|
||||
)
|
||||
|
||||
# ── 5. Keyboard no-variant normalization (BACKLOG #91) ────────────────
|
||||
if len(sys.argv) == 4:
|
||||
# The picker sentinel must be normalized after both input paths converge,
|
||||
# not only inside the interactive gum branch.
|
||||
normalize_re = re.compile(
|
||||
r'if \[\[ "\$KB_VARIANT" == "\(none\)" \]\]; then\s+'
|
||||
r'KB_VARIANT=""\s+fi'
|
||||
)
|
||||
normalizations = list(normalize_re.finditer(script))
|
||||
check(
|
||||
len(normalizations) == 1,
|
||||
"expected one set -e-safe keyboard `(none)` normalization",
|
||||
)
|
||||
normalize_at = normalizations[0].start() if normalizations else -1
|
||||
input_branch_end = script.find(
|
||||
'[[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]]', normalize_at
|
||||
)
|
||||
unattended_at = script.find('KB_VARIANT="${NOMARCHY_KB_VARIANT:-}"')
|
||||
check(
|
||||
unattended_at < normalize_at < input_branch_end,
|
||||
"keyboard variant normalization is not on the shared input boundary",
|
||||
)
|
||||
|
||||
values = {
|
||||
"hostname": "keyboard-test",
|
||||
"username": "nomarchy",
|
||||
"timezone": "UTC",
|
||||
"locale": "en_US.UTF-8",
|
||||
"keyboardLayout": "us",
|
||||
# This is the no-variant row as gum presents it. Even if a caller
|
||||
# reaches the patcher before shell normalization, it must stay data,
|
||||
# never become an XKB include name.
|
||||
"keyboardVariant": "(none)",
|
||||
"hashedPassword": "$6$test$hash",
|
||||
"autoLogin": False,
|
||||
"laptop": False,
|
||||
"thermald": False,
|
||||
"hardwareProfiles": [],
|
||||
"hardware": {},
|
||||
"resumeOffset": None,
|
||||
"rootUuid": None,
|
||||
}
|
||||
|
||||
patcher = Path(sys.argv[2])
|
||||
template = Path(sys.argv[3])
|
||||
with tempfile.TemporaryDirectory(prefix="nomarchy-keyboard-") as tmp:
|
||||
generated = Path(tmp) / "flake"
|
||||
shutil.copytree(template, generated)
|
||||
# Nix-store templates are read-only; the real installer copy is
|
||||
# writable, so reproduce that property in the build sandbox.
|
||||
for path in generated.iterdir():
|
||||
if path.is_file():
|
||||
path.chmod(path.stat().st_mode | 0o200)
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(patcher), str(generated)],
|
||||
input=json.dumps(values),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
check(
|
||||
result.returncode == 0,
|
||||
f"keyboard fixture patch failed: {result.stderr.strip()}",
|
||||
)
|
||||
|
||||
home = (generated / "home.nix").read_text(encoding="utf-8")
|
||||
system = (generated / "system.nix").read_text(encoding="utf-8")
|
||||
expected = (
|
||||
(home, 'nomarchy.keyboard.layout = "us";'),
|
||||
(home, 'nomarchy.keyboard.variant = "";'),
|
||||
(system, 'services.xserver.xkb.layout = "us";'),
|
||||
(system, 'services.xserver.xkb.variant = "";'),
|
||||
)
|
||||
for text, line in expected:
|
||||
check(line in text, f"generated configuration missing {line!r}")
|
||||
check(
|
||||
"(none)" not in home and "(none)" not in system,
|
||||
"keyboard picker sentinel leaked into generated Nix",
|
||||
)
|
||||
|
||||
print("installer-safety: all contracts hold")
|
||||
|
||||
@@ -95,6 +95,8 @@ echo " matrix: SWAP_GB=$TEST_SWAP_GB NO_LUKS=$TEST_NO_LUKS"
|
||||
echo 'export NOMARCHY_USERNAME=ada'
|
||||
echo 'export NOMARCHY_PASSWORD=nomarchy'
|
||||
echo 'export NOMARCHY_HOSTNAME=testbox'
|
||||
echo 'export NOMARCHY_KB_LAYOUT=us'
|
||||
echo 'export NOMARCHY_KB_VARIANT="(none)"'
|
||||
echo "export NOMARCHY_SWAP_GB=$TEST_SWAP_GB"
|
||||
echo 'export NOMARCHY_FINISH=poweroff'
|
||||
if [[ "$TEST_NO_LUKS" == "1" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user