nix: Add NixOS-based VMs for live testing in clean desktop env (#7928)

- Separate perSystemOutputs & globalOutputs in flake.nix
  (lots of indent changes!)
- Add NixOS VM configs for Gnome & Plasma testing
- Add section in contrib docs
This commit is contained in:
Benoit de Chezelles 2026-07-13 21:15:04 +02:00 committed by GitHub
parent d9dc7f5130
commit 489972d8c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 497 additions and 213 deletions

1
.gitignore vendored
View File

@ -65,3 +65,4 @@ dhat-heap.json
/assets/flatpak/generated-sources.json
/__pycache__/
/nix/result
*.qcow2

View File

@ -89,6 +89,12 @@ $ run
$ bt
```
Starting WezTerm with `wezterm-gui start --always-new-process` is useful to ensure Mux logs are not
hidden in an background process started in an earlier test.
Start WezTerm with `wezterm-gui --config-file ./test-conf.lua ……` to test a custom config file.
### Please include tests to cover your changes!
This will help ensure that your contributions keep working as things change.
@ -106,6 +112,48 @@ match expectations](https://github.com/wezterm/wezterm/blob/fd532a8c2fb3b5659359
Please also make a point of adding comments to your tests to help
clarify the intent of the test!
### Testing in a NixOS VM
If you need to test WezTerm in a clean desktop environment (e.g. to reproduce a
display server bug or verify a desktop integration), you can use the provided
NixOS VM configurations.
Two desktop variants are available:
- GNOME (`testing-on-gnome`)
- KDE Plasma (`testing-on-plasma`).
**Prerequisites:** Nix is installed, with flakes enabled (`experimental-features = nix-command flakes`).
**Workflow:**
```console
$ cargo build # build wezterm locally
$ nixos-rebuild build-vm --flake ./nix#testing-on-plasma # build the Plasma VM image
$ REPO=$PWD ./result/bin/run-nixos-vm # start the VM
```
> [!NOTE]
> You might need to tweak desktop settings (e.g. the keyboard layout) on first VM start.
>
> The VM state is stored in `nixos.qcow2` in the current working directory on the host.
Inside the VM, open a terminal (e.g. _Console_ on GNOME, _Konsole_ on Plasma), then:
```console
$ cd repo # go into ~/repo
$ ./target/debug/wezterm-gui # run wezterm for your tests
```
The host repository is mounted into the VM at `/home/dev/repo` via a 9p shared directory,
so changes on the host (rebuilds, test config file, ..) are immediately visible inside the VM.
Keep the VM running and make your changes locally, re-build wezterm on your host and kill/start
wezterm again in the VM for your tests.
> [!WARNING]
> The host repo is supposed to be mounted in read-write but I (@bew) don't know why changes in the
> repo are not actually propagated to the host. (PR welcome!)
### Please also include documentation if you are adding or changing behavior
This helps to keep things well-understood and working in the long term.

6
nix/flake.lock generated
View File

@ -71,11 +71,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1780711917,
"narHash": "sha256-Cw9useKmvzRyrzQtghZPi56ImYXKsyrh+vo94hCYM3I=",
"lastModified": 1783816212,
"narHash": "sha256-2aZisVTVGSFEk3MYcOiWQp3zvwyzbqpktB69Rcfp150=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f3ce60d5372cbb1490033766bdd4748dc0128e86",
"rev": "3b32825de172d0bc85664f495edb096b10862524",
"type": "github"
},
"original": {

View File

@ -42,222 +42,248 @@
outputs =
inputs@{ self, ... }:
inputs.flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [ (import inputs.rust-overlay) ];
pkgs = import (inputs.nixpkgs) { inherit system overlays; };
let
perSystemOutputs = inputs.flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [ (import inputs.rust-overlay) ];
pkgs = import inputs.nixpkgs { inherit system overlays; };
inherit (inputs.nixpkgs) lib;
inherit (pkgs) stdenv;
inherit (inputs.nixpkgs) lib;
inherit (pkgs) stdenv;
nativeBuildInputs =
with pkgs;
[
installShellFiles
ncurses # tic for terminfo
pkg-config
python3
]
++ lib.optional stdenv.isDarwin perl;
buildInputs =
with pkgs;
[
fontconfig
openssl
zlib
]
++ lib.optionals stdenv.isLinux [
libxkbcommon
wayland
libx11
libxcb
libxcb-util
libxcb-image
libxcb-keysyms
libxcb-wm # contains xcb-ewmh among others
]
++ lib.optionals stdenv.isDarwin ([
libiconv
]);
libPath = lib.makeLibraryPath (
with pkgs;
[
libxcb-image
libGL
vulkan-loader
]
);
rustPlatform = pkgs.makeRustPlatform {
cargo = pkgs.rust-bin.stable.latest.minimal;
rustc = pkgs.rust-bin.stable.latest.minimal;
};
in
{
packages.default = rustPlatform.buildRustPackage (finalAttrs: {
inherit buildInputs nativeBuildInputs;
pname = "wezterm";
src = ./..;
# Rebuild the usual version number of the project from info of commit being built.
# Format: `<date>-<time>-<shorthash>` (Example: `20200608-110940-3fb3a61`)
# note: adds `-dirty` when the build includes uncomitted changes
version = (
builtins.concatStringsSep "-" (
# note: `self.lastModifiedDate` looks like `20240209045744`
# So this match gives list with 2 items: `20240209` (date) & `045744` (time)
builtins.match "(.{8})(.{6})" self.lastModifiedDate
++ [ (builtins.substring 0 8 self.rev or self.dirtyRev) ]
++ lib.lists.optional (self ? dirtyRev) "dirty"
)
);
cargoLock = {
lockFile = ../Cargo.lock;
allowBuiltinFetchGit = true;
};
# Inject collected submodules in-place for Nix build
prePatch = ''
rm -rf deps/freetype/{freetype2,libpng,zlib} deps/harfbuzz/harfbuzz
ln -s ${inputs.freetype2} deps/freetype/freetype2
ln -s ${inputs.libpng} deps/freetype/libpng
ln -s ${inputs.zlib} deps/freetype/zlib
ln -s ${inputs.harfbuzz} deps/harfbuzz/harfbuzz
'';
postPatch = ''
echo ${finalAttrs.version} > .tag
# tests are failing with: Unable to exchange encryption keys
rm -r wezterm-ssh/tests
# hash does not work well with NixOS
substituteInPlace assets/shell-integration/wezterm.sh \
--replace-fail 'hash wezterm 2>/dev/null' 'command type -P wezterm &>/dev/null' \
--replace-fail 'hash base64 2>/dev/null' 'command type -P base64 &>/dev/null' \
--replace-fail 'hash hostname 2>/dev/null' 'command type -P hostname &>/dev/null' \
--replace-fail 'hash hostnamectl 2>/dev/null' 'command type -P hostnamectl &>/dev/null'
'';
# Disable cargo-auditable until https://github.com/rust-secure-code/cargo-auditable/issues/124 is fixed
auditable = false;
preFixup =
lib.optionalString stdenv.isLinux /* bash */ ''
patchelf \
--add-needed "${pkgs.libGL}/lib/libEGL.so.1" \
--add-needed "${pkgs.vulkan-loader}/lib/libvulkan.so.1" \
$out/bin/wezterm-gui
''
+ lib.optionalString stdenv.isDarwin /* bash */ ''
mkdir -p "$out/Applications"
OUT_APP="$out/Applications/WezTerm.app"
cp -r assets/macos/WezTerm.app "$OUT_APP"
rm $OUT_APP/*.dylib
cp -r assets/shell-integration/* "$OUT_APP/Contents/Resources"
# macOS will only recognize our application bundle
# if the binaries are inside of it. Move them there
# and create symbolic links for them in bin/.
mkdir -p "$OUT_APP/Contents/MacOS"
mv $out/bin/{wezterm,wezterm-mux-server,wezterm-gui,strip-ansi-escapes} "$OUT_APP/Contents/MacOS"
ln -s "$OUT_APP/Contents/MacOS"/{wezterm,wezterm-mux-server,wezterm-gui,strip-ansi-escapes} "$out/bin"
'';
preBuild = ''
echo "Building Wezterm ${finalAttrs.version}..."
'';
postInstall = ''
mkdir -p $out/nix-support
echo "${finalAttrs.passthru.terminfo}" >> $out/nix-support/propagated-user-env-packages
install -Dm644 assets/icon/terminal.png $out/share/icons/hicolor/128x128/apps/org.wezfurlong.wezterm.png
install -Dm644 assets/wezterm.desktop $out/share/applications/org.wezfurlong.wezterm.desktop
install -Dm644 assets/wezterm.appdata.xml $out/share/metainfo/org.wezfurlong.wezterm.appdata.xml
install -Dm644 assets/shell-integration/wezterm.sh -t $out/etc/profile.d
installShellCompletion --cmd wezterm \
--bash assets/shell-completion/bash \
--fish assets/shell-completion/fish \
--zsh assets/shell-completion/zsh
install -Dm644 assets/wezterm-nautilus.py -t $out/share/nautilus-python/extensions
'';
passthru = {
# the headless variant is useful when deploying wezterm's mux server on remote severs
headless = rustPlatform.buildRustPackage {
pname = "wezterm-headless";
inherit (finalAttrs)
version
src
postPatch
cargoLock
meta
;
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.openssl ];
cargoBuildFlags = [
"--package"
"wezterm"
"--package"
"wezterm-mux-server"
];
doCheck = false;
postInstall = ''
install -Dm644 assets/shell-integration/wezterm.sh -t $out/etc/profile.d
install -Dm644 ${finalAttrs.passthru.terminfo}/share/terminfo/w/wezterm -t $out/share/terminfo/w
'';
};
terminfo =
pkgs.runCommand "wezterm-terminfo-${finalAttrs.version}"
{
nativeBuildInputs = [ pkgs.ncurses ];
}
''
mkdir -p $out/share/terminfo $out/nix-support
tic -x -o $out/share/terminfo ${finalAttrs.src}/termwiz/data/wezterm.terminfo
'';
};
meta.mainProgram = "wezterm";
});
devShell = pkgs.mkShell {
name = "wezterm-shell";
inherit nativeBuildInputs;
nativeBuildInputs =
with pkgs;
[
installShellFiles
ncurses # tic for terminfo
pkg-config
python3
]
++ lib.optional stdenv.isDarwin perl;
buildInputs =
buildInputs
++ (with pkgs.rust-bin; [
(stable.latest.minimal.override {
extensions = [
"clippy"
"rust-src"
];
})
with pkgs;
[
fontconfig
openssl
zlib
]
++ lib.optionals stdenv.isLinux [
libxkbcommon
wayland
nightly.latest.rustfmt
nightly.latest.rust-analyzer
libx11
libxcb
libxcb-util
libxcb-image
libxcb-keysyms
libxcb-wm # contains xcb-ewmh among others
]
++ lib.optionals stdenv.isDarwin ([
libiconv
]);
LD_LIBRARY_PATH = libPath;
};
libPath = lib.makeLibraryPath (
with pkgs;
[
libxcb-image
libGL
vulkan-loader
]
);
formatter = pkgs.nixfmt-rfc-style;
}
);
rustPlatform = pkgs.makeRustPlatform {
cargo = pkgs.rust-bin.stable.latest.minimal;
rustc = pkgs.rust-bin.stable.latest.minimal;
};
in
{
packages.default = rustPlatform.buildRustPackage (finalAttrs: {
inherit buildInputs nativeBuildInputs;
pname = "wezterm";
src = ./..;
# Rebuild the usual version number of the project from info of commit being built.
# Format: `<date>-<time>-<shorthash>` (Example: `20200608-110940-3fb3a61`)
# note: adds `-dirty` when the build includes uncomitted changes
version = (
builtins.concatStringsSep "-" (
# note: `self.lastModifiedDate` looks like `20240209045744`
# So this match gives list with 2 items: `20240209` (date) & `045744` (time)
builtins.match "(.{8})(.{6})" self.lastModifiedDate
++ [ (builtins.substring 0 8 self.rev or self.dirtyRev) ]
++ lib.lists.optional (self ? dirtyRev) "dirty"
)
);
cargoLock = {
lockFile = ../Cargo.lock;
allowBuiltinFetchGit = true;
};
# Inject collected submodules in-place for Nix build
prePatch = ''
rm -rf deps/freetype/{freetype2,libpng,zlib} deps/harfbuzz/harfbuzz
ln -s ${inputs.freetype2} deps/freetype/freetype2
ln -s ${inputs.libpng} deps/freetype/libpng
ln -s ${inputs.zlib} deps/freetype/zlib
ln -s ${inputs.harfbuzz} deps/harfbuzz/harfbuzz
'';
postPatch = ''
echo ${finalAttrs.version} > .tag
# tests are failing with: Unable to exchange encryption keys
rm -r wezterm-ssh/tests
# hash does not work well with NixOS
substituteInPlace assets/shell-integration/wezterm.sh \
--replace-fail 'hash wezterm 2>/dev/null' 'command type -P wezterm &>/dev/null' \
--replace-fail 'hash base64 2>/dev/null' 'command type -P base64 &>/dev/null' \
--replace-fail 'hash hostname 2>/dev/null' 'command type -P hostname &>/dev/null' \
--replace-fail 'hash hostnamectl 2>/dev/null' 'command type -P hostnamectl &>/dev/null'
'';
# Disable cargo-auditable until https://github.com/rust-secure-code/cargo-auditable/issues/124 is fixed
auditable = false;
preFixup =
lib.optionalString stdenv.isLinux /* bash */ ''
patchelf \
--add-needed "${pkgs.libGL}/lib/libEGL.so.1" \
--add-needed "${pkgs.vulkan-loader}/lib/libvulkan.so.1" \
$out/bin/wezterm-gui
''
+ lib.optionalString stdenv.isDarwin /* bash */ ''
mkdir -p "$out/Applications"
OUT_APP="$out/Applications/WezTerm.app"
cp -r assets/macos/WezTerm.app "$OUT_APP"
rm $OUT_APP/*.dylib
cp -r assets/shell-integration/* "$OUT_APP/Contents/Resources"
# macOS will only recognize our application bundle
# if the binaries are inside of it. Move them there
# and create symbolic links for them in bin/.
mkdir -p "$OUT_APP/Contents/MacOS"
mv $out/bin/{wezterm,wezterm-mux-server,wezterm-gui,strip-ansi-escapes} "$OUT_APP/Contents/MacOS"
ln -s "$OUT_APP/Contents/MacOS"/{wezterm,wezterm-mux-server,wezterm-gui,strip-ansi-escapes} "$out/bin"
'';
preBuild = ''
echo "Building Wezterm ${finalAttrs.version}..."
'';
postInstall = ''
mkdir -p $out/nix-support
echo "${finalAttrs.passthru.terminfo}" >> $out/nix-support/propagated-user-env-packages
install -Dm644 assets/icon/terminal.png $out/share/icons/hicolor/128x128/apps/org.wezfurlong.wezterm.png
install -Dm644 assets/wezterm.desktop $out/share/applications/org.wezfurlong.wezterm.desktop
install -Dm644 assets/wezterm.appdata.xml $out/share/metainfo/org.wezfurlong.wezterm.appdata.xml
install -Dm644 assets/shell-integration/wezterm.sh -t $out/etc/profile.d
installShellCompletion --cmd wezterm \
--bash assets/shell-completion/bash \
--fish assets/shell-completion/fish \
--zsh assets/shell-completion/zsh
install -Dm644 assets/wezterm-nautilus.py -t $out/share/nautilus-python/extensions
'';
passthru = {
# the headless variant is useful when deploying wezterm's mux server on remote severs
headless = rustPlatform.buildRustPackage {
pname = "wezterm-headless";
inherit (finalAttrs)
version
src
postPatch
cargoLock
meta
;
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.openssl ];
cargoBuildFlags = [
"--package"
"wezterm"
"--package"
"wezterm-mux-server"
];
doCheck = false;
postInstall = ''
install -Dm644 assets/shell-integration/wezterm.sh -t $out/etc/profile.d
install -Dm644 ${finalAttrs.passthru.terminfo}/share/terminfo/w/wezterm -t $out/share/terminfo/w
'';
};
terminfo =
pkgs.runCommand "wezterm-terminfo-${finalAttrs.version}"
{
nativeBuildInputs = [ pkgs.ncurses ];
}
''
mkdir -p $out/share/terminfo $out/nix-support
tic -x -o $out/share/terminfo ${finalAttrs.src}/termwiz/data/wezterm.terminfo
'';
};
meta.mainProgram = "wezterm";
});
devShell = pkgs.mkShell {
name = "wezterm-shell";
inherit nativeBuildInputs;
buildInputs =
buildInputs
++ (with pkgs.rust-bin; [
(stable.latest.minimal.override {
extensions = [
"clippy"
"rust-src"
];
})
nightly.latest.rustfmt
nightly.latest.rust-analyzer
]);
LD_LIBRARY_PATH = libPath;
};
formatter = pkgs.nixfmt-tree;
}
);
testingSystem = "x86_64-linux";
globalOutputs = {
# 0. Build Wezterm locally: `cargo build`
# 1. Build the VM: `nixos-rebuild build-vm --flake ./nix#testing-on-gnome`
# 2. Start the VM: `REPO=$PWD ./result/bin/run-nixos-vm`
# 3. (In the VM) (optional, 1-time) Adjust your keyboard layout in settings
# 4. (In the VM) Open a terminal 'Console' on Gnome, 'Konsole' on KDE
# 5. (In the VM) `cd ~/repo`
# 6. (In the VM) Run the binary built! ``
#
# FIXME: cannot write to $REPO in the VM...
nixosConfigurations.testing-on-gnome = import ./vm {
inherit (inputs) nixpkgs;
system = testingSystem;
desktopModule = ./vm/desktop-gnome.nix;
};
nixosConfigurations.testing-on-plasma = import ./vm {
inherit (inputs) nixpkgs;
system = testingSystem;
desktopModule = ./vm/desktop-plasma.nix;
};
};
in
perSystemOutputs // globalOutputs;
}

36
nix/vm/base.nix Normal file
View File

@ -0,0 +1,36 @@
{ pkgs, ... }:
{
users.mutableUsers = false;
users.groups.devs = { };
users.users.dev = {
isNormalUser = true;
group = "devs";
extraGroups = [ "wheel" ];
initialPassword = "test";
};
services.displayManager.autoLogin = {
enable = true;
user = "dev";
};
environment.systemPackages = [
pkgs.neovim
pkgs.ripgrep
pkgs.git
];
nix.settings = {
trusted-users = [
"root"
"dev"
];
experimental-features = [
"nix-command"
"flakes"
];
};
documentation.nixos.enable = false;
system.stateVersion = pkgs.lib.trivial.release;
}

16
nix/vm/default.nix Normal file
View File

@ -0,0 +1,16 @@
{
nixpkgs,
system,
desktopModule,
}:
nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./base.nix
./vm-settings.nix
./patch-opengl.nix # 👀
./shared-repo.nix
desktopModule
];
}

58
nix/vm/desktop-gnome.nix Normal file
View File

@ -0,0 +1,58 @@
{ pkgs, ... }:
{
services.displayManager.gdm.enable = true;
services.desktopManager.gnome = {
enable = true;
# The following overrides are necessary to have fractional scaling settings available
# (not just 100%/200%)
# ref: https://discourse.nixos.org/t/how-to-set-fractional-scaling-via-nix-configuration-for-gnome-wayland/56774/4
extraGSettingsOverridePackages = [ pkgs.mutter ];
extraGSettingsOverrides = ''
[org.gnome.mutter]
experimental-features=['scale-monitor-framebuffer', 'xwayland-native-scaling']
'';
};
# Skip adding many Gnome packages (lighter VM!)
# Copied from https://github.com/ghostty-org/ghostty/blob/main/nix/vm/common-gnome.nix
environment.gnome.excludePackages = with pkgs; [
atomix
baobab
cheese
epiphany
evince
file-roller
geary
gnome-backgrounds
gnome-calculator
gnome-calendar
gnome-clocks
gnome-connections
gnome-contacts
gnome-disk-utility
gnome-extension-manager
gnome-logs
gnome-maps
gnome-music
gnome-photos
gnome-software
gnome-system-monitor
gnome-text-editor
gnome-themes-extra
gnome-tour
gnome-user-docs
gnome-weather
hitori
iagno
loupe
orca
seahorse
simple-scan
snapshot
sushi
tali
totem
yelp
];
}

30
nix/vm/desktop-plasma.nix Normal file
View File

@ -0,0 +1,30 @@
{ pkgs, ... }:
{
services.desktopManager.plasma6.enable = true;
services.displayManager.sddm = {
enable = true;
wayland = {
enable = true;
};
};
# Skip some optional Plasma packages
# https://wiki.nixos.org/wiki/KDE#Excluding_applications_from_the_default_install
environment.plasma6.excludePackages = with pkgs.kdePackages; [
aurorae
plasma-browser-integration
plasma-workspace-wallpapers
kwin-x11
ark
elisa
gwenview
okular
kate
khelpcenter
baloo-widgets # baloo information in Dolphin
dolphin-plugins
ffmpegthumbs
krdp
];
}

21
nix/vm/patch-opengl.nix Normal file
View File

@ -0,0 +1,21 @@
{ pkgs, ... }:
{
hardware.graphics = {
enable = true;
extraPackages = [
pkgs.mesa
pkgs.libglvnd # provides libEGL.so.1
];
};
# NOTE: I managed to start the wezterm binary with manual LD patching:
# ```
# find -name 'libEGL.so.1' 2>/dev/null
# LD_LIBRARY_PATH=/nix/store/path/to/libglvnd/lib ./target/debug/wezterm-gui
# ```
# (although I got many error/warning from graphics rendering on start)
# So we patch that in the env directly 👀
environment.sessionVariables = {
LD_LIBRARY_PATH = [ "${pkgs.libglvnd}/lib" ];
};
}

28
nix/vm/shared-repo.nix Normal file
View File

@ -0,0 +1,28 @@
{ ... }:
let
repoTarget = "/home/dev/repo";
in
{
virtualisation.vmVariant = {
virtualisation.sharedDirectories."wezterm-repo" = {
# NOTE: env var needs to be set on VM start!
source = ''"$REPO"'';
target = repoTarget;
};
};
fileSystems.${repoTarget} = {
device = "wezterm-repo";
fsType = "9p";
options = [
# automount to keep boot fast
"x-systemd.automount"
# Read/Write access
"rw"
# noauto/nofail so the guest still boots even if the host path disappears
"noauto"
"nofail"
];
};
}

20
nix/vm/vm-settings.nix Normal file
View File

@ -0,0 +1,20 @@
{ modulesPath, ... }:
{
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
];
services.qemuGuest.enable = true;
virtualisation.vmVariant = {
virtualisation = {
graphics = true;
# Bump machine settings (defaults are misarable, e.g. 1 core)
cores = 4;
memorySize = 6 * 1024;
diskSize = 8192;
};
};
}