fix: handle desktop readiness and validate compositor updates
This commit is contained in:
Executable
+45
@@ -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"
|
||||||
+17
-4
@@ -1,5 +1,9 @@
|
|||||||
# Disposable graphical audit. Test credentials/autologin NEVER reach the host.
|
# Disposable graphical audit. Test credentials/autologin NEVER reach the host.
|
||||||
{ pkgs, inputs }:
|
{
|
||||||
|
pkgs,
|
||||||
|
inputs,
|
||||||
|
hostRenderNode ? null,
|
||||||
|
}:
|
||||||
pkgs.testers.runNixOSTest {
|
pkgs.testers.runNixOSTest {
|
||||||
name = "development-desktop";
|
name = "development-desktop";
|
||||||
node.pkgsReadOnly = false;
|
node.pkgsReadOnly = false;
|
||||||
@@ -20,19 +24,28 @@ pkgs.testers.runNixOSTest {
|
|||||||
./workstation.nix
|
./workstation.nix
|
||||||
];
|
];
|
||||||
system.stateVersion = "26.05";
|
system.stateVersion = "26.05";
|
||||||
|
boot.blacklistedKernelModules = [ "floppy" ];
|
||||||
virtualisation = {
|
virtualisation = {
|
||||||
memorySize = 6144;
|
memorySize = 6144;
|
||||||
cores = 4;
|
diskSize = 4096;
|
||||||
|
cores = 8;
|
||||||
resolution = {
|
resolution = {
|
||||||
x = 1920;
|
x = 1920;
|
||||||
y = 1080;
|
y = 1080;
|
||||||
};
|
};
|
||||||
qemu.options = [
|
qemu.options = [
|
||||||
"-vga none"
|
"-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 ];
|
environment.systemPackages = [ pkgs.python3 ];
|
||||||
users.users.dev.hashedPasswordFile = toString (
|
users.users.dev.hashedPasswordFile = toString (
|
||||||
pkgs.runCommand "test-only-password-hash" { nativeBuildInputs = [ pkgs.mkpasswd ]; } ''
|
pkgs.runCommand "test-only-password-hash" { nativeBuildInputs = [ pkgs.mkpasswd ]; } ''
|
||||||
|
|||||||
+40
-17
@@ -2,16 +2,36 @@ import json
|
|||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
|
|
||||||
def user(command):
|
def as_user(command):
|
||||||
return machine.succeed(
|
return "runuser --login dev --command " + shlex.quote(
|
||||||
"runuser -u dev -- env XDG_RUNTIME_DIR=/run/user/1001 "
|
"export XDG_RUNTIME_DIR=/run/user/1001 "
|
||||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus "
|
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus; " + command
|
||||||
"bash -lc " + shlex.quote(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):
|
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):
|
def launch(name, command):
|
||||||
@@ -33,40 +53,43 @@ try:
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
print(machine.succeed("journalctl -b --no-pager _UID=1001"))
|
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")
|
machine.screenshot("startup-failed")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
machine.screenshot("startup")
|
wait_layer("ashell-main-layer")
|
||||||
|
screenshot("startup")
|
||||||
assert session("hyprctl configerrors").strip() in ("", "ok")
|
assert session("hyprctl configerrors").strip() in ("", "ok")
|
||||||
assert "JetBrainsMono" in user("fc-match 'JetBrainsMono Nerd Font'")
|
assert "JetBrainsMono" in user("fc-match 'JetBrainsMono Nerd Font'")
|
||||||
assert "Inter" in user("fc-match Inter")
|
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 "zsh" in user("getent passwd dev")
|
||||||
assert "test-speakers" in session("wpctl status --name")
|
assert "test-speakers" in session("wpctl status --name")
|
||||||
machine.succeed("systemctl is-active systemd-resolved")
|
machine.succeed("systemctl is-active systemd-resolved")
|
||||||
|
|
||||||
launch("terminal", "kitty --title 'Workspace ready' sh -c " + shlex.quote(
|
launch("terminal", "kitty --title 'Workspace ready' sh -c " + shlex.quote(
|
||||||
"printf '\\n WORKSPACE READY\\n\\n'; "
|
"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 '\\n Ctrl-R history | Ctrl-T files | Alt-C directories\\n'; "
|
||||||
"printf ' Super-Space launcher | Super-Enter terminal\\n\\n'; exec zsh -i"
|
"printf ' Super-Space launcher | Super-Enter terminal\\n\\n'; exec zsh -i"
|
||||||
))
|
))
|
||||||
launch("monitor", "kitty --title 'System monitor' -e btop")
|
launch("monitor", "kitty --title 'System monitor' -e btop")
|
||||||
machine.wait_until_succeeds("pgrep -u dev btop")
|
machine.wait_until_succeeds("pgrep -u dev btop")
|
||||||
machine.sleep(5)
|
machine.sleep(5)
|
||||||
machine.screenshot("desktop-100")
|
screenshot("desktop-100")
|
||||||
|
|
||||||
launch("launcher", "anyrun")
|
launch("launcher", "anyrun")
|
||||||
|
wait_layer("anyrun")
|
||||||
machine.sleep(2)
|
machine.sleep(2)
|
||||||
machine.send_chars("kitty")
|
machine.send_chars("kitty")
|
||||||
machine.sleep(2)
|
machine.sleep(2)
|
||||||
machine.screenshot("launcher-100")
|
screenshot("launcher-100")
|
||||||
machine.send_key("esc")
|
machine.send_key("esc")
|
||||||
session("notify-send 'Desktop ready' 'Readable text, working audio and native Wayland services.'")
|
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.sleep(1)
|
||||||
machine.screenshot("notification-100")
|
screenshot("notification-100")
|
||||||
|
|
||||||
monitors = json.loads(session("hyprctl -j monitors"))
|
monitors = json.loads(session("hyprctl -j monitors"))
|
||||||
output = monitors[0]["name"]
|
output = monitors[0]["name"]
|
||||||
@@ -75,13 +98,13 @@ session("hyprctl eval " + shlex.quote(
|
|||||||
))
|
))
|
||||||
machine.sleep(4)
|
machine.sleep(4)
|
||||||
assert json.loads(session("hyprctl -j monitors"))[0]["scale"] == 1.5
|
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.
|
# Exercise real PAM/session locking using the disposable fixture password.
|
||||||
session("loginctl lock-session")
|
session("loginctl lock-session")
|
||||||
machine.wait_until_succeeds("pgrep -u dev hyprlock")
|
machine.wait_until_succeeds("pgrep -u dev hyprlock")
|
||||||
machine.sleep(2)
|
machine.sleep(2)
|
||||||
machine.screenshot("lock-150")
|
screenshot("lock-150")
|
||||||
machine.send_chars("incorrect")
|
machine.send_chars("incorrect")
|
||||||
machine.send_key("ret")
|
machine.send_key("ret")
|
||||||
machine.sleep(3)
|
machine.sleep(3)
|
||||||
@@ -96,7 +119,7 @@ session("hyprctl eval " + shlex.quote(
|
|||||||
))
|
))
|
||||||
launch("audio", "pavucontrol")
|
launch("audio", "pavucontrol")
|
||||||
machine.sleep(3)
|
machine.sleep(3)
|
||||||
machine.screenshot("audio-controls")
|
screenshot("audio-controls")
|
||||||
|
|
||||||
# Preferences must not be a read-only Home Manager symlink.
|
# Preferences must not be a read-only Home Manager symlink.
|
||||||
user("test -w ~/.config/keepassxc/keepassxc.ini && test ! -L ~/.config/keepassxc/keepassxc.ini")
|
user("test -w ~/.config/keepassxc/keepassxc.ini && test ! -L ~/.config/keepassxc/keepassxc.ini")
|
||||||
|
|||||||
+24
-6
@@ -10,12 +10,31 @@ let
|
|||||||
wallpaper = pkgs.runCommand "quiet-orbit.png" { nativeBuildInputs = [ pkgs.resvg ]; } ''
|
wallpaper = pkgs.runCommand "quiet-orbit.png" { nativeBuildInputs = [ pkgs.resvg ]; } ''
|
||||||
resvg ${./wallpaper.svg} "$out"
|
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 {
|
screenshot = pkgs.writeShellApplication {
|
||||||
name = "desktop-screenshot";
|
name = "desktop-screenshot";
|
||||||
runtimeInputs = with pkgs; [
|
runtimeInputs = with pkgs; [
|
||||||
grim
|
grim
|
||||||
slurp
|
slurp
|
||||||
satty
|
satty
|
||||||
|
wl-clipboard
|
||||||
coreutils
|
coreutils
|
||||||
];
|
];
|
||||||
text = ''
|
text = ''
|
||||||
@@ -130,10 +149,8 @@ in
|
|||||||
qt = {
|
qt = {
|
||||||
enable = true;
|
enable = true;
|
||||||
platformTheme.name = "gtk3";
|
platformTheme.name = "gtk3";
|
||||||
style = {
|
# Let Home Manager provide BOTH Qt 5 and Qt 6 style plugins.
|
||||||
name = "adwaita-dark";
|
style.name = "adwaita-dark";
|
||||||
package = pkgs.adwaita-qt;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
dconf.settings."org/gnome/desktop/interface" = {
|
dconf.settings."org/gnome/desktop/interface" = {
|
||||||
color-scheme = "prefer-dark";
|
color-scheme = "prefer-dark";
|
||||||
@@ -152,8 +169,9 @@ in
|
|||||||
services.hyprpolkitagent.enable = true;
|
services.hyprpolkitagent.enable = true;
|
||||||
services.awww.enable = true;
|
services.awww.enable = true;
|
||||||
systemd.user.services.awww.Service = {
|
systemd.user.services.awww.Service = {
|
||||||
Type = "notify"; # awww 0.12 signals socket readiness; no guessed sleep.
|
Type = "notify";
|
||||||
ExecStartPost = "${pkgs.awww}/bin/awww img ${wallpaper} --transition-type fade --transition-duration 0.5";
|
CacheDirectory = "awww";
|
||||||
|
ExecStartPost = "${wallpaperInit}/bin/initialize-wallpaper";
|
||||||
};
|
};
|
||||||
|
|
||||||
programs.ashell = {
|
programs.ashell = {
|
||||||
|
|||||||
@@ -43,6 +43,14 @@
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
desktop = import ./desktop-test.nix { inherit inputs pkgs; };
|
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 =
|
updates =
|
||||||
pkgs.runCommand "update-workflow-check"
|
pkgs.runCommand "update-workflow-check"
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ hl.config({
|
|||||||
touchpad = { natural_scroll = true, tap_to_click = true },
|
touchpad = { natural_scroll = true, tap_to_click = true },
|
||||||
},
|
},
|
||||||
misc = { disable_hyprland_logo = true, force_default_wallpaper = 0 },
|
misc = { disable_hyprland_logo = true, force_default_wallpaper = 0 },
|
||||||
|
ecosystem = { no_update_news = true },
|
||||||
})
|
})
|
||||||
hl.env("XCURSOR_SIZE", "24")
|
hl.env("XCURSOR_SIZE", "24")
|
||||||
hl.env("HYPRCURSOR_SIZE", "24")
|
hl.env("HYPRCURSOR_SIZE", "24")
|
||||||
|
|||||||
+2
-1
@@ -28,7 +28,8 @@ if git diff --quiet -- flake.lock; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
nix flake check --no-build --no-update-lock-file
|
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 add flake.lock
|
||||||
git -c user.name='NixOS Updater' -c user.email='nixos-updater@localhost' \
|
git -c user.name='NixOS Updater' -c user.email='nixos-updater@localhost' \
|
||||||
commit -m 'chore: update stable NixOS and Home Manager inputs'
|
commit -m 'chore: update stable NixOS and Home Manager inputs'
|
||||||
|
|||||||
Reference in New Issue
Block a user