9.9 KiB
Testing vuinputd on NixOS with Incus
This guide walks through setting up a reproducible NixOS test environment for vuinputd
using Incus, an open-source system container and VM manager.
Complete Script
If you are already familiar with Incus and NixOS, here is the full sequence at a glance. The sections below explain each step in detail.
# --- Setup ---
# Download NixOS VM image (only needed once)
incus image copy images:nixos/25.11 local: --alias nixos/25.11 --vm
# Create and start the VM
incus launch local:nixos/25.11 nixos-vm --vm
# Adjust resources (requires a stop/start cycle)
incus stop nixos-vm
incus config set nixos-vm limits.memory 3GiB
incus config set nixos-vm security.secureboot false
incus start nixos-vm
# --- Configuration ---
# Extend the NixOS configuration to include the vuinputd test module
incus exec nixos-vm -- sed -i '/imports = \[/a\ ./vuinputd-test-automation.nix' \
/etc/nixos/configuration.nix
# Push the test module into the VM
incus file push vuinputd-test-automation.nix nixos-vm/etc/nixos/vuinputd-test-automation.nix
# Apply the configuration (takes a few minutes on first run)
incus exec nixos-vm -- nixos-rebuild switch --max-jobs 1
# --- Run the test ---
incus exec nixos-vm -- bwrap \
--unshare-net \
--ro-bind / / \
--tmpfs /tmp \
--tmpfs /run/udev \
--dev-bind /run/vuinputd/vuinput/dev-input /dev/input \
--dev-bind /dev/vuinput /dev/uinput \
/run/current-system/sw/bin/test-scenarios basic-keyboard
Why Incus?
Testing vuinputd requires a full Linux system stack: a running kernel, udev, /dev/uinput,
CUSE support, and a container runtime. A plain Docker container or unit test harness is not
sufficient because many of these subsystems only exist in a fully booted environment.
Incus is used here for several reasons:
- Full VM support: Incus can launch proper virtual machines (not just containers), which
gives each test environment its own kernel, udev tree, and device namespace — exactly what
vuinputdneeds to operate. - Clean image lifecycle: Incus pulls pre-built NixOS images from the Linux Containers image server, so there is no need to build a NixOS ISO or maintain a local image manually.
- Easy configuration injection:
incus file pushandincus execallow pushing NixOS configuration files into the VM and triggering anixos-rebuild switchwithout requiring SSH or manual setup. - Reproducibility: Each test run can start from a fresh image, ensuring that leftover state from a previous run does not affect results.
- Isolation from the host: The VM is fully isolated from the host system, so test runs cannot accidentally interfere with the host's input devices or udev state.
Note: While
vuinputdis compatible with other container runtimes such assystemd-nspawn,Docker,LXC, andPodman, Incus VMs are the recommended environment for automated NixOS testing because they provide a fully booted NixOS system with minimal setup effort.
Prerequisites
- Incus installed and initialized on the host (
incus admin initcompleted). - The
vuinputd-test-automation.nixNixOS module available in your working directory. This module configuresvuinputdand the test tooling inside the VM. - Sufficient disk space for the NixOS VM image (roughly 3–4 GiB).
Step-by-Step Guide
1. Download the NixOS VM Image
incus image copy images:nixos/25.11 local: --alias nixos/25.11 --vm
This pulls the NixOS 25.11 image from the public Linux Containers image server and stores it
locally under the alias nixos/25.11. The --vm flag ensures the image is treated as a
full virtual machine image rather than a system container rootfs.
2. Launch the VM
incus launch local:nixos/25.11 nixos-vm --vm
This creates and starts a new VM instance named nixos-vm from the downloaded image.
At this point the VM boots NixOS with its default configuration.
3. Adjust VM Resources
Stop the VM briefly to apply resource limits before running the NixOS rebuild, which is memory-intensive:
incus stop nixos-vm
incus config set nixos-vm limits.memory 3GiB
NixOS system builds (nixos-rebuild switch) evaluate a large Nix expression tree and compile
Rust code. Without sufficient memory, the build may be killed by the OOM reaper.
Secure Boot must also be disabled because the NixOS kernel modules required by vuinputd
(CUSE/FUSE) are not signed for Secure Boot by default:
incus config set nixos-vm security.secureboot false
Then restart the VM:
incus start nixos-vm
4. Inject the Test Configuration
The NixOS configuration inside the VM needs to be extended to include the vuinputd test
module. Commands are run inside the VM via incus exec. The -- separator tells Incus where
its own arguments end and where the command to execute inside the VM begins — everything after
-- is passed verbatim to the VM shell.
First, append the import to the existing configuration.nix:
incus exec nixos-vm -- sed -i '/imports = \[/a\ ./vuinputd-test-automation.nix' \
/etc/nixos/configuration.nix
This uses sed to insert ./vuinputd-test-automation.nix immediately after the imports = [
line in the NixOS configuration, so NixOS will pick it up during the next rebuild.
Next, push the test module file into the VM:
incus file push vuinputd-test-automation.nix nixos-vm/etc/nixos/vuinputd-test-automation.nix
The vuinputd-test-automation.nix module is responsible for:
- Installing and enabling
vuinputdas a systemd service. - Providing any additional packages needed by the test scenarios (e.g.,
bwrap,test-scenarios). - Configuring udev rules for device isolation.
5. Apply the NixOS Configuration
Trigger a NixOS system rebuild inside the VM:
incus exec nixos-vm -- nixos-rebuild switch --max-jobs 1
The --max-jobs 1 flag limits parallel build jobs to avoid exhausting VM memory during
compilation. This step may take several minutes on first run because Nix will fetch and
build all required dependencies.
After the rebuild completes, the VM is running a fully configured NixOS system with
vuinputd installed and active.
6. Run the Test Scenarios
Execute a test scenario inside a restricted sandbox using bwrap (Bubblewrap).
Bubblewrap was chosen as the sandboxing tool for several reasons:
- Minimal and universally available:
bwrapis a small, single binary with no daemon and no runtime dependencies beyond the kernel. It is packaged in virtually every Linux distribution, so there is no need to install a full container runtime just to run tests. - Reuses host binaries directly: Because
bwrapcan bind-mount the host (or VM) filesystem read-only into the sandbox, the test binary and all its dependencies are taken straight from the running NixOS system — no separate rootfs or image needs to be prepared. - Good proxy for heavier runtimes: The namespace isolation that
bwrapprovides (mount, network, udev) is the same fundamental mechanism used by Docker, Podman, and systemd-nspawn. Ifvuinputdworks correctly inside abwrapsandbox, the same behavior can be expected from more heavyweight container runtimes, makingbwrapa lightweight but representative stand-in for integration testing.
incus exec nixos-vm -- bwrap \
--unshare-net \
--ro-bind / / \
--tmpfs /tmp \
--tmpfs /run/udev \
--dev-bind /run/vuinputd/vuinput/dev-input /dev/input \
--dev-bind /dev/vuinput /dev/uinput \
/run/current-system/sw/bin/test-scenarios basic-keyboard
What this command does
| Flag | Purpose |
|---|---|
--unshare-net |
Removes network access from the sandbox, ensuring the test is self-contained. |
--ro-bind / / |
Mounts the entire VM filesystem read-only as the sandbox root. |
--tmpfs /tmp |
Provides a writable temporary directory. |
--tmpfs /run/udev |
Provides a writable udev runtime directory, isolating the sandbox from the VM's real udev socket. |
--dev-bind /run/vuinputd/vuinput/dev-input /dev/input |
Exposes the input devices managed by vuinputd (the container-scoped /dev/input subtree) at the expected path inside the sandbox. |
--dev-bind /dev/vuinput /dev/uinput |
Exposes the CUSE-backed virtual /dev/uinput device provided by vuinputd at the standard /dev/uinput path inside the sandbox. |
This sandbox mimics what a containerized application would see: it has access to the virtual
/dev/uinput provided by vuinputd and the corresponding /dev/input event devices, but
cannot reach the host's real uinput or other system resources.
The final argument basic-keyboard selects which test scenario to run. Additional scenarios
may be available; refer to the test-scenarios binary's help output for the full list.
Cleaning Up
To delete the VM and free disk space after testing:
incus delete --force nixos-vm
To also remove the cached image:
incus image delete nixos/25.11
Troubleshooting
If the test fails or vuinputd does not appear to be running inside the VM, refer to
docs/DEBUG.md for debugging
strategies applicable to container environments.
Common issues:
- CUSE not available: Ensure that the
cusekernel module is loaded inside the VM (modprobe cuse). The test automation module should handle this automatically. nixos-rebuildruns out of memory: Increase the memory limit above 3 GiB or reduce parallelism further with--max-jobs 1 --cores 1.- Secure Boot blocking the kernel module: Verify that
security.securebootis set tofalseon the VM instance. /dev/vuinputnot present: Check that thevuinputdsystemd service is active (systemctl status vuinputd) and that CUSE is loaded.