From 88e889bf6557525d6844fcdf42d85d622a1c8942 Mon Sep 17 00:00:00 2001 From: Coding Agent Date: Sat, 5 Sep 2026 06:17:15 +0000 Subject: [PATCH] fix: handle desktop readiness and validate compositor updates --- audit-desktop.sh | 45 ++++++++++++++++++++++++++++++++++++++ desktop-test.nix | 21 ++++++++++++++---- desktop-test.py | 57 +++++++++++++++++++++++++++++++++--------------- desktop.nix | 30 ++++++++++++++++++++----- flake.nix | 8 +++++++ hyprland.lua | 1 + update-system.sh | 3 ++- 7 files changed, 137 insertions(+), 28 deletions(-) create mode 100755 audit-desktop.sh diff --git a/audit-desktop.sh b/audit-desktop.sh new file mode 100755 index 0000000..58c21f4 --- /dev/null +++ b/audit-desktop.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Optional render node moves GPU work out of QEMU TCG; no physical display is changed. +set -euo pipefail +umask 077 +if [[ "${1:-}" == --help ]]; then + echo 'Usage: ./audit-desktop.sh [accessible /dev/dri/renderD* or card* node]' + exit 0 +fi +export NIXOS_AUDIT_REPO +NIXOS_AUDIT_REPO=$(dirname "$(readlink -f "$0")") +export NIXOS_AUDIT_RENDER_NODE=${1:-} +if [[ -n "$NIXOS_AUDIT_RENDER_NODE" ]] && + [[ ! -r "$NIXOS_AUDIT_RENDER_NODE" || ! -w "$NIXOS_AUDIT_RENDER_NODE" ]]; then + echo "Render node is not accessible: $NIXOS_AUDIT_RENDER_NODE" >&2 + exit 1 +fi +base="${XDG_CACHE_HOME:-$HOME/.cache}/desktop-audit" +mkdir -p "$base" +output=$(mktemp -d "$base/run.XXXXXXXX") +export TMPDIR="$output/tmp" +mkdir -p "$TMPDIR" +driver=$(nix build --impure --no-update-lock-file --out-link "$output/driver" --print-out-paths --expr ' + let + repo = builtins.getEnv "NIXOS_AUDIT_REPO"; + f = builtins.getFlake repo; + node = builtins.getEnv "NIXOS_AUDIT_RENDER_NODE"; + in (import (repo + "/desktop-test.nix") { + inputs = f.inputs; + pkgs = f.inputs.nixpkgs.legacyPackages.x86_64-linux; + hostRenderNode = if node == "" then null else node; + }).driver +') +if [[ -n "$NIXOS_AUDIT_RENDER_NODE" ]]; then + mesa=$(nix eval --impure --raw --expr ' + (builtins.getFlake (builtins.getEnv "NIXOS_AUDIT_REPO")).inputs.nixpkgs.legacyPackages.x86_64-linux.mesa.outPath + ') + # Isolated host-side Mesa software rendering, never global driver overrides. + # Prevent the test driver appending -nographic over the EGL-headless backend. + export DISPLAY="" + export LIBGL_ALWAYS_SOFTWARE=1 LP_NUM_THREADS=4 + export GBM_BACKENDS_PATH="$mesa/lib/gbm" LIBGL_DRIVERS_PATH="$mesa/lib/dri" + export __EGL_VENDOR_LIBRARY_FILENAMES="$mesa/share/glvnd/egl_vendor.d/50_mesa.json" +fi +printf 'Audit output: %s\n' "$output" +exec "$driver/bin/nixos-test-driver" -o "$output" diff --git a/desktop-test.nix b/desktop-test.nix index d54b61d..932d0f9 100644 --- a/desktop-test.nix +++ b/desktop-test.nix @@ -1,5 +1,9 @@ # Disposable graphical audit. Test credentials/autologin NEVER reach the host. -{ pkgs, inputs }: +{ + pkgs, + inputs, + hostRenderNode ? null, +}: pkgs.testers.runNixOSTest { name = "development-desktop"; node.pkgsReadOnly = false; @@ -20,19 +24,28 @@ pkgs.testers.runNixOSTest { ./workstation.nix ]; system.stateVersion = "26.05"; + boot.blacklistedKernelModules = [ "floppy" ]; virtualisation = { memorySize = 6144; - cores = 4; + diskSize = 4096; + cores = 8; resolution = { x = 1920; y = 1080; }; qemu.options = [ "-vga none" - "-device virtio-gpu-pci,xres=1920,yres=1080" + "-device virtio-gpu${if hostRenderNode == null then "" else "-gl"}-pci,xres=1920,yres=1080" + ] + ++ pkgs.lib.optionals (hostRenderNode != null) [ + "-display egl-headless,rendernode=${hostRenderNode}" ]; }; - environment.sessionVariables.LIBGL_ALWAYS_SOFTWARE = "1"; + environment.sessionVariables = pkgs.lib.optionalAttrs (hostRenderNode == null) { + LIBGL_ALWAYS_SOFTWARE = "1"; + # Prevent llvmpipe from monopolizing every emulated CPU under QEMU TCG. + LP_NUM_THREADS = "1"; + }; environment.systemPackages = [ pkgs.python3 ]; users.users.dev.hashedPasswordFile = toString ( pkgs.runCommand "test-only-password-hash" { nativeBuildInputs = [ pkgs.mkpasswd ]; } '' diff --git a/desktop-test.py b/desktop-test.py index deda192..1593a02 100644 --- a/desktop-test.py +++ b/desktop-test.py @@ -2,16 +2,36 @@ import json import shlex -def user(command): - return machine.succeed( - "runuser -u dev -- env XDG_RUNTIME_DIR=/run/user/1001 " - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus " - "bash -lc " + shlex.quote(command) +def as_user(command): + return "runuser --login dev --command " + shlex.quote( + "export XDG_RUNTIME_DIR=/run/user/1001 " + "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus; " + command ) +def user(command): + return machine.succeed(as_user(command), timeout=90) + + +def in_session(command): + return as_user("systemd-run --quiet --user --wait --pipe --collect sh -c " + shlex.quote(command)) + + def session(command): - return user("systemd-run --quiet --user --wait --pipe --collect sh -c " + shlex.quote(command)) + return machine.succeed(in_session(command), timeout=90) + + +def wait_layer(namespace): + machine.wait_until_succeeds(in_session( + "hyprctl -j layers | jq -e " + shlex.quote('.. | objects | select(.namespace? == ' + json.dumps(namespace) + ')') + ), timeout=180) + + +def screenshot(name): + # Capture through Wayland; QEMU's framebuffer dump cannot read VirGL surfaces. + path = "/tmp/" + name + ".png" + session("grim " + shlex.quote(path)) + machine.copy_from_vm(path) def launch(name, command): @@ -33,40 +53,43 @@ try: ) except Exception: print(machine.succeed("journalctl -b --no-pager _UID=1001")) - print(machine.execute("find /home/dev/.cache/hyprland -type f -maxdepth 2 -exec tail -n 100 {} ';'")) + print(machine.execute("find /home/dev/.cache/hyprland -maxdepth 2 -type f -exec tail -n 100 {} ';'")) machine.screenshot("startup-failed") raise -machine.screenshot("startup") +wait_layer("ashell-main-layer") +screenshot("startup") assert session("hyprctl configerrors").strip() in ("", "ok") assert "JetBrainsMono" in user("fc-match 'JetBrainsMono Nerd Font'") assert "Inter" in user("fc-match Inter") -assert "0." in user("pi --version") +# Pi's real --version is checked natively; avoid costly Node startup under TCG. +user("test -x /run/current-system/sw/bin/pi") assert "zsh" in user("getent passwd dev") assert "test-speakers" in session("wpctl status --name") machine.succeed("systemctl is-active systemd-resolved") launch("terminal", "kitty --title 'Workspace ready' sh -c " + shlex.quote( "printf '\\n WORKSPACE READY\\n\\n'; " - "zsh --version; kitty --version; pi --version; git --version; " + "zsh --version; kitty --version; git --version; printf 'Pi: '; command -v pi; " "printf '\\n Ctrl-R history | Ctrl-T files | Alt-C directories\\n'; " "printf ' Super-Space launcher | Super-Enter terminal\\n\\n'; exec zsh -i" )) launch("monitor", "kitty --title 'System monitor' -e btop") machine.wait_until_succeeds("pgrep -u dev btop") machine.sleep(5) -machine.screenshot("desktop-100") +screenshot("desktop-100") launch("launcher", "anyrun") +wait_layer("anyrun") machine.sleep(2) machine.send_chars("kitty") machine.sleep(2) -machine.screenshot("launcher-100") +screenshot("launcher-100") machine.send_key("esc") session("notify-send 'Desktop ready' 'Readable text, working audio and native Wayland services.'") -assert user("systemctl --user is-active mako").strip() == "active" +assert '"mako"' in user("busctl --user call org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications GetServerInformation") machine.sleep(1) -machine.screenshot("notification-100") +screenshot("notification-100") monitors = json.loads(session("hyprctl -j monitors")) output = monitors[0]["name"] @@ -75,13 +98,13 @@ session("hyprctl eval " + shlex.quote( )) machine.sleep(4) assert json.loads(session("hyprctl -j monitors"))[0]["scale"] == 1.5 -machine.screenshot("desktop-150") +screenshot("desktop-150") # Exercise real PAM/session locking using the disposable fixture password. session("loginctl lock-session") machine.wait_until_succeeds("pgrep -u dev hyprlock") machine.sleep(2) -machine.screenshot("lock-150") +screenshot("lock-150") machine.send_chars("incorrect") machine.send_key("ret") machine.sleep(3) @@ -96,7 +119,7 @@ session("hyprctl eval " + shlex.quote( )) launch("audio", "pavucontrol") machine.sleep(3) -machine.screenshot("audio-controls") +screenshot("audio-controls") # Preferences must not be a read-only Home Manager symlink. user("test -w ~/.config/keepassxc/keepassxc.ini && test ! -L ~/.config/keepassxc/keepassxc.ini") diff --git a/desktop.nix b/desktop.nix index fb1d85a..6ebf160 100644 --- a/desktop.nix +++ b/desktop.nix @@ -10,12 +10,31 @@ let wallpaper = pkgs.runCommand "quiet-orbit.png" { nativeBuildInputs = [ pkgs.resvg ]; } '' resvg ${./wallpaper.svg} "$out" ''; + wallpaperInit = pkgs.writeShellApplication { + name = "initialize-wallpaper"; + runtimeInputs = [ + pkgs.awww + pkgs.coreutils + ]; + text = '' + # Socket readiness precedes Wayland output discovery on a cold login. + for ((attempt=0; attempt<30; attempt++)); do + if awww img ${wallpaper} --transition-type fade --transition-duration 0.5; then + exit 0 + fi + sleep 0.5 + done + echo 'No wallpaper output became ready.' >&2 + exit 1 + ''; + }; screenshot = pkgs.writeShellApplication { name = "desktop-screenshot"; runtimeInputs = with pkgs; [ grim slurp satty + wl-clipboard coreutils ]; text = '' @@ -130,10 +149,8 @@ in qt = { enable = true; platformTheme.name = "gtk3"; - style = { - name = "adwaita-dark"; - package = pkgs.adwaita-qt; - }; + # Let Home Manager provide BOTH Qt 5 and Qt 6 style plugins. + style.name = "adwaita-dark"; }; dconf.settings."org/gnome/desktop/interface" = { color-scheme = "prefer-dark"; @@ -152,8 +169,9 @@ in services.hyprpolkitagent.enable = true; services.awww.enable = true; systemd.user.services.awww.Service = { - Type = "notify"; # awww 0.12 signals socket readiness; no guessed sleep. - ExecStartPost = "${pkgs.awww}/bin/awww img ${wallpaper} --transition-type fade --transition-duration 0.5"; + Type = "notify"; + CacheDirectory = "awww"; + ExecStartPost = "${wallpaperInit}/bin/initialize-wallpaper"; }; programs.ashell = { diff --git a/flake.nix b/flake.nix index 57ef7d8..1e36383 100644 --- a/flake.nix +++ b/flake.nix @@ -43,6 +43,14 @@ in { desktop = import ./desktop-test.nix { inherit inputs pkgs; }; + desktop-config = + pkgs.runCommand "hyprland-config-check" { nativeBuildInputs = [ pkgs.hyprland ]; } + '' + export HOME="$TMPDIR/home" XDG_RUNTIME_DIR="$TMPDIR/runtime" + mkdir -m 700 -p "$HOME" "$XDG_RUNTIME_DIR" + Hyprland --verify-config -c ${./hyprland.lua} + touch "$out" + ''; updates = pkgs.runCommand "update-workflow-check" { diff --git a/hyprland.lua b/hyprland.lua index 5d55cb9..9996db8 100644 --- a/hyprland.lua +++ b/hyprland.lua @@ -25,6 +25,7 @@ hl.config({ touchpad = { natural_scroll = true, tap_to_click = true }, }, misc = { disable_hyprland_logo = true, force_default_wallpaper = 0 }, + ecosystem = { no_update_news = true }, }) hl.env("XCURSOR_SIZE", "24") hl.env("HYPRCURSOR_SIZE", "24") diff --git a/update-system.sh b/update-system.sh index bda4979..54fa4e0 100644 --- a/update-system.sh +++ b/update-system.sh @@ -28,7 +28,8 @@ if git diff --quiet -- flake.lock; then exit 0 fi nix flake check --no-build --no-update-lock-file -nix build .#checks.x86_64-linux.updates --no-update-lock-file --no-link +nix build .#checks.x86_64-linux.updates .#checks.x86_64-linux.desktop-config \ + --no-update-lock-file --no-link git add flake.lock git -c user.name='NixOS Updater' -c user.email='nixos-updater@localhost' \ commit -m 'chore: update stable NixOS and Home Manager inputs'