Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ Testing your project on different Linux distributions is essential, but time-con
* Debian
* Fedora
* Rocky
* Arch Linux

And run your tests using a single CLI command.

## Overview

This project builds on the [NixOS VM test](https://nixos.org/manual/nixos/stable/#sec-nixos-tests) infrastructure to allow you to test your software instantly on Ubuntu, Debian, Fedora, and Rocky virtual machines.
This project builds on the [NixOS VM test](https://nixos.org/manual/nixos/stable/#sec-nixos-tests) infrastructure to allow you to test your software instantly on Ubuntu, Debian, Fedora, Rocky, and Arch Linux virtual machines.

It runs on any Linux machine with Nix installed.

Expand Down
114 changes: 114 additions & 0 deletions archlinux/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{ generic, pkgs, lib, system }:
let
imagesJSON = lib.importJSON ./images.json;
fetchImage = image: pkgs.fetchurl {
inherit (image) hash;
url = image.url;
};
images = lib.mapAttrs (k: v: fetchImage v) (imagesJSON.${system} or {});
makeVmTestForImage = imageID: image: { testScript, sharedDirs ? {}, diskSize ? null, extraPathsToRegister ? [ ] }: generic.makeVmTest {
name = "vm-test-archlinux_${imageID}";
inherit system testScript sharedDirs;
image = prepareArchlinuxImage {
inherit diskSize extraPathsToRegister;
hostPkgs = pkgs;
originalImage = image;
};
};

# Arch basic image: GPT with BIOS boot + EFI + btrfs root on partition 3.
resizeService = pkgs.writeText "resizeService" ''
[Service]
Type = oneshot
ExecStart = /bin/sh -euc 'sfdisk --relocate=gpt-bak-std /dev/sda; echo ",+" | sfdisk --no-reread --force -N 3 /dev/sda; partx -u /dev/sda; btrfs filesystem resize max /'

[Install]
WantedBy = multi-user.target
'';

prepareArchlinuxImage = { hostPkgs, originalImage, diskSize, extraPathsToRegister }:
let
pkgs = hostPkgs;
resultImg = "./image.qcow2";
in
pkgs.runCommand "${originalImage.name}-nix-vm-test.qcow2" { } ''
install -m777 ${originalImage} ${resultImg}

cp ${generic.backdoor { scriptPath = "/usr/bin/backdoorScript"; }} backdoor.service
cp ${generic.mountStore { pathsToRegister = extraPathsToRegister; }} mount-store.service
cp ${resizeService} resizeguest.service
cp ${generic.backdoorScript} backdoorScript

# Patching the patched shebang to a reasonable path: /bin/bash.
sed -i 's/\/nix\/store\/.*/\/bin\/bash/g' backdoorScript

${lib.optionalString (diskSize != null) ''
export PATH="${pkgs.qemu}/bin:$PATH"
qemu-img resize ${resultImg} ${diskSize}
''}

${lib.concatStringsSep " \\\n" [
"${pkgs.guestfs-tools}/bin/virt-customize"
"-a ${resultImg}"
"--smp 2"
"--memsize 256"
"--no-network"
"--copy-in backdoorScript:/usr/bin"
"--copy-in backdoor.service:/etc/systemd/system"
"--copy-in mount-store.service:/etc/systemd/system"
"--copy-in resizeguest.service:/etc/systemd/system"
"--run"
(pkgs.writeShellScript "run-script" ''
passwd -d root

groupadd nixbld

# Don't spawn ttys on these devices, they are used for test instrumentation
systemctl mask serial-getty@ttyS0.service
systemctl mask serial-getty@hvc0.service

# We have no reliable network in the test VMs
systemctl mask sshd.service
systemctl mask sshd.socket

# arch-boxes enables systemd-time-wait-sync which blocks
# time-sync.target -> multi-user.target forever when NTP is unreachable.
systemctl mask systemd-time-wait-sync.service

# arch-boxes also enables a pacman-init and keyring-sync pair that
# need the network to run first-boot key initialization.
rm -f /etc/systemd/system/pacman-init.service
systemctl mask pacman-init.service
systemctl mask archlinux-keyring-wkd-sync.service
systemctl mask archlinux-keyring-wkd-sync.timer

# Skip waiting for the network to be "online"
systemctl mask systemd-networkd-wait-online.service

# arch-boxes installs GRUB; systemd-boot-update is pointless
systemctl mask systemd-boot-update.service

# Drop GRUB's interactive timeout so the VM doesn't wait at the menu,
# and route the kernel console to ttyS0 so systemd stage 2 is visible
# on the same serial line the test driver reads.
if [ -f /boot/grub/grub.cfg ]; then
sed -i 's/^set timeout=.*/set timeout=0/' /boot/grub/grub.cfg
sed -i 's|\(linux\s\+/boot/vmlinuz-linux[^\n]*\)|\1 console=tty0 console=ttyS0|' /boot/grub/grub.cfg
fi
if [ -f /etc/default/grub ]; then
sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/' /etc/default/grub
sed -i 's|^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"|GRUB_CMDLINE_LINUX_DEFAULT="\1 console=tty0 console=ttyS0"|' /etc/default/grub
fi

${lib.optionalString (diskSize != null) ''
systemctl enable resizeguest.service
''}
systemctl enable backdoor.service
'')
]};

cp ${resultImg} $out
'';
in {
inherit images prepareArchlinuxImage;
} // lib.mapAttrs makeVmTestForImage images
9 changes: 9 additions & 0 deletions archlinux/images.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"x86_64-linux": {
"20260401": {
"url": "https://geo.mirror.pkgbuild.com/images/v20260401.509747/Arch-Linux-x86_64-basic-20260401.509747.qcow2",
"name": "Arch-Linux-x86_64-basic-20260401.509747.qcow2",
"hash": "sha256-/gAq9jComkqFthi+jvchO68q3Bpd3bp4qbGkQxmPPa8="
}
}
}
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
description = "Nix-VM-Test, re-use the NixOS VM integration test infrastructure on Ubuntu, Debian and Fedora";
description = "Nix-VM-Test, re-use the NixOS VM integration test infrastructure on Ubuntu, Debian, Fedora, Rocky and Arch Linux";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
Expand Down
3 changes: 2 additions & 1 deletion lib.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ let
debian = pkgs.callPackage ./debian { inherit generic system; };
fedora = pkgs.callPackage ./fedora { inherit generic system; };
rocky = pkgs.callPackage ./rocky { inherit generic system; };
archlinux = pkgs.callPackage ./archlinux { inherit generic system; };
# Function that can be used when defining inline modules to get better location
# reporting in module-system errors.
# Usage example:
# { _file = "${printAttrPos (builtins.unsafeGetAttrPos "a" { a = null; })}: inline module"; }
nixos = "${nixpkgs}/nixos";
in {
inherit ubuntu debian fedora rocky;
inherit ubuntu debian fedora rocky archlinux;
}
3 changes: 2 additions & 1 deletion overlay.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ let
debian = prev.callPackage ./debian { inherit generic system; };
fedora = prev.callPackage ./fedora { inherit generic system; };
rocky = prev.callPackage ./rocky { inherit generic system; };
archlinux = prev.callPackage ./archlinux { inherit generic system; };
in

{
testers = prev.testers or { } // {
nonNixOSDistros = prev.testers.nonNixOSDistros or {} // {
inherit debian ubuntu fedora rocky;
inherit debian ubuntu fedora rocky archlinux;
};
};
}
39 changes: 39 additions & 0 deletions scripts/update-images.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,49 @@ def gen_entry_dict(entry):
}
return json.dumps(res)

def get_latest_archlinux_release(index_url):
print(f"[+] Parsing archlinux index {index_url}")
page = requests.get(index_url)
soup = BeautifulSoup(page.content, "html.parser")
links = soup.find_all("a")
versioned = [
link["href"].rstrip("/")
for link in links
if re.compile(r"^v[0-9]{8}\.[0-9]+/?$").match(link["href"])
]
parsed = [
(datetime.strptime(v[1:9], "%Y%m%d"), v) for v in versioned
]
return max(parsed)[1]

def archlinux_parse():
index_url = "https://geo.mirror.pkgbuild.com/images/"
latest = get_latest_archlinux_release(index_url)
date_part = latest[1:9]
release_url = f"{index_url}{latest}/"

def fetch_sha256(url):
print(f"[+] Fetching SHA256 for {url}")
return requests.get(url).text.split()[0]

url = f"{release_url}Arch-Linux-x86_64-basic-{latest[1:]}.qcow2"
return json.dumps({
"x86_64-linux": {
date_part: {
"url": url,
"name": f"Arch-Linux-x86_64-basic-{latest[1:]}.qcow2",
"hash": nix_hash(url),
}
}
})

if __name__ == '__main__':
ubuntu_json = ubuntu_parse()
with open("ubuntu.json", "w") as f:
f.write(ubuntu_json)
debian_json = debian_parse()
with open("debian.json", "w") as f:
f.write(debian_json)
archlinux_json = archlinux_parse()
with open("archlinux.json", "w") as f:
f.write(archlinux_json)
16 changes: 16 additions & 0 deletions tests/archlinux.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{ pkgs, package, system }:
let
lib = package;
multiUserTest = runner: (runner {
sharedDirs = {};
testScript = ''
vm.wait_for_unit("multi-user.target")
'';
}).sandboxed;
runTestOnEveryImage = test:
pkgs.lib.mapAttrs'
(n: v: pkgs.lib.nameValuePair "${n}-multi-user-test" (test lib.archlinux.${n}))
lib.archlinux.images;
in
runTestOnEveryImage multiUserTest //
package.archlinux.images
3 changes: 2 additions & 1 deletion tests/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ let
debian = addPrefixToTests "debian-" (import ./debian.nix { inherit package pkgs system; });
fedora = addPrefixToTests "fedora-" (import ./fedora.nix { inherit package pkgs system; });
rocky = addPrefixToTests "rocky-" (import ./rocky.nix { inherit package pkgs system; });
in ubuntu // debian // fedora // rocky
archlinux = addPrefixToTests "archlinux-" (import ./archlinux.nix { inherit package pkgs system; });
in ubuntu // debian // fedora // rocky // archlinux