git mirror - github.com/owenewans/holy - branch master
clone: https://src.holypkg.eu/holy/

file tests/hardware.py

#!/usr/bin/env python3
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time


report = Path(os.environ.get("REPORT", "out/hardware.json"))
scope = os.environ.get("HARDWARE_SCOPE", "holy")
result = {"schema": "holy-hardware-1", "scope": scope,
          "release_gate": scope == "holy", "checks": []}


def finish(status, message):
    result["status"] = "pass" if status == 0 else "missing-requirement" if status == 6 else "fail"
    result["message"] = message
    report.parent.mkdir(parents=True, exist_ok=True)
    report.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n")
    print("holy-hardware: " + message, file=sys.stderr if status else sys.stdout)
    raise SystemExit(status)


def probe(name, argv, limit=15):
    start = time.monotonic()
    try:
        completed = subprocess.run(argv, capture_output=True, text=True, errors="replace", timeout=limit)
    except subprocess.TimeoutExpired:
        result["checks"].append({"name": name, "argv": argv, "status": "fail", "reason": "timeout"})
        finish(1, name + " timed out")
    item = {"name": name, "argv": argv, "exit_code": completed.returncode,
            "seconds": round(time.monotonic() - start, 3),
            "stdout": completed.stdout[:16384], "stderr": completed.stderr[:4096]}
    result["checks"].append(item)
    return completed, item


if scope not in ("holy", "host"):
    finish(2, "HARDWARE_SCOPE must be holy or host")
release_file = Path("/etc/os-release")
release_lines = release_file.read_text().splitlines() if release_file.is_file() else []
result["host_os"] = next((line.split("=", 1)[1].strip().strip('"')
                          for line in release_lines
                          if line.startswith("ID=")), "unknown")
if scope == "holy":
    if not Path("/var/lib/holypkg/installed").is_dir():
        finish(6, "installed Holy database required; host probe uses HARDWARE_SCOPE=host")
    manager = shutil.which("holypkg")
    if not manager:
        finish(6, "installed holypkg required")
    state, item = probe("installed-state", [manager, "db", "status", "--root", "/", "--json"])
    if state.returncode:
        item["status"] = "fail"
        finish(6, "installed Holy database unavailable")
    item["status"] = "pass"

required = ("lspci", "vulkaninfo", "vkcube", "glxinfo", "glxgears", "timeout")
missing = [name for name in required if not shutil.which(name)]
if missing:
    finish(6, "required tools missing: " + ", ".join(missing))
if not os.environ.get("DISPLAY"):
    finish(6, "DISPLAY required for OpenGL presentation probe")
render_nodes = sorted(Path("/dev/dri").glob("renderD*"))
if not any(os.access(path, os.R_OK | os.W_OK) for path in render_nodes):
    finish(6, "accessible DRM render node required")
result["render_nodes"] = [str(path) for path in render_nodes]

pci, item = probe("nouveau-kernel", ["lspci", "-nnk", "-d", "10de:"])
gpu_blocks = re.split(r"(?=^[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-7] )", pci.stdout, flags=re.M)
if pci.returncode or not any(("VGA compatible controller" in block or "3D controller" in block)
                             and "Kernel driver in use: nouveau" in block for block in gpu_blocks):
    item["status"] = "fail"
    finish(1, "NVIDIA GPU bound to nouveau not found")
item["status"] = "pass"

vulkan, item = probe("vulkan-nvk", ["vulkaninfo", "--summary"])
devices = re.finditer(r"^GPU(\d+):\s*\n(.*?)(?=^GPU\d+:|\Z)", vulkan.stdout, flags=re.M | re.S)
gpu = next((int(match.group(1)) for match in devices
            if re.search(r"^\s*vendorID\s*=\s*0x10de\s*$", match.group(2), flags=re.M)
            and re.search(r"^\s*driverID\s*=\s*DRIVER_ID_MESA_NVK\s*$", match.group(2), flags=re.M)), None)
if vulkan.returncode or gpu is None:
    item["status"] = "fail"
    finish(1, "Mesa NVK Vulkan device not found")
item["status"] = "pass"
result["vulkan_gpu_index"] = gpu

wsi = "wayland" if os.environ.get("WAYLAND_DISPLAY") else "xlib"
cube, item = probe("vulkan-present", ["vkcube", "--gpu_number", str(gpu), "--wsi", wsi,
                                       "--c", "30", "--width", "320", "--height", "240"], 20)
cube_log = cube.stdout + cube.stderr
if cube.returncode or f"Selected GPU {gpu}:" not in cube_log or "NVK" not in cube_log:
    item["status"] = "fail"
    finish(1, "NVK presentation probe failed")
item["status"] = "pass"

gl, item = probe("opengl-context", ["glxinfo", "-B"])
renderer = re.search(r"^OpenGL renderer string:\s*(.+)$", gl.stdout, flags=re.M)
if gl.returncode or "direct rendering: Yes" not in gl.stdout or "Accelerated: yes" not in gl.stdout or \
        "(0x10de)" not in gl.stdout or not renderer or \
        any(name in renderer.group(1).lower() for name in ("llvmpipe", "softpipe")):
    item["status"] = "fail"
    finish(1, "accelerated NVIDIA OpenGL context not found")
item["status"] = "pass"
result["opengl_renderer"] = renderer.group(1)

gears, item = probe("opengl-present", ["timeout", "8", "glxgears", "-geometry", "320x240"], 12)
frames = re.search(r"(\d+) frames in [\d.]+ seconds = ([\d.]+) FPS", gears.stdout)
if gears.returncode != 124 or not frames or int(frames.group(1)) < 1 or float(frames.group(2)) <= 0:
    item["status"] = "fail"
    finish(1, "OpenGL presentation probe produced no measured frames")
item["status"] = "pass"
result["opengl_frames"] = int(frames.group(1))
result["opengl_fps"] = float(frames.group(2))
finish(0, "Nouveau/NVK and OpenGL presentation probes passed (" + scope + ")")