Recently Written · git

waybase

Bootable installer: KISS, Gentoo, Arch or Alpine + Nix + a Wayland compositor (sway, river, labwc, dwl)

git clone https://github.com/equwal/waybase

Log | Files | Refs


test-qemu.py (6990 bytes)

1 #!/usr/bin/env python3
2 """Unattended QEMU test of waybase.img for one base.
3 
4 Usage: test-qemu.py IMG WORKDIR BASE "COMPOSITORS" [encrypt|plain]
5 Stdlib only. Env: QEMU, QEMU_IMG, OVMF (UEFI firmware), MEM (MiB, 2048),
6 ACCEL (kvm|tcg), CPU (default Nehalem = x86-64-v2, so no binary may need AVX).
7 
8 1. UEFI boot of the live image as a USB stick; install.sh with an answers file.
9 2. BIOS boot of the installed disk to the passphrase or login prompt.
10 3. UEFI boot of the installed disk: unlock, log in as the user, then check
11    `nix --version`, the nix daemon, and that each compositor starts headless
12    and creates a Wayland socket.
13 Exit 0 only if every check passes. The serial log goes to WORKDIR/BASE.log.
14 """
15 import os, re, socket, subprocess, sys, time
16 
17 img, work, base, comps = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
18 encrypt = (sys.argv[5] if len(sys.argv) > 5 else "encrypt") == "encrypt"
19 disk = os.path.join(work, f"{base}.qcow2")
20 QEMU = os.environ.get("QEMU", "qemu-system-x86_64")
21 OVMF = os.environ.get("OVMF", "/usr/share/ovmf/bios.bin")
22 ACCEL, CPU = os.environ.get("ACCEL", "kvm"), os.environ.get("CPU", "Nehalem")
23 LOG = open(os.path.join(work, f"{base}.log"), "a", encoding="utf-8", errors="replace")
24 PW = {"luks": "test-luks", "user": "test-user", "root": "test-root"}
25 
26 
27 def note(s):
28     LOG.write(f"\n##### {s}\n"); LOG.flush(); print(f"##### {base}: {s}", flush=True)
29 
30 
31 class VM:
32     def __init__(self, uefi, live, port=45560):
33         args = [QEMU, "-accel", ACCEL, "-cpu", CPU, "-m", os.environ.get("MEM", "2048"), "-smp", "2",
34                 "-display", "none", "-monitor", "none",
35                 "-chardev", f"socket,id=s0,host=127.0.0.1,port={port},server=on,wait=on",
36                 "-serial", "chardev:s0", "-nic", "user,model=virtio-net-pci",
37                 "-drive", f"file={disk},if=virtio,format=qcow2,cache=unsafe"]
38         if uefi:
39             args += ["-bios", OVMF]
40         if live:
41             args += ["-device", "qemu-xhci", "-drive", f"if=none,id=usb,file={img},format=raw,readonly=on",
42                      "-device", "usb-storage,drive=usb,bootindex=0"]
43         self.p = subprocess.Popen(args)
44         for _ in range(150):
45             try:
46                 self.s = socket.create_connection(("127.0.0.1", port)); break
47             except OSError:
48                 time.sleep(0.2)
49         else:
50             sys.exit("FAIL: no QEMU serial socket")
51         self.buf = ""
52 
53     def expect(self, pat, timeout):
54         end = time.time() + timeout
55         while True:
56             m = re.search(pat, self.buf, re.S)
57             if m:
58                 self.buf = self.buf[m.end():]
59                 return m
60             if time.time() > end or self.p.poll() is not None:
61                 self.kill()
62                 sys.exit(f"FAIL: {base}: timeout or QEMU exit waiting for {pat!r}")
63             self.s.settimeout(5)
64             try:
65                 d = self.s.recv(65536)
66             except socket.timeout:
67                 continue
68             if not d:
69                 sys.exit(f"FAIL: {base}: serial closed waiting for {pat!r}")
70             t = d.decode("utf-8", "replace")
71             if "\x1b[6n" in t:  # busybox ash asks for the cursor position
72                 self.s.sendall(b"\x1b[50;200R")
73             LOG.write(t); LOG.flush()
74             self.buf = (self.buf + t)[-200000:]
75 
76     def send(self, line):
77         self.s.sendall((line + "\r").encode())
78 
79     def run(self, script, timeout=600):
80         """Run script with sh and return the output between markers."""
81         self.buf = ""
82         self.send("sh <<'EOS'\recho @@BEG\"\"@@\r" + script.strip().replace("\n", "\r") + "\recho @@END\"\"@@\rEOS")
83         return self.expect(r"@@BEG@@(.*?)@@END@@", timeout).group(1)
84 
85     def kill(self):
86         self.p.kill(); self.p.wait()
87 
88 
89 def login(vm, user, pw):
90     vm.expect(r"login: ", 1800)
91     vm.send(user)
92     vm.expect("Password:", 60)
93     vm.send(pw)
94     time.sleep(10)
95 
96 
97 fails = []
98 def check(name, ok, detail=""):
99     note(f"CHECK {name}: {'ok' if ok else 'FAIL'} {detail}")
100     ok or fails.append(name)
101 
102 
103 if os.path.exists(disk):
104     os.remove(disk)
105 subprocess.run([os.environ.get("QEMU_IMG", "qemu-img"), "create", "-q", "-f", "qcow2", disk, "32G"], check=True)
106 note(f"compositors={comps} encrypt={encrypt} accel={ACCEL} cpu={CPU}")
107 
108 note("1/3 UEFI live boot + unattended install")
109 t0 = time.time()
110 vm = VM(uefi=True, live=True)
111 vm.expect("login:", 900); vm.send("root"); time.sleep(5)
112 answers = (f"BASE={base}\nCOMPOSITOR='{comps}'\nDISK=/dev/vda\nYES=1\nSERIAL=1\nFIRMWARE=no\n"
113            f"FIRMWARE_TAG=20260916\nFIRMWARE_LIST=$D/firmware.example\n"
114            f"ENCRYPT={'yes' if encrypt else 'no'}\nLUKS_PASS={PW['luks']}\nUSERNAME=tester\n"
115            f"USER_PASS={PW['user']}\nROOT_PASS={PW['root']}\n")
116 vm.run(f"cat > /tmp/answers <<'EOA'\n{answers}EOA")
117 vm.send("sh /media/*/waybase/install.sh /tmp/answers; echo INSTALL_RC=$?; sleep 2; poweroff")
118 m = vm.expect(r"INSTALL_RC=(\d+)", 4 * 3600)
119 vm.p.wait(300)
120 note(f"install took {int(time.time() - t0)} s")
121 if int(m.group(1)):
122     sys.exit(f"FAIL: {base}: install.sh rc={m.group(1)}")
123 
124 prompt = "Enter passphrase|Passphrase|passphrase for" if encrypt else "login: "
125 note("2/3 BIOS boot of the installed disk")
126 vm = VM(uefi=False, live=False); vm.expect(prompt, 900); vm.kill()
127 check("BIOS boot reaches " + ("passphrase" if encrypt else "login"), True)
128 
129 note("3/3 UEFI boot of the installed disk + checks")
130 vm = VM(uefi=True, live=False)
131 if encrypt:
132     vm.expect(prompt, 900); time.sleep(2); vm.send(PW["luks"])
133 login(vm, "tester", PW["user"])
134 r = vm.run(r"""
135 . /etc/profile >/dev/null 2>&1
136 nix --version 2>&1 | head -1 | sed 's/^/NIXVER /'
137 nix store info --store daemon >/tmp/ns.log 2>&1 && echo NIXDAEMON_OK || tail -3 /tmp/ns.log
138 for b in foot fuzzel pipewire wireplumber waybase-session; do command -v $b >/dev/null || echo MISSING:$b; done
139 echo FROMNIX $(cat /etc/waybase-from-nix)
140 test -s /lib/firmware/i915/kbl_dmc_ver1_04.bin && echo FW_OK
141 for c in %s; do
142   d=$(mktemp -d); chmod 700 $d
143   XDG_RUNTIME_DIR=$d WLR_BACKENDS=headless WLR_RENDERER=pixman WLR_LIBINPUT_NO_DEVICES=1 $c >/tmp/$c.log 2>&1 &
144   pid=$!; i=0
145   while [ $i -lt 30 ] && ! ls $d | grep -q '^wayland-[0-9]*$'; do sleep 1; i=$((i+1)); done
146   if ls $d | grep -q '^wayland-[0-9]*$'; then echo UP:$c; else echo DOWN:$c; tail -5 /tmp/$c.log; fi
147   kill $pid 2>/dev/null; sleep 1
148 done
149 """ % comps, 900)
150 check("nix --version", "NIXVER nix (Nix)" in r, (re.search(r"NIXVER (.*)", r) or [None, ""])[1].strip())
151 check("nix daemon reachable", "NIXDAEMON_OK" in r)
152 check("pinned firmware installed", "FW_OK" in r)
153 check("session programs", "MISSING:" not in r, " ".join(re.findall(r"MISSING:\S+", r)))
154 note("supplied by Nix: " + (re.search(r"FROMNIX(.*)", r) or [None, ""])[1].strip())
155 for c in comps.split():
156     check(f"{c} starts headless", f"UP:{c}" in r)
157 vm.send("exit"); time.sleep(3)
158 vm.kill()
159 note(f"QEMU TEST {base} " + ("PASSED" if not fails else "FAILED: " + ", ".join(fails)))
160 sys.exit(1 if fails else 0)