aboutsummaryrefslogtreecommitdiffstats
path: root/preview
blob: 944d08262f652d3894bd2cc423e818fa1cb55523 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/env python3
# preview: boot dist/udt in real GRUB under QEMU, before root installs it.
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
# Licensed under the GNU General Public License v2 only.
"""Boot the built theme in GRUB x86_64-efi under QEMU and OVMF.

Usage: preview [--live] [WIDTHxHEIGHT]

Default: screenshot to dist/preview.png, headless. --live opens a QEMU
window instead, to move through the menu and try the console ('c').
The mode defaults to 2560x1080. The menu entries are stand-ins.
"""
import shutil
import socket
import subprocess
import sys
import tempfile
import time
from pathlib import Path

REPO = Path(__file__).resolve().parent
THEME = REPO / "dist" / "udt"
SHOT = REPO / "dist" / "preview.png"
OVMF = Path("/usr/share/OVMF")
MODULES = ("part_gpt part_msdos fat normal configfile all_video efi_gop gfxterm "
           "gfxmenu png jpeg font true").split()
BOOT_WAIT = 12  # ponytail: fixed wait for OVMF + GRUB; raise it if the shot is blank
CFG = """\
insmod all_video
insmod gfxterm
set gfxmode={mode},auto
terminal_output gfxterm
{fonts}
set theme=($root)/boot/grub/themes/udt/theme.txt
set timeout=30
menuentry 'Slackware (preview)' --class slackware --class os {{ true }}
submenu 'Advanced options for Slackware' {{ menuentry 'Recovery' {{ true }} }}
menuentry 'UEFI Firmware Settings' --class efi {{ true }}
"""


def main():
    args = sys.argv[1:]
    live = "--live" in args
    args = [a for a in args if a != "--live"]
    mode = args[0] if args else "2560x1080"
    xres, yres = mode.split("x")
    if not (THEME / "theme.txt").exists():
        sys.exit("preview: dist/udt missing; run ./build first")
    with tempfile.TemporaryDirectory() as tmp:
        tmp = Path(tmp)
        root = tmp / "esp"
        grub = root / "boot/grub"
        (root / "EFI/BOOT").mkdir(parents=True)
        shutil.copytree(THEME, grub / "themes/udt")
        # grub-mkconfig writes one loadfont per .pf2 in the theme dir; do the same.
        fonts = "\n".join(f"loadfont ($root)/boot/grub/themes/udt/{f.name}"
                          for f in sorted(THEME.glob("*.pf2")))
        (grub / "grub.cfg").write_text(CFG.format(mode=mode, fonts=fonts))
        subprocess.run(["grub-mkimage", "-O", "x86_64-efi", "-p", "/boot/grub",
                        "-o", str(root / "EFI/BOOT/BOOTX64.EFI"), *MODULES], check=True)
        vars_fd = tmp / "vars.fd"
        vars_fd.write_bytes((OVMF / "OVMF_VARS.fd").read_bytes())
        mon = tmp / "mon.sock"
        cmd = ["qemu-system-x86_64", "-machine", "q35", "-m", "512",
               "-drive", f"if=pflash,format=raw,readonly=on,file={OVMF / 'OVMF_CODE.fd'}",
               "-drive", f"if=pflash,format=raw,file={vars_fd}",
               "-drive", f"file=fat:rw:{root},format=raw",
               "-vga", "none", "-device", f"VGA,edid=on,xres={xres},yres={yres}"]
        if live:
            return subprocess.run(cmd).returncode
        q = subprocess.Popen(cmd + ["-display", "none", "-monitor", f"unix:{mon},server,nowait"])
        try:
            time.sleep(BOOT_WAIT)
            s = socket.socket(socket.AF_UNIX)
            s.connect(str(mon))
            ppm = tmp / "shot.ppm"
            s.sendall(f"screendump {ppm}\n".encode())
            for _ in range(50):  # the dump is written asynchronously
                if ppm.exists() and ppm.stat().st_size:
                    break
                time.sleep(0.2)
            time.sleep(0.5)
            s.sendall(b"quit\n")
            q.wait(timeout=10)
        finally:
            if q.poll() is None:
                q.kill()
        subprocess.run(["magick", str(ppm), str(SHOT)], check=True)
    print(f"preview: {SHOT}")


if __name__ == "__main__":
    main()