aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-10-17 17:30:15 +0900
committerOphestra <cat@gensokyo.uk>2026-10-17 18:01:12 +0900
commitd1a648f9379fe2b4f6a3b2230889d520cae2f868 (patch)
treea9d74fc44d7a4bb135af5b2acccdc8b14fb4bcd8
parent79cd9088c7b541d2a3c9e9cad879f0ae46632ca6 (diff)
cmd/hakurei/testsuite: migrate testsHEADmasterdevelop
This runs much faster than the NixOS test suite, and is much more precise. This also uncovered a recent severe pipewire regression. This change removes all remaining deprecated nix code. Closes #22. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--.gitea/workflows/test.yml2
-rw-r--r--cmd/hakurei/testsuite/configuration.nix252
-rw-r--r--cmd/hakurei/testsuite/default.nix81
-rw-r--r--cmd/hakurei/testsuite/flake.lock49
-rw-r--r--cmd/hakurei/testsuite/flake.nix75
-rw-r--r--cmd/hakurei/testsuite/hsu.nix23
-rw-r--r--cmd/hakurei/testsuite/main.go832
-rw-r--r--cmd/hakurei/testsuite/nixos.nix407
-rw-r--r--cmd/hakurei/testsuite/options.nix364
-rw-r--r--cmd/hakurei/testsuite/package.nix145
-rw-r--r--cmd/hakurei/testsuite/test.py315
-rw-r--r--internal/testsuite/testsuite.go87
-rw-r--r--internal/workflows/Containerfile3
-rw-r--r--internal/workflows/doc.go3
-rw-r--r--internal/workflows/step.go12
-rw-r--r--internal/workflows/test.go22
16 files changed, 912 insertions, 1760 deletions
diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml
index 85841526..639c4f22 100644
--- a/.gitea/workflows/test.yml
+++ b/.gitea/workflows/test.yml
@@ -1 +1 @@
-{"name":"Test","on":["push"],"jobs":{"hakurei-legacy":{"name":"Hakurei (legacy)","runs-on":"nix","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Run NixOS test","run":"nix build --out-link result --print-out-paths --print-build-logs ./cmd/hakurei/testsuite#checks.x86_64-linux.hakurei"},{"name":"Upload test output","uses":"actions/upload-artifact@v3","with":{"name":"hakurei-vm-output","path":"result/*","retention-days":1}}]},"hakurei-race-legacy":{"name":"Hakurei (legacy with race instrument)","runs-on":"nix","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Run NixOS test","run":"nix build --out-link result --print-out-paths --print-build-logs ./cmd/hakurei/testsuite#checks.x86_64-linux.race"},{"name":"Upload test output","uses":"actions/upload-artifact@v3","with":{"name":"hakurei-race-vm-output","path":"result/*","retention-days":1}}]},"sandbox":{"name":"Sandbox","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci dist -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite/sandbox"}]},"sandbox-race":{"name":"Sandbox (with race instrument)","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci race -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite/sandbox"}]},"sharefs":{"name":"ShareFS","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci dist -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Mount sharefs","run":"useradd -ru 1023 -md /var/lib/sdcard -k /var/empty -s /sbin/nologin media_rw \u0026\u0026 install -dm0 /sdcard \u0026\u0026 sharefs -o rw,noexec,nosuid,nodev,noatime,allow_other,mkdir,source=/var/lib/sdcard,setuid=1023,setgid=1023 /sdcard"},{"name":"Compile and run test suite","run":"sharefs -V \u0026\u0026 go run -tags=testsuite ./cmd/sharefs/testsuite"},{"name":"Upload test output","uses":"actions/upload-artifact@v3","with":{"name":"fs_mark","path":"result/*","retention-days":1}}]}}}
+{"name":"Test","on":["push"],"jobs":{"hakurei":{"name":"Hakurei","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci dist -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite"}]},"hakurei-race":{"name":"Hakurei (with race instrument)","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci race -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite"}]},"sandbox":{"name":"Sandbox","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci dist -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite/sandbox"}]},"sandbox-race":{"name":"Sandbox (with race instrument)","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci race -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite/sandbox"}]},"sharefs":{"name":"ShareFS","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci dist -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Mount sharefs","run":"useradd -ru 1023 -md /var/lib/sdcard -k /var/empty -s /sbin/nologin media_rw \u0026\u0026 install -dm0 /sdcard \u0026\u0026 sharefs -o rw,noexec,nosuid,nodev,noatime,allow_other,mkdir,source=/var/lib/sdcard,setuid=1023,setgid=1023 /sdcard"},{"name":"Compile and run test suite","run":"sharefs -V \u0026\u0026 go run -tags=testsuite ./cmd/sharefs/testsuite"},{"name":"Upload test output","uses":"actions/upload-artifact@v3","with":{"name":"fs_mark","path":"result/*","retention-days":1}}]}}}
diff --git a/cmd/hakurei/testsuite/configuration.nix b/cmd/hakurei/testsuite/configuration.nix
deleted file mode 100644
index 62d8239d..00000000
--- a/cmd/hakurei/testsuite/configuration.nix
+++ /dev/null
@@ -1,252 +0,0 @@
-{
- lib,
- pkgs,
- config,
- ...
-}:
-{
- users.users = {
- alice = {
- isNormalUser = true;
- description = "Alice Foobar";
- password = "foobar";
- uid = 1000;
- };
- untrusted = {
- isNormalUser = true;
- description = "Untrusted user";
- password = "foobar";
- uid = 1001;
-
- # For deny unmapped uid test:
- packages = [ config.environment.hakurei.package ];
- };
- };
-
- home-manager.users.alice.home.stateVersion = "24.11";
-
- # Automatically login on tty1 as a normal user:
- services.getty.autologinUser = "alice";
-
- security.pam.loginLimits = [
- {
- domain = "@users";
- item = "rtprio";
- type = "-";
- value = 1;
- }
- ];
-
- environment = {
- systemPackages = with pkgs; [
- # For D-Bus tests:
- mako
- libnotify
- ];
-
- variables = {
- SWAYSOCK = "/tmp/sway-ipc.sock";
- WLR_RENDERER = "pixman";
- };
-
- # To help with OCR:
- etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
- main = {
- font = "inconsolata:size=14";
- };
- colors = rec {
- foreground = "000000";
- background = "ffffff";
- regular2 = foreground;
- };
- };
- };
-
- fonts.packages = [ pkgs.inconsolata ];
-
- # Automatically configure and start Sway when logging in on tty1:
- programs.bash.loginShellInit = ''
- if [ "$(tty)" = "/dev/tty1" ]; then
- set -e
-
- mkdir -p ~/.config/sway
- (sed s/Mod4/Mod1/ /etc/sway/config &&
- echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
- echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
-
- sway --validate
- systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
- fi
- '';
-
- programs.sway.enable = true;
-
- # For PulseAudio tests:
- security.rtkit.enable = true;
- services.pipewire = {
- enable = true;
- alsa.enable = true;
- alsa.support32Bit = true;
- pulse.enable = true;
- jack.enable = true;
- };
-
- virtualisation = {
- # Hopefully reduces spurious test failures:
- memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192;
-
- qemu.options = [
- # Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
- "-vga none -device virtio-gpu-pci"
-
- # Increase Go test compiler performance:
- "-smp 16"
- ];
- };
-
- # Disk image is too small for some tests:
- boot.tmp.useTmpfs = true;
-
- environment.hakurei = {
- enable = true;
- stateDir = "/var/lib/hakurei";
- users.alice = 0;
-
- extraHomeConfig =
- { config, ... }:
- {
- # To test merge deduplication:
- options._hakurei.stateVersion = lib.mkOption { type = lib.types.str; };
-
- config = {
- home = { inherit (config._hakurei) stateVersion; };
- _hakurei.stateVersion = "23.05";
- };
- };
-
- commonPaths = [
- {
- type = "bind";
- src = "/var/tmp";
- write = true;
- }
- ];
-
- apps = {
- "cat.gensokyo.extern.bash.linger-timeout" = {
- name = "hakurei-check-linger-timeout";
- identity = 9999;
- share = pkgs.bash;
- packages = [ pkgs.bash ];
- command = ''
- sleep infinity & disown
- exit
- '';
- wait_delay = 1;
- enablements = {
- wayland = false;
- pipewire = false;
- };
- };
-
- "cat.gensokyo.extern.foot.noEnablements" = {
- name = "ne-foot";
- identity = 1;
- shareUid = true;
- verbose = true;
- share = pkgs.foot;
- packages = with pkgs; [
- foot
-
- # For wayland-info:
- wayland-utils
- ];
- command = "foot";
- enablements = {
- dbus = false;
- pipewire = false;
- };
- };
-
- "cat.gensokyo.extern.foot.noEnablements.immediate" = {
- name = "ne-foot-immediate";
- identity = 1;
- shareUid = true;
- verbose = true;
- wait_delay = -1;
- share = pkgs.foot;
- packages = [ ];
- command = "foot";
- enablements = {
- dbus = false;
- pipewire = false;
- };
- };
-
- "cat.gensokyo.extern.foot.pulseaudio" = {
- name = "pa-foot";
- identity = 2;
- verbose = true;
- share = pkgs.foot;
- packages = [ pkgs.foot ];
- command = "foot";
- enablements.dbus = false;
- };
-
- "cat.gensokyo.extern.Alacritty.x11" = {
- name = "x11-alacritty";
- identity = 1;
- shareUid = true;
- verbose = true;
- share = pkgs.alacritty;
- packages = with pkgs; [
- # For X11 terminal emulator:
- alacritty
-
- # For glinfo:
- mesa-demos
- ];
- command = "alacritty";
- enablements = {
- wayland = false;
- x11 = true;
- dbus = false;
- pipewire = false;
- };
- };
-
- "cat.gensokyo.extern.foot.directWayland" = {
- name = "da-foot";
- identity = 4;
- verbose = true;
- insecureWayland = true;
- share = pkgs.foot;
- packages = with pkgs; [
- foot
-
- # For wayland-info:
- wayland-utils
- ];
- command = "foot";
- enablements = {
- dbus = false;
- pipewire = false;
- };
- };
-
- "cat.gensokyo.extern.strace.wantFail" = {
- name = "strace-failure";
- identity = 5;
- verbose = true;
- share = pkgs.strace;
- command = "strace true";
- enablements = {
- wayland = false;
- x11 = false;
- dbus = false;
- pipewire = false;
- };
- };
- };
- };
-}
diff --git a/cmd/hakurei/testsuite/default.nix b/cmd/hakurei/testsuite/default.nix
deleted file mode 100644
index 81daa0a2..00000000
--- a/cmd/hakurei/testsuite/default.nix
+++ /dev/null
@@ -1,81 +0,0 @@
-{
- lib,
- testers,
- buildFHSEnv,
- writeShellScriptBin,
-
- system,
- self,
- withRace ? false,
-}:
-
-testers.nixosTest {
- name = "hakurei" + (if withRace then "-race" else "");
- nodes.machine =
- { options, pkgs, ... }:
- let
- fhs =
- let
- hakurei = options.environment.hakurei.package.default;
- in
- buildFHSEnv {
- pname = "hakurei-fhs";
- inherit (hakurei) version;
- targetPkgs = _: hakurei.targetPkgs;
- extraOutputsToInstall = [ "dev" ];
- profile = ''
- export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
- '';
- };
- in
- {
- environment.systemPackages = [
- # For go tests:
- (writeShellScriptBin "hakurei-test" ''
- # Assert hst CGO_ENABLED=0: ${
- with pkgs;
- runCommand "hakurei-hst-cgo" { nativeBuildInputs = [ self.packages.${system}.hakurei.go ]; } ''
- cp -r ${options.environment.hakurei.package.default.src} "$out"
- chmod -R +w "$out"
- cp ${writeText "hst_cgo_test.go" ''package hakurei_test;import("testing";"hakurei.app/hst");func TestTemplate(t *testing.T){hst.Template()}''} "$out/hst_cgo_test.go"
- (cd "$out" && HOME="$(mktemp -d)" CGO_ENABLED=0 go test .)
- ''
- }
-
- cd ${self.packages.${system}.hakurei.src}
- ${fhs}/bin/hakurei-fhs -c \
- 'CC="clang -O3 -Werror" go test --tags=noskip ${if withRace then "-race" else "-count 16"} ./...' \
- &> /tmp/hakurei-test.log && \
- touch /tmp/hakurei-test-ok
- touch /tmp/hakurei-test-done
- '')
- ];
-
- # Run with Go race detector:
- environment.hakurei = lib.mkIf withRace rec {
- # race detector does not support static linking
- package = (pkgs.callPackage ./package.nix { }).overrideAttrs (previousAttrs: {
- env = previousAttrs.env // {
- GOFLAGS = previousAttrs.env.GOFLAGS + " -race";
- };
- });
- hsuPackage = options.environment.hakurei.hsuPackage.default.override { hakurei = package; };
- };
-
- imports = [
- ./configuration.nix
-
- self.nixosModules.hakurei
- self.inputs.home-manager.nixosModules.home-manager
- ];
- };
-
- # adapted from nixos sway integration tests
-
- # testScriptWithTypes:49: error: Cannot call function of unknown type
- # (machine.succeed if succeed else machine.execute)(
- # ^
- # Found 1 error in 1 file (checked 1 source file)
- skipTypeCheck = true;
- testScript = builtins.readFile ./test.py;
-}
diff --git a/cmd/hakurei/testsuite/flake.lock b/cmd/hakurei/testsuite/flake.lock
deleted file mode 100644
index 5537506a..00000000
--- a/cmd/hakurei/testsuite/flake.lock
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "nodes": {
- "home-manager": {
- "inputs": {
- "nixpkgs": [
- "nixpkgs"
- ]
- },
- "locked": {
- "lastModified": 1787146702,
- "narHash": "sha256-YbRcLdU/yK4gWsQg7V8WTKZHfXL33g8+wSFUX3wyevs=",
- "owner": "nix-community",
- "repo": "home-manager",
- "rev": "173b7e8d40fdc8c296a9c99854314f17a3a1704c",
- "type": "github"
- },
- "original": {
- "owner": "nix-community",
- "ref": "release-26.05",
- "repo": "home-manager",
- "type": "github"
- }
- },
- "nixpkgs": {
- "locked": {
- "lastModified": 1787101114,
- "narHash": "sha256-gwrPcFf/rDjHPaVflbDZ040ZDmBTRj/7+s8ZmE2SaIM=",
- "owner": "NixOS",
- "repo": "nixpkgs",
- "rev": "b18a4b905f8d028dc4476412e6d6891728695379",
- "type": "github"
- },
- "original": {
- "owner": "NixOS",
- "ref": "nixos-26.05",
- "repo": "nixpkgs",
- "type": "github"
- }
- },
- "root": {
- "inputs": {
- "home-manager": "home-manager",
- "nixpkgs": "nixpkgs"
- }
- }
- },
- "root": "root",
- "version": 7
-}
diff --git a/cmd/hakurei/testsuite/flake.nix b/cmd/hakurei/testsuite/flake.nix
deleted file mode 100644
index e1df8990..00000000
--- a/cmd/hakurei/testsuite/flake.nix
+++ /dev/null
@@ -1,75 +0,0 @@
-{
- description = "hakurei container tool and nixos module";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
-
- home-manager = {
- url = "github:nix-community/home-manager/release-26.05";
- inputs.nixpkgs.follows = "nixpkgs";
- };
- };
-
- outputs =
- {
- self,
- nixpkgs,
- home-manager,
- }:
- let
- supportedSystems = [ "x86_64-linux" ];
-
- forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
- nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
- in
- {
- nixosModules.hakurei = import ./nixos.nix self.packages;
-
- checks = forAllSystems (
- system:
- let
- pkgs = nixpkgsFor.${system};
-
- inherit (pkgs) callPackage;
- in
- {
- hakurei = callPackage ./. { inherit system self; };
- race = callPackage ./. {
- inherit system self;
- withRace = true;
- };
- }
- );
-
- packages = forAllSystems (
- system:
- let
- inherit (self.packages.${system}) hakurei hsu;
- pkgs = nixpkgsFor.${system};
- in
- {
- default = hakurei;
- hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
- inherit (pkgs)
- # passthru.buildInputs
- go_1_27
- clang
-
- # nativeBuildInputs
- pkg-config
- wayland-scanner
- makeBinaryWrapper
-
- # appPackages
- glibc
- xdg-dbus-proxy
-
- # for check
- nettools
- ;
- };
- hsu = pkgs.callPackage ./hsu.nix { inherit (self.packages.${system}) hakurei; };
- }
- );
- };
-}
diff --git a/cmd/hakurei/testsuite/hsu.nix b/cmd/hakurei/testsuite/hsu.nix
deleted file mode 100644
index d1ddcb77..00000000
--- a/cmd/hakurei/testsuite/hsu.nix
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- lib,
- buildGoModule,
- hakurei ? abort "hakurei package required",
-}:
-
-buildGoModule {
- pname = "${hakurei.pname}-hsu";
- inherit (hakurei) version;
-
- src = ../../hsu;
- inherit (hakurei) vendorHash;
- env.CGO_ENABLED = 0;
-
- preBuild = ''
- go mod init hsu >& /dev/null
- '';
-
- ldflags = lib.attrsets.foldlAttrs (
- ldflags: name: value:
- ldflags ++ [ "-X main.${name}=${value}" ]
- ) [ "-s -w" ] { hakureiPath = "${hakurei}/libexec/hakurei"; };
-}
diff --git a/cmd/hakurei/testsuite/main.go b/cmd/hakurei/testsuite/main.go
new file mode 100644
index 00000000..ca48be8e
--- /dev/null
+++ b/cmd/hakurei/testsuite/main.go
@@ -0,0 +1,832 @@
+//go:build testsuite
+
+// The hakurei test program tests cmd/hakurei behaviour against a graphical
+// session and applications simulating a desktop workload.
+package main
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "slices"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+ "unsafe"
+
+ "hakurei.app/check"
+ "hakurei.app/ext"
+ "hakurei.app/fhs"
+ "hakurei.app/hst"
+ "hakurei.app/internal/testsuite"
+)
+
+// newConfig returns the address of a new bare minimal hakurei configuration.
+func newConfig(
+ name string,
+ file string,
+ args ...string,
+) *hst.Config {
+ c := hst.Config{
+ ID: "app.hakurei." + name,
+ Identity: int(testsuite.NewSerial() % hst.IdentityEnd),
+
+ Container: &hst.ContainerConfig{
+ Hostname: name,
+ Filesystem: []hst.FilesystemConfigJSON{
+ {FilesystemConfig: &hst.FSBind{
+ Target: fhs.AbsRoot,
+ Source: fhs.AbsRoot,
+ Special: true,
+ }},
+ },
+ Username: "nobody",
+ Shell: fhs.AbsUsrBin.Append("bash"),
+ Home: hst.AbsPrivateTmp,
+ },
+ }
+
+ if p, err := exec.LookPath(file); err != nil {
+ log.Fatal(err)
+ } else if p, err = filepath.Abs(p); err != nil {
+ log.Fatal(err)
+ } else {
+ c.Container.Path = check.MustAbs(p)
+ }
+ c.Container.Args = []string{filepath.Base(file)}
+ c.Container.Args = append(c.Container.Args, args...)
+ return &c
+}
+
+// checkHakurei starts a hakurei container and calls f.
+func checkHakurei(
+ ctx context.Context,
+ wg *sync.WaitGroup,
+ cred *syscall.Credential,
+ dbusEnv string,
+
+ name, long string,
+ scan, verbose, insecure bool,
+ processConfig func(c *hst.Config),
+ f func(p hst.State, pid int) error,
+
+ checkOutput func(output string) bool,
+ wantCode ...int,
+) {
+ if !scan && f != nil {
+ log.Fatalf("(%s) cannot inspect without scanning", name)
+ }
+ if checkOutput == nil {
+ checkOutput = func(output string) bool { return output == "" }
+ }
+ if len(wantCode) == 0 {
+ wantCode = []int{130}
+ }
+
+ c := newConfig(name, "sleep", "infinity")
+ if processConfig != nil {
+ processConfig(c)
+ }
+
+ wg.Go(func() {
+ p, pid := testsuite.MustStartHakurei(
+ ctx, wg, cred, dbusEnv, func(err error, output string) {
+ if err == nil {
+ if !slices.Contains(wantCode, 0) {
+ log.Fatalf(
+ "%s test process unexpectedly succeeded\n%s",
+ long, output,
+ )
+ }
+ } else if e, ok := errors.AsType[*exec.ExitError](err); !ok {
+ log.Fatalf("%s: %v\n%s", long, err, output)
+ } else if !slices.Contains(wantCode, e.ExitCode()) {
+ log.Fatalf("%s: %v\n%s", long, e, output)
+ }
+
+ if !checkOutput(output) {
+ log.Fatalf("%s: %q", long, output)
+ }
+
+ log.Println("validated", long)
+ }, scan, false, verbose, insecure, c,
+ )
+
+ if f != nil {
+ if err := f(p, pid); err != nil {
+ log.Fatalf("(%s) %v", name, err)
+ }
+ }
+
+ if scan {
+ testsuite.MustRun(
+ cred, testsuite.SessionEnv(dbusEnv),
+ "hakurei", "kill", p.ID.String(),
+ )
+ testsuite.PollPID(long, pid)
+ }
+ })
+}
+
+func main() {
+ go testsuite.ReceiveSignals()
+ cred := syscall.Credential{Uid: 1000, Gid: 100}
+
+ // the signal handler does not wait for termination
+ ctx := context.Background()
+
+ testsuite.MustPrepareUserDirs(&cred)
+ var swg sync.WaitGroup
+ defer swg.Wait()
+ dbusEnv := testsuite.MustStartSessionBus(&cred)
+ testsuite.MustStartSway(&swg, &cred, dbusEnv)
+ defer testsuite.TerminateSway(&cred)
+ pipewireVersion := testsuite.MustStartPipeWire(&cred, dbusEnv)
+ sessionEnv := testsuite.SessionEnv(dbusEnv)
+ absNonexistent := fhs.AbsProc.Append("nonexistent")
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ wg.Go(func() {
+ defer log.Println("validated static linking")
+
+ for _, name := range []string{
+ "hakurei",
+ "hsu",
+ "sharefs",
+ } {
+ pathname := fhs.AbsUsr.Append("local/bin", name)
+ testsuite.AssertOutput(
+ "validating "+pathname.String()+" static linking",
+ "", "\tnot a dynamic executable\n",
+ )(testsuite.MustOutput(
+ ctx, nil, nil, hst.ExitFailure,
+ "validate "+pathname.String()+" static linking",
+ "ldd", pathname.String(),
+ ))
+ }
+ })
+
+ wg.Go(func() {
+ defer log.Println("validated deny unmapped uid")
+
+ testsuite.AssertOutput(
+ "denying unmapped uid",
+ "", "hsu: uid 65534 is not in the hsurc file\n",
+ )(testsuite.MustOutput(
+ ctx, &syscall.Credential{
+ Uid: 65534,
+ Gid: 100,
+ }, nil, hst.ExitFailure, "deny unmapped uid",
+ "hakurei", "exec", "true",
+ ))
+ })
+
+ wg.Go(func() {
+ defer log.Println("validated deny unmapped uid (verbose)")
+
+ testsuite.AssertOutput(
+ "denying unmapped uid (verbose)",
+ "", "hsu: uid 65534 is not in the hsurc file\n"+
+ "hakurei: *cannot retrieve user id from setuid wrapper: "+
+ "current user is not in the hsurc file\n",
+ )(testsuite.MustOutput(
+ ctx, &syscall.Credential{
+ Uid: 65534,
+ Gid: 100,
+ }, nil, hst.ExitFailure, "deny unmapped uid (verbose)",
+ "hakurei", "-v", "exec", "true",
+ ))
+ })
+
+ wg.Go(func() {
+ defer log.Println("validated direct hsu call")
+
+ testsuite.AssertOutput(
+ "calling hsu directly",
+ "0", "",
+ )(testsuite.MustOutput(
+ ctx, &cred, nil, 0, "direct hsu call",
+ "hsu",
+ ))
+ })
+
+ wg.Go(func() {
+ defer log.Println("validated direct hsu deny")
+
+ testsuite.AssertOutput(
+ "from direct hsu denial",
+ "", "hsu: uid 65534 is not in the hsurc file\n",
+ )(testsuite.MustOutput(
+ ctx, &syscall.Credential{
+ Uid: 65534,
+ Gid: 100,
+ }, nil, hst.ExitFailure, "direct hsu deny",
+ "hsu",
+ ))
+ })
+
+ wg.Go(func() {
+ defer log.Println("validated timeout behaviour")
+
+ testsuite.AssertOutput(
+ "validating timeout behaviour",
+ "", "init: timeout exceeded waiting for lingering processes\n",
+ )(testsuite.MustOutput(
+ ctx, &cred, nil, 0, "timeout lingering process",
+ "hakurei", "exec", "--no-wait",
+ "bash", "-c", "sleep infinity & disown",
+ ))
+ })
+
+ wg.Go(func() {
+ defer log.Println("validated pd shared tmpdir")
+
+ serial := testsuite.NewSerial()
+ _serial := strconv.FormatUint(serial, 10)
+ testsuite.AssertOutput(
+ "validating pd shared tmpdir",
+ "", "",
+ )(testsuite.MustOutput(
+ ctx, &cred, nil, 0, "pd shared tmpdir",
+ "hakurei", "exec", "touch", "/tmp/ok."+_serial,
+ ))
+
+ if err := os.Remove(
+ testsuite.HakureiPrefix + "tmpdir/0/ok." + _serial,
+ ); err != nil {
+ log.Fatal(err)
+ }
+ })
+
+ wg.Go(func() {
+ defer log.Println("validated graceful failure on bad Wayland display name")
+
+ testsuite.AssertOutput(
+ "validating Wayland graceful failure",
+ "", "hakurei: cannot connect to "+
+ "/tmp/hakurei.0/compat/wayland-0: no such file or directory\n",
+ )(testsuite.MustOutput(
+ ctx, &cred, nil, hst.ExitRequest, "bad Wayland name",
+ "hakurei", "exec", "--wayland",
+ "true",
+ ))
+ })
+
+ wg.Go(func() {
+ if _, _, err := testsuite.StartHakurei(
+ ctx, &wg, &cred, dbusEnv, func(cmd *exec.Cmd) {
+ cmd.ExtraFiles = cmd.ExtraFiles[:len(cmd.ExtraFiles)-1]
+ }, func(err error, output string) {
+ defer log.Println("validated bad identifier fd")
+
+ if err == nil {
+ log.Fatal("bad identity fd unexpectedly succeeded")
+ }
+ if e, ok := errors.AsType[*exec.ExitError](err); !ok {
+ log.Fatal(err)
+ } else if e.ExitCode() != hst.ExitRequest {
+ fmt.Print(output)
+ log.Fatal(e)
+ }
+
+ if output != "hakurei: cannot write identifier: "+
+ "bad file descriptor\r\n" {
+ log.Fatalf(
+ "bad identity fd unexpected output: %q",
+ output,
+ )
+ }
+ }, false, false, false, false, &hst.Config{
+ Container: &hst.ContainerConfig{
+ Shell: absNonexistent,
+ Home: absNonexistent,
+ Path: absNonexistent,
+ },
+ },
+ ); err != nil {
+ log.Fatal(err)
+ }
+ })
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "indirect-termination", "indirect termination",
+ true, false, false, nil, nil, nil,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "immediate-termination", "immediate termination",
+ true, false, false, func(c *hst.Config) {
+ c.Container.WaitDelay = -1
+ }, nil, nil, 254,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "shim-poke", "unexpected shim notification",
+ true, true, false, nil, func(p hst.State, pid int) error {
+ if err := syscall.Kill(p.ShimPID, syscall.SIGCONT); err != nil {
+ return os.NewSyscallError("kill", err)
+ }
+ return nil
+ }, func(output string) bool {
+ return strings.Contains(
+ output,
+ "shim: got SIGCONT from unexpected process\r\n",
+ )
+ },
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "reject-deprecated", "reject insecure configuration",
+ false, false, false, func(c *hst.Config) {
+ c.Enablements = new(hst.EPulse)
+ }, nil, func(output string) bool {
+ return output == "hakurei: enablement PulseAudio is insecure "+
+ "and no longer supported\r\n"
+ }, hst.ExitFailure,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "pipewire-pulse", "pipewire proxying behaviour",
+ false, false, false, func(c *hst.Config) {
+ c.Enablements = new(hst.EPipeWire)
+ c.Container.Path = fhs.AbsUsrBin.Append("pactl")
+ c.Container.Args = []string{"pactl", "info"}
+ }, nil, func(output string) bool {
+ _output, _, ok := strings.CutLast(output, "Cookie: ")
+ if !ok {
+ return false
+ }
+ output, _output, ok = strings.Cut(_output, "\r\nClient Index: ")
+ if !ok {
+ return false
+ }
+ _, _output, ok = strings.Cut(_output, "\r\n")
+ if !ok {
+ return false
+ }
+ output += "\r\n" + _output
+ output = strings.Replace(
+ output,
+ "@DEFAULT_SINK@",
+ "auto_null",
+ 1,
+ )
+
+ return output == "Server String: unix:/run/user/65534/pulse/native\r\n"+
+ "Library Protocol Version: 35\r\n"+
+ "Server Protocol Version: 35\r\n"+
+ "Is Local: yes\r\n"+
+ "Tile Size: "+strconv.Itoa((os.Getpagesize()-4)*16)+"\r\n"+
+ "User Name: nobody\r\n"+
+ "Host Name: hakurei-pipewire-pulse\r\n"+
+ "Server Name: PulseAudio (on PipeWire "+pipewireVersion+")\r\n"+
+ "Server Version: 15.0.0\r\n"+
+ "Default Sample Specification: float32le 2ch 48000Hz\r\n"+
+ "Default Channel Map: front-left,front-right\r\n"+
+ "Default Sink: auto_null\r\n"+
+ "Default Source: @DEFAULT_SOURCE@\r\n"
+ }, 0,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "pipewire-security", "pipewire SecurityContext",
+ false, false, false, func(c *hst.Config) {
+ c.Enablements = new(hst.EPipeWire)
+ c.Container.Path = fhs.AbsUsrBin.Append("pactl")
+ c.Container.Args = []string{
+ "pactl",
+ "set-sink-mute",
+ "auto_null",
+ "toggle",
+ }
+ }, nil, func(output string) bool {
+ return output == "Failure: Access denied\r\n"
+ }, hst.ExitFailure,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "pipewire-direct-failure", "pipewire cover direct access",
+ false, false, false, func(c *hst.Config) {
+ c.Enablements = new(hst.EPipeWire)
+ c.Container.Path = fhs.AbsUsrBin.Append("pw-dump")
+ c.Container.Args = []string{"pw-dump"}
+ }, nil, func(output string) bool {
+ return output == "can't connect: Host is down\r\n"
+ }, 255,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "pipewire-direct", "pipewire direct access",
+ false, false, true, func(c *hst.Config) {
+ c.Enablements = new(hst.EPipeWire)
+ c.DirectPipeWire = true
+ c.Container.Path = fhs.AbsUsrBin.Append("pw-dump")
+ c.Container.Args = []string{"pw-dump", "-RN"}
+ }, nil, func(output string) bool {
+ return json.Valid(unsafe.Slice(unsafe.StringData(output), len(output)))
+ }, 0,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "wayland-client", "wayland client",
+ true, false, false, func(c *hst.Config) {
+ c.Enablements = new(hst.EWayland)
+ c.Container.Path = fhs.AbsUsrBin.Append("foot")
+ c.Container.Args = []string{"foot"}
+ }, func(p hst.State, pid int) error {
+ testsuite.PollSwayWindow(ctx, &cred, func(n *testsuite.SwayNode) bool {
+ return n.Sandbox == "app.hakurei" &&
+ n.ID == "app.hakurei.wayland-client" &&
+ n.Instance == p.ID &&
+ n.AppID == "foot"
+ })
+ return nil
+ }, func(string) bool { return true }, 230,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "wayland-security-context", "wayland security-context-v1",
+ false, false, false, func(c *hst.Config) {
+ c.Enablements = new(hst.EWayland)
+ c.Container.Path = fhs.AbsUsrBin.Append("grim")
+ c.Container.Args = []string{"grim"}
+ }, nil, func(output string) bool {
+ return output == "warning: zxdg_output_manager_v1 isn't available, "+
+ "guessing the output layout\r\n"+
+ "compositor doesn't support wlr-screencopy-unstable-v1\r\n"
+ }, hst.ExitFailure,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "wlr-screencopy-unstable-v1", "direct wlr-screencopy-unstable-v1",
+ false, false, true, func(c *hst.Config) {
+ c.Enablements = new(hst.EWayland)
+ c.DirectWayland = true
+ c.Container.Path = fhs.AbsUsrBin.Append("grim")
+ c.Container.Args = []string{"grim", "/dev/null"}
+ }, nil, nil, 0,
+ )
+
+ // this client lacks attached identification
+ wg.Wait()
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "direct-wayland-client", "direct wayland client",
+ true, false, true, func(c *hst.Config) {
+ c.Enablements = new(hst.EWayland)
+ c.DirectWayland = true
+ c.Container.Path = fhs.AbsUsrBin.Append("foot")
+ c.Container.Args = []string{"foot"}
+ }, func(p hst.State, pid int) error {
+ testsuite.PollSwayWindow(ctx, &cred, func(n *testsuite.SwayNode) bool {
+ return n.Sandbox == "" &&
+ n.ID == "" &&
+ n.Instance == hst.ID{} &&
+ n.AppID == "foot"
+ })
+ return nil
+ }, func(string) bool { return true }, 230,
+ )
+ wg.Wait()
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "xwayland-client", "xwayland client",
+ false, false, false, func(c *hst.Config) {
+ c.Enablements = new(hst.EX11)
+ c.Container.Path = fhs.AbsUsrBin.Append("xset")
+ c.Container.Args = []string{"xset", "q"}
+ c.Container.Flags |= hst.FHostAbstract
+ }, nil, func(output string) bool {
+ return strings.Contains(output, "DPMS")
+ }, 0,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "dbus", "proxied dbus call",
+ false, false, false, func(c *hst.Config) {
+ c.Enablements = new(hst.EDBus)
+ c.SessionBus = &hst.BusConfig{
+ Talk: []string{"org.freedesktop.Notifications"},
+ Filter: true,
+ }
+ c.Container.Path = fhs.AbsUsrBin.Append("notify-send")
+ c.Container.Args = []string{
+ "notify-send",
+ "Test notification",
+ }
+ }, nil, func(output string) bool {
+ return output == "GDBus.Error:"+
+ "org.freedesktop.DBus.Error.ServiceUnknown: "+
+ "The name org.freedesktop.Notifications "+
+ "was not provided by any .service files\r\n"
+ }, hst.ExitFailure,
+ )
+
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "dbus reject", "rejected dbus call",
+ false, false, false, func(c *hst.Config) {
+ c.Enablements = new(hst.EDBus)
+ c.SessionBus = &hst.BusConfig{Filter: true}
+ c.Container.Path = fhs.AbsUsrBin.Append("notify-send")
+ c.Container.Args = []string{
+ "notify-send",
+ "Test notification",
+ }
+ }, nil, func(output string) bool {
+ return output == "GDBus.Error:"+
+ "org.freedesktop.DBus.Error.ServiceUnknown: "+
+ "org.freedesktop.DBus.Error.ServiceUnknown\r\n"
+ }, hst.ExitFailure,
+ )
+
+ silentOutputInterrupt := func(args ...string) {
+ wg.Go(func() {
+ defer log.Printf(
+ "validated silent output after interrupt (args: %q)",
+ strings.Join(args, " "),
+ )
+
+ serial := testsuite.NewSerial()
+ _serial := strconv.FormatUint(serial, 10)
+ cmd := exec.CommandContext(ctx, "hakurei", slices.Concat(
+ []string{"exec"},
+ args,
+ []string{"sh", "-c",
+ "touch /tmp/ok." + _serial + " && sleep infinity"},
+ )...)
+ var stdout, stderr strings.Builder
+ cmd.Stdout, cmd.Stderr = &stdout, &stderr
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGTERM,
+ Credential: &cred,
+ Setpgid: true,
+ }
+ testsuite.MustAppendEnv(cmd, sessionEnv...)
+
+ done := testsuite.MustStart(cmd)
+ for {
+ select {
+ case err := <-done:
+ fmt.Print(stdout.String())
+ fmt.Print(stderr.String())
+ if err == nil {
+ log.Fatalf(
+ "test process %d terminated unexpectedly",
+ serial,
+ )
+ }
+ log.Fatalf(
+ "test process %d terminated unexpectedly: %v",
+ serial, err,
+ )
+ default:
+ break
+ }
+
+ time.Sleep(100 * time.Millisecond)
+ if err := os.Remove(
+ testsuite.HakureiPrefix + "tmpdir/0/ok." + _serial,
+ ); err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ continue
+ }
+ log.Fatal(err)
+ }
+ break
+ }
+
+ if err := syscall.Kill(
+ -cmd.Process.Pid,
+ syscall.SIGINT,
+ ); err != nil {
+ log.Fatalf(
+ "cannot interrupt process group %d: %v",
+ serial, err,
+ )
+ }
+
+ if err := <-done; err == nil {
+ log.Fatal("interrupted process unexpectedly succeeded")
+ } else if e, ok := errors.AsType[*exec.ExitError](err); !ok {
+ log.Fatalf("validate silent interrupt: %v", err)
+ } else if code := e.ExitCode(); !slices.Contains([]int{
+ hst.ExitFailure,
+ hst.ExitCancel,
+ 127, 130, 254,
+ }, code) {
+ log.Fatalf("validate silent interrupt: %v", e)
+ }
+
+ if stdout.Len() != 0 || stderr.Len() != 0 {
+ log.Fatalf(
+ "interrupt is not silent (args: %q)"+
+ "\nstdout = %q\nstderr = %q",
+ strings.Join(args, " "),
+ stdout.String(),
+ stderr.String(),
+ )
+ }
+ })
+ }
+ silentOutputInterrupt()
+ silentOutputInterrupt("--dbus")
+ silentOutputInterrupt("--wayland", "-X", "--dbus", "--pulse")
+
+ const RLIMIT_RTPRIO = 14
+ if err := syscall.Setrlimit(RLIMIT_RTPRIO, &syscall.Rlimit{
+ Cur: 49,
+ Max: 49,
+ }); err != nil {
+ log.Fatalf("cannot increase rtprio limit: %v", err)
+ }
+ for _, policy := range []ext.SchedPolicy{
+ ext.SCHED_NORMAL,
+ ext.SCHED_IDLE,
+ ext.SCHED_RR,
+ } {
+ _policy := policy
+ checkHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ "setscheduler-"+strconv.Itoa(int(policy)), "setscheduler",
+ true, false, false, func(c *hst.Config) {
+ c.SchedPolicy = _policy
+ }, func(p hst.State, pid int) error {
+ got := -1
+ r, err := os.Open(filepath.Join(
+ fhs.Proc,
+ strconv.Itoa(pid),
+ "sched",
+ ))
+ if err != nil {
+ return err
+ }
+
+ s := bufio.NewScanner(r)
+
+ for range 2 {
+ if !s.Scan() {
+ _ = r.Close()
+ if err = s.Err(); err != nil {
+ return err
+ }
+ return errors.New("sched header too short")
+ }
+ }
+
+ for s.Scan() {
+ if !strings.HasPrefix(s.Text(), "policy") {
+ continue
+ }
+
+ k, v, ok := strings.Cut(s.Text(), ":")
+ if !ok {
+ continue
+ }
+ if strings.TrimSpace(k) != "policy" {
+ continue
+ }
+
+ got, err = strconv.Atoi(strings.TrimSpace(v))
+ if err != nil {
+ _ = r.Close()
+ return err
+ }
+ }
+ if err = s.Err(); err != nil {
+ _ = r.Close()
+ return err
+ } else if err = r.Close(); err != nil {
+ return err
+ }
+
+ if _got := ext.SchedPolicy(got); _got != policy {
+ return fmt.Errorf("unexpected %q policy %q", policy, _got)
+ }
+ return nil
+ }, nil,
+ )
+
+ wg.Go(func() {
+ defer log.Println("validated revert type selection")
+
+ c := newConfig("rts 0", "sleep", "infinity")
+ c.Container.Flags |= hst.FShareRuntime
+ uid := hst.ToUser(0, c.Identity)
+
+ onDone := func(err error, output string) {
+ if err == nil {
+ log.Fatal("revert type selection test process " +
+ "terminated unexpectedly")
+ } else if e, ok := errors.AsType[*exec.ExitError](err); !ok {
+ log.Fatalf("revert type selection: %v\n%s", err, output)
+ } else if e.ExitCode() != 130 {
+ log.Fatalf("revert type selection: %v\n%s", e, output)
+ }
+
+ if output != "" {
+ log.Fatalf("unexpected output\n%s", output)
+ }
+ }
+
+ validatePresent := func() {
+ testsuite.AssertOutput(
+ "validating ACLs for rts present",
+ "user::rwx\n"+
+ "user:"+strconv.Itoa(uid)+":rwx\n"+
+ "group::---\n"+
+ "mask::rwx\n"+
+ "other::---\n"+
+ "\n",
+ "",
+ )(testsuite.MustOutput(
+ ctx, nil, nil, 0, "getfacl rts present",
+ "getfacl",
+ "--absolute-names",
+ "--omit-header",
+ "--numeric",
+ "/tmp/hakurei.0/runtime/"+strconv.Itoa(c.Identity),
+ ))
+ }
+ validateReverted := func() {
+ testsuite.AssertOutput(
+ "validating ACLs for rts reverted",
+ "user::rwx\n"+
+ "group::---\n"+
+ "mask::---\n"+
+ "other::---\n"+
+ "\n",
+ "",
+ )(testsuite.MustOutput(
+ ctx, nil, nil, 0, "getfacl rts present",
+ "getfacl",
+ "--absolute-names",
+ "--omit-header",
+ "--numeric",
+ "/tmp/hakurei.0/runtime/"+strconv.Itoa(c.Identity),
+ ))
+ }
+
+ p0, pid0 := testsuite.MustStartHakurei(
+ ctx, &wg, &cred, dbusEnv, onDone,
+ true, false, false, false, c,
+ )
+ validatePresent()
+
+ p1, pid1 := testsuite.MustStartHakurei(
+ ctx, &wg, &cred, dbusEnv, onDone,
+ true, false, false, false, c,
+ )
+ validatePresent()
+
+ testsuite.MustRun(
+ &cred, testsuite.SessionEnv(dbusEnv),
+ "hakurei", "kill", p1.ID.String(),
+ )
+ testsuite.PollPID("revert type selection 1", pid1)
+ validatePresent()
+
+ testsuite.MustRun(
+ &cred, testsuite.SessionEnv(dbusEnv),
+ "hakurei", "kill", p0.ID.String(),
+ )
+ testsuite.PollPID("revert type selection 0", pid0)
+ validateReverted()
+ })
+
+ wg.Wait()
+
+ if dents, err := os.ReadDir("/tmp"); err != nil {
+ log.Fatal(err)
+ } else {
+ for _, dent := range dents {
+ if name := dent.Name(); strings.HasPrefix(name, ".hakurei-shim-") {
+ log.Fatalf("leftover shim work dir %q", name)
+ }
+ }
+ }
+ }
+}
diff --git a/cmd/hakurei/testsuite/nixos.nix b/cmd/hakurei/testsuite/nixos.nix
deleted file mode 100644
index 49bfffb6..00000000
--- a/cmd/hakurei/testsuite/nixos.nix
+++ /dev/null
@@ -1,407 +0,0 @@
-packages:
-{
- lib,
- pkgs,
- config,
- ...
-}:
-
-let
- inherit (lib)
- lists
- attrsets
- mkMerge
- mkIf
- mapAttrs
- foldlAttrs
- optional
- optionals
- ;
-
- cfg = config.environment.hakurei;
-
- # userid*userOffset + appStart + appid
- getsubuid = userid: appid: userid * 100000 + 10000 + appid;
- getsubname = userid: appid: "u${toString userid}_a${toString appid}";
- getsubhome = userid: appid: "${cfg.stateDir}/u${toString userid}/a${toString appid}";
-
- mountpoints = {
- ${cfg.sharefs.name} = mkIf (cfg.sharefs.source != null) {
- depends = [ cfg.sharefs.source ];
- device = "sharefs";
- fsType = "fuse.sharefs";
- noCheck = true;
- options = [
- "rw"
- "noexec"
- "nosuid"
- "nodev"
- "noatime"
- "allow_other"
- "mkdir"
- "source=${cfg.sharefs.source}"
- "setuid=${toString config.users.users.${cfg.sharefs.user}.uid}"
- "setgid=${toString config.users.groups.${cfg.sharefs.group}.gid}"
- ];
- };
- };
-in
-
-{
- imports = [ (import ./options.nix packages) ];
-
- options = {
- # Forward declare a dummy option for VM filesystems since the real one won't exist
- # unless the VM module is actually imported.
- virtualisation.fileSystems = lib.mkOption { };
- };
-
- config = mkIf cfg.enable {
- assertions = [
- (
- let
- conflictingApps = foldlAttrs (
- acc: id: app:
- (
- acc
- ++ foldlAttrs (
- acc': id': app':
- if id == id' || app.shareUid && app'.shareUid || app.identity != app'.identity then acc' else acc' ++ [ id ]
- ) [ ] cfg.apps
- )
- ) [ ] cfg.apps;
- in
- {
- assertion = (lists.length conflictingApps) == 0;
- message = "the following hakurei apps have conflicting identities: " + (builtins.concatStringsSep ", " conflictingApps);
- }
- )
- ];
-
- security.wrappers.hsu = {
- source = "${cfg.hsuPackage}/bin/hsu";
- setuid = true;
- owner = "root";
- group = "root";
- };
-
- environment.etc.hsurc = {
- mode = "0400";
- text = foldlAttrs (
- acc: username: fid:
- "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc
- ) "" cfg.users;
- };
-
- environment.systemPackages = optional (cfg.sharefs.source != null) cfg.sharefs.package;
- fileSystems = mountpoints;
- virtualisation.fileSystems = mountpoints;
-
- home-manager =
- let
- privPackages = mapAttrs (_: userid: {
- home.packages = foldlAttrs (
- acc: id: app:
- [
- (
- let
- extendDBusDefault = id: ext: {
- filter = true;
-
- talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
- own = [
- "${id}.*"
- "org.mpris.MediaPlayer2.${id}.*"
- ]
- ++ ext.own;
-
- inherit (ext) call broadcast;
- };
- dbusConfig =
- let
- default = {
- talk = [ ];
- own = [ ];
- call = { };
- broadcast = { };
- };
- in
- {
- session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault id)) else (extendDBusDefault id default);
- system_bus = app.dbus.system;
- };
- command = if app.command == null then app.name else app.command;
- script = if app.script == null then ("exec " + command + " $@") else app.script;
- isGraphical = if app.gpu != null then app.gpu else app.enablements.wayland || app.enablements.x11;
-
- conf = {
- inherit id;
- inherit (app) identity enablements;
- inherit (dbusConfig) session_bus system_bus;
- direct_wayland = app.insecureWayland;
- sched_policy = app.schedPolicy;
- sched_priority = app.schedPriority;
- groups = app.groups ++ optional (cfg.sharefs.source != null) cfg.sharefs.group;
-
- container = {
- inherit (app)
- wait_delay
- devel
- userns
- device
- tty
- multiarch
- env
- ;
- map_real_uid = app.mapRealUid;
- host_net = app.hostNet;
- host_abstract = app.hostAbstract;
- share_runtime = app.shareRuntime;
- share_tmpdir = app.shareTmpdir;
-
- filesystem =
- let
- bind = src: {
- type = "bind";
- inherit src;
- };
- optBind = src: {
- type = "bind";
- inherit src;
- optional = true;
- };
- optDevBind = src: {
- type = "bind";
- inherit src;
- dev = true;
- optional = true;
- };
- in
- [
- (bind "/bin")
- (bind "/usr/bin")
- (bind "/nix/store")
- (optBind "/sys/block")
- (optBind "/sys/bus")
- (optBind "/sys/class")
- (optBind "/sys/dev")
- (optBind "/sys/devices")
- ]
- ++ optionals app.nix [
- (bind "/nix/var")
- ]
- ++ optionals isGraphical [
- (optDevBind "/dev/dri")
- (optDevBind "/dev/nvidiactl")
- (optDevBind "/dev/nvidia-modeset")
- (optDevBind "/dev/nvidia-uvm")
- (optDevBind "/dev/nvidia-uvm-tools")
- (optDevBind "/dev/nvidia0")
- ]
- ++ optionals app.useCommonPaths cfg.commonPaths
- ++ app.extraPaths
- ++ [
- {
- type = "bind";
- dst = "/etc/";
- src = "/etc/";
- special = true;
- }
- {
- type = "link";
- dst = "/run/current-system";
- linkname = "/run/current-system";
- dereference = true;
- }
- ]
- ++ optionals (isGraphical && config.hardware.graphics.enable) (
- [
- {
- type = "link";
- dst = "/run/opengl-driver";
- linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument;
- }
- ]
- ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [
- {
- type = "link";
- dst = "/run/opengl-driver-32";
- linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument;
- }
- ]
- )
- ++ [
- {
- type = "bind";
- src = getsubhome userid app.identity;
- write = true;
- ensure = true;
- }
- ];
-
- username = getsubname userid app.identity;
- inherit (cfg) shell;
- home = getsubhome userid app.identity;
-
- path =
- if app.path == null then
- pkgs.writeScript "${app.name}-start" ''
- #!${pkgs.zsh}${pkgs.zsh.shellPath}
- ${script}
- ''
- else
- app.path;
- args = if app.args == null then [ "${app.name}-start" ] else app.args;
- };
- };
-
- checkedConfig =
- name: value:
- let
- file = pkgs.writeText name (builtins.toJSON value);
- in
- pkgs.runCommand "checked-${name}" { nativeBuildInputs = [ cfg.package ]; } ''
- ln -vs ${file} "$out"
- hakurei show --no-store ${file}
- '';
- in
- pkgs.writeShellScriptBin app.name ''
- exec hakurei${if app.verbose then " -v" else ""}${if app.insecureWayland then " --insecure" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@
- ''
- )
- ]
- ++ (
- let
- pkg = if app.share != null then app.share else pkgs.${app.name};
- copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true";
- in
- optional (app.enablements.wayland || app.enablements.x11) (
- pkgs.runCommand "${app.name}-share" { } ''
- mkdir -p $out/share
- ${copy "${pkg}/share/applications"}
- ${copy "${pkg}/share/pixmaps"}
- ${copy "${pkg}/share/icons"}
- ${copy "${pkg}/share/man"}
-
- if test -d "$out/share/applications"; then
- substituteInPlace $out/share/applications/* \
- --replace-warn '${pkg}/bin/' "" \
- --replace-warn '${pkg}/libexec/' ""
- fi
- ''
- )
- )
- ++ acc
- ) [ cfg.package ] cfg.apps;
- }) cfg.users;
- in
- {
- useUserPackages = false; # prevent users.users entries from being added
-
- users =
- mkMerge
- (foldlAttrs
- (
- acc: _: fid:
- foldlAttrs
- (
- acc: _: app:
- (
- let
- key = getsubname fid app.identity;
- in
- {
- usernames = acc.usernames // {
- ${key} = true;
- };
- merge = acc.merge ++ [
- {
- ${key} = mkMerge (
- [
- app.extraConfig
- { home.packages = app.packages; }
- ]
- ++ lib.optional (!attrsets.hasAttrByPath [ key ] acc.usernames) cfg.extraHomeConfig
- );
- }
- ];
- }
- )
- )
- {
- inherit (acc) usernames;
- merge = acc.merge ++ [ { ${getsubname fid 0} = cfg.extraHomeConfig; } ];
- }
- cfg.apps
- )
- {
- usernames = { };
- merge = [ privPackages ];
- }
- cfg.users
- ).merge;
- };
-
- users =
- let
- getuser = userid: appid: {
- isSystemUser = true;
- createHome = true;
- description = "Hakurei subordinate user ${toString appid} (u${toString userid})";
- group = getsubname userid appid;
- home = getsubhome userid appid;
- uid = getsubuid userid appid;
- };
- getgroup = userid: appid: { gid = getsubuid userid appid; };
- in
- {
- users = mkMerge (
- foldlAttrs
- (
- acc: username: fid:
- acc
- ++
- foldlAttrs
- (
- acc': _: app:
- acc' ++ [ { ${getsubname fid app.identity} = getuser fid app.identity; } ]
- )
- [
- {
- ${getsubname fid 0} = getuser fid 0;
- ${username}.extraGroups = [ cfg.sharefs.group ];
- }
- ]
- cfg.apps
- )
- (optional (cfg.sharefs.source != null) {
- ${cfg.sharefs.user} = {
- uid = lib.mkDefault 1023;
- inherit (cfg.sharefs) group;
- isSystemUser = true;
- home = cfg.sharefs.source;
- };
- })
- cfg.users
- );
-
- groups = mkMerge (
- foldlAttrs
- (
- acc: _: fid:
- acc
- ++ foldlAttrs (
- acc': _: app:
- acc' ++ [ { ${getsubname fid app.identity} = getgroup fid app.identity; } ]
- ) [ { ${getsubname fid 0} = getgroup fid 0; } ] cfg.apps
- )
- (optional (cfg.sharefs.source != null) {
- ${cfg.sharefs.group} = {
- gid = lib.mkDefault 1023;
- };
- })
- cfg.users
- );
- };
- };
-}
diff --git a/cmd/hakurei/testsuite/options.nix b/cmd/hakurei/testsuite/options.nix
deleted file mode 100644
index f624b6f5..00000000
--- a/cmd/hakurei/testsuite/options.nix
+++ /dev/null
@@ -1,364 +0,0 @@
-packages:
-{
- lib,
- pkgs,
- config,
- ...
-}:
-
-let
- inherit (lib) types mkOption mkEnableOption;
-
- cfg = config.environment.hakurei;
-in
-
-{
- options = {
- environment.hakurei = {
- enable = mkEnableOption "hakurei";
-
- package = mkOption {
- type = types.package;
- default = packages.${pkgs.stdenv.hostPlatform.system}.hakurei;
- description = "The hakurei package to use.";
- };
-
- hsuPackage = mkOption {
- type = types.package;
- default = packages.${pkgs.stdenv.hostPlatform.system}.hsu;
- description = "The hsu package to use.";
- };
-
- users = mkOption {
- type =
- let
- inherit (types) attrsOf ints;
- in
- attrsOf (ints.between 0 99);
- description = ''
- Users allowed to spawn hakurei apps and their corresponding hakurei identity.
- '';
- };
-
- extraHomeConfig = mkOption {
- type = types.anything;
- description = ''
- Extra home-manager configuration to merge with all target users.
- '';
- };
-
- sharefs = {
- package = mkOption {
- type = types.package;
- default = pkgs.linkFarm "sharefs" {
- "bin/sharefs" = "${cfg.package}/libexec/sharefs";
- "bin/mount.fuse.sharefs" = "${cfg.package}/libexec/sharefs";
- };
- description = "The sharefs package to use.";
- };
-
- user = mkOption {
- type = types.str;
- default = "sharefs";
- description = ''
- Name of the user to run the sharefs daemon as.
- '';
- };
-
- group = mkOption {
- type = types.str;
- default = "sharefs";
- description = ''
- Name of the group to run the sharefs daemon as.
- '';
- };
-
- name = mkOption {
- type = types.str;
- default = "/sdcard";
- description = ''
- Host path to mount sharefs on.
- '';
- };
-
- source = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = ''
- Writable backing directory. Setting this to null disables sharefs.
- '';
- };
- };
-
- apps = mkOption {
- type =
- let
- inherit (types)
- int
- ints
- str
- bool
- enum
- package
- anything
- submodule
- listOf
- attrsOf
- nullOr
- functionTo
- ;
- in
- attrsOf (submodule {
- options = {
- name = mkOption {
- type = str;
- description = ''
- Name of the app's launcher script.
- '';
- };
-
- verbose = mkEnableOption "launchers with verbose output";
-
- identity = mkOption {
- type = ints.between 1 9999;
- description = ''
- Application identity. Identity 0 is reserved for system services.
- '';
- };
- shareUid = mkEnableOption "sharing identity with another application";
-
- packages = mkOption {
- type = listOf package;
- default = [ ];
- description = ''
- List of extra packages to install via home-manager.
- '';
- };
-
- extraConfig = mkOption {
- type = anything;
- default = { };
- description = ''
- Extra home-manager configuration.
- '';
- };
-
- path = mkOption {
- type = nullOr str;
- default = null;
- description = ''
- Custom executable path.
- Setting this to null will default to the start script.
- '';
- };
-
- args = mkOption {
- type = nullOr (listOf str);
- default = null;
- description = ''
- Custom args.
- Setting this to null will default to script name.
- '';
- };
-
- script = mkOption {
- type = nullOr str;
- default = null;
- description = ''
- Application launch script.
- '';
- };
-
- command = mkOption {
- type = nullOr str;
- default = null;
- description = ''
- Command to run as the target user.
- Setting this to null will default command to launcher name.
- Has no effect when script is set.
- '';
- };
-
- groups = mkOption {
- type = listOf str;
- default = [ ];
- description = ''
- List of groups to inherit from the privileged user.
- '';
- };
-
- shareRuntime = mkEnableOption "sharing of XDG_RUNTIME_DIR between containers under the same identity";
- shareTmpdir = mkEnableOption "sharing of TMPDIR between containers under the same identity";
-
- dbus = {
- session = mkOption {
- type = nullOr (functionTo anything);
- default = null;
- description = ''
- D-Bus session bus custom configuration.
- Setting this to null will enable built-in defaults.
- '';
- };
-
- system = mkOption {
- type = nullOr anything;
- default = null;
- description = ''
- D-Bus system bus custom configuration.
- Setting this to null will disable the system bus proxy.
- '';
- };
- };
-
- env = mkOption {
- type = nullOr (attrsOf str);
- default = null;
- description = ''
- Environment variables to set for the initial process in the sandbox.
- '';
- };
-
- wait_delay = mkOption {
- type = nullOr int;
- default = null;
- description = ''
- Duration to wait for after interrupting a container's initial process in nanoseconds.
- A negative value causes the container to be terminated immediately on cancellation.
- Setting this to null defaults to five seconds.
- '';
- };
-
- devel = mkEnableOption "debugging-related kernel interfaces";
- userns = mkEnableOption "user namespace creation";
- tty = mkEnableOption "access to the controlling terminal";
- multiarch = mkEnableOption "multiarch kernel-level support";
-
- hostNet = mkEnableOption "share host net namespace" // {
- default = true;
- };
- hostAbstract = mkEnableOption "share abstract unix socket scope";
-
- schedPolicy = mkOption {
- type = nullOr (enum [
- "fifo"
- "rr"
- "batch"
- "idle"
- "deadline"
- "ext"
- ]);
- default = null;
- description = ''
- Scheduling policy to set for the container.
- The zero value retains the current scheduling policy.
- '';
- };
- schedPriority = mkOption {
- type = nullOr (ints.between 1 99);
- default = null;
- description = ''
- Scheduling priority to set for the container.
- '';
- };
-
- nix = mkEnableOption "nix daemon access";
- mapRealUid = mkEnableOption "mapping to priv-user uid";
- device = mkEnableOption "access to all devices";
- insecureWayland = mkEnableOption "direct access to the Wayland socket";
-
- gpu = mkOption {
- type = nullOr bool;
- default = null;
- description = ''
- Target process GPU and driver access.
- Setting this to null will enable GPU whenever X or Wayland is enabled.
- '';
- };
-
- useCommonPaths = mkEnableOption "common extra paths" // {
- default = true;
- };
-
- extraPaths = mkOption {
- type = listOf (attrsOf anything);
- default = [ ];
- description = ''
- Extra paths to make available to the container.
- '';
- };
-
- enablements = {
- wayland = mkOption {
- type = nullOr bool;
- default = true;
- description = ''
- Whether to share the Wayland server via security-context-v1.
- '';
- };
-
- x11 = mkOption {
- type = nullOr bool;
- default = false;
- description = ''
- Whether to share the X11 socket and allow connection.
- '';
- };
-
- dbus = mkOption {
- type = nullOr bool;
- default = true;
- description = ''
- Whether to proxy D-Bus.
- '';
- };
-
- pipewire = mkOption {
- type = nullOr bool;
- default = true;
- description = ''
- Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket.
- '';
- };
- };
-
- share = mkOption {
- type = nullOr package;
- default = null;
- description = ''
- Package containing share files.
- Setting this to null will default package name to wrapper name.
- '';
- };
- };
- });
- default = { };
- description = ''
- Declaratively configured hakurei apps.
- '';
- };
-
- commonPaths = mkOption {
- type = types.listOf (types.attrsOf types.anything);
- default = [ ];
- description = ''
- Common extra paths to make available to the container.
- '';
- };
-
- shell = mkOption {
- type = types.str;
- default = "/run/current-system/sw/bin/bash";
- description = ''
- Absolute path to preferred shell.
- '';
- };
-
- stateDir = mkOption {
- type = types.str;
- description = ''
- The state directory where app home directories are stored.
- '';
- };
- };
- };
-}
diff --git a/cmd/hakurei/testsuite/package.nix b/cmd/hakurei/testsuite/package.nix
deleted file mode 100644
index 12196cf6..00000000
--- a/cmd/hakurei/testsuite/package.nix
+++ /dev/null
@@ -1,145 +0,0 @@
-{
- lib,
- stdenv,
- buildGo127Module,
- makeBinaryWrapper,
- xdg-dbus-proxy,
- pkg-config,
- libffi,
- libseccomp,
- acl,
- wayland,
- wayland-protocols,
- wayland-scanner,
-
- libxcb,
- libxau,
- libxdmcp,
-
- # for sharefs
- fuse3,
-
- # for passthru.buildInputs
- go_1_27,
- clang,
- xorgproto,
-
- # for check
- util-linux,
- nettools,
-
- glibc, # for ldd
- withStatic ? stdenv.hostPlatform.isStatic,
-}:
-
-buildGo127Module rec {
- pname = "hakurei";
- version = with lib.strings; removePrefix "v" (trim (builtins.readFile ../../dist/VERSION));
-
- srcFiltered = builtins.path {
- name = "${pname}-src";
- path = lib.cleanSource ../../../.;
- filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/hsu" path);
- };
- vendorHash = null;
-
- src = stdenv.mkDerivation {
- name = "${pname}-src-full";
- inherit version;
- enableParallelBuilding = true;
- src = srcFiltered;
-
- buildInputs = [
- wayland
- wayland-protocols
- ];
-
- nativeBuildInputs = [
- go_1_27
- pkg-config
- wayland-scanner
- ];
-
- buildPhase = "GOCACHE=$(mktemp -d) go generate ./...";
- installPhase = "cp -r . $out";
- };
-
- ldflags =
- lib.attrsets.foldlAttrs
- (
- ldflags: name: value:
- ldflags ++ [ "-X hakurei.app/internal/info.${name}=${value}" ]
- )
- (
- [ "-s -w" ]
- ++ lib.optionals withStatic [
- "-linkmode external"
- "-extldflags \"-static\""
- ]
- )
- {
- buildVersion = "v${version}";
- hakureiPath = "${placeholder "out"}/libexec/hakurei";
- hsuPath = "/run/wrappers/bin/hsu";
- };
-
- env = {
- # use clang instead of gcc
- CC = "clang -O3 -Werror";
- };
-
- buildInputs = [
- libffi
- libseccomp
- fuse3
- acl
- wayland
- util-linux
-
- libxcb
- libxau
- libxdmcp
- ];
-
- nativeBuildInputs = [
- pkg-config
- makeBinaryWrapper
-
- # for container example
- nettools
- ];
-
- postInstall =
- let
- appPackages = [
- glibc
- xdg-dbus-proxy
- ];
- in
- ''
- install -D --target-directory=$out/share/zsh/site-functions cmd/dist/comp/*
-
- mkdir "$out/libexec"
- mv "$out"/bin/* "$out/libexec/"
-
- makeBinaryWrapper "$out/libexec/hakurei" "$out/bin/hakurei" \
- --inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
- '';
-
- passthru = {
- go = go_1_27;
-
- targetPkgs = [
- go_1_27
- clang
- xorgproto
- util-linux
-
- # for go generate
- wayland-protocols
- wayland-scanner
- ]
- ++ buildInputs
- ++ nativeBuildInputs;
- };
-}
diff --git a/cmd/hakurei/testsuite/test.py b/cmd/hakurei/testsuite/test.py
deleted file mode 100644
index 98f08275..00000000
--- a/cmd/hakurei/testsuite/test.py
+++ /dev/null
@@ -1,315 +0,0 @@
-import json
-import shlex
-
-q = shlex.quote
-NODE_GROUPS = ["nodes", "floating_nodes"]
-
-
-def swaymsg(command: str = "", succeed=True, type="command"):
- assert command != "" or type != "command", "Must specify command or type"
- shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
- with machine.nested(f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)):
- ret = (machine.succeed if succeed else machine.execute)(
- f"su - alice -c {shell}"
- )
-
- # execute also returns a status code, but disregard.
- if not succeed:
- _, ret = ret
-
- if not succeed and not ret:
- return None
-
- parsed = json.loads(ret)
- return parsed
-
-
-def walk(tree):
- yield tree
- for group in NODE_GROUPS:
- for node in tree.get(group, []):
- yield from walk(node)
-
-
-def wait_for_window(pattern):
- def func(last_chance):
- nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
-
- if last_chance:
- nodes = list(nodes)
- machine.log(f"Last call! Current list of windows: {nodes}")
-
- return any(pattern in name for name in nodes)
-
- retry(func)
-
-
-def collect_state_ui(name):
- swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
- machine.wait_for_file(f"/tmp/{name}.ps")
- machine.copy_from_vm(f"/tmp/{name}.ps", "")
- swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
- machine.wait_for_file(f"/tmp/{name}.json")
- machine.copy_from_vm(f"/tmp/{name}.json", "")
- machine.screenshot(name)
-
-
-def check_state(name, enablements):
- instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
- if len(instances) != 1:
- raise Exception(f"unexpected state length {len(instances)}")
- instance = instances[0]
-
- command = f"{name}-start"
- if not (instance['container']['path'].startswith("/nix/store/")) or not (instance['container']['path'].endswith(command)):
- raise Exception(f"unexpected path {instance['path']}")
-
- if len(instance['container']['args']) != 1 or instance['container']['args'][0] != command:
- raise Exception(f"unexpected args {instance['args']}")
-
- if instance['enablements'] != enablements:
- raise Exception(f"unexpected enablements {instance['enablements']['enablements']}")
-
-
-def hakurei(command):
- swaymsg(f"exec hakurei {command}")
-
-
-start_all()
-machine.wait_for_unit("multi-user.target")
-
-# To check hakurei's version:
-print(machine.succeed("sudo -u alice -i hakurei version"))
-
-# Wait for Sway to complete startup:
-machine.wait_for_file("/run/user/1000/wayland-1")
-machine.wait_for_file("/tmp/sway-ipc.sock")
-
-# Run hakurei Go tests outside of nix build in the background:
-swaymsg("exec hakurei-test")
-
-# Deny unmapped uid:
-denyOutput = machine.fail("sudo -u untrusted -i hakurei exec &>/dev/stdout")
-print(denyOutput)
-denyOutputVerbose = machine.fail("sudo -u untrusted -i hakurei -v exec &>/dev/stdout")
-print(denyOutputVerbose)
-
-# Direct hsu call:
-userid = machine.succeed("sudo -u alice -i hsu")
-if userid != "0":
- raise Exception(f"unexpected userid: {userid}")
-
-# Verify hsu fault behaviour:
-if denyOutput != "hsu: uid 1001 is not in the hsurc file\n":
- raise Exception(f"unexpected deny output:\n{denyOutput}")
-if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot retrieve user id from setuid wrapper: current user is not in the hsurc file\n":
- raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
-
-# Verify timeout behaviour:
-machine.succeed('sudo -u alice -i hakurei-check-linger-timeout > /var/tmp/linger-stdout 2> /var/tmp/linger-stderr || (cat /var/tmp/linger-stderr; false)')
-linger_stdout = machine.succeed("cat /var/tmp/linger-stdout")
-linger_stderr = machine.succeed("cat /var/tmp/linger-stderr")
-if linger_stdout != "":
- raise Exception(f"unexpected stdout: {linger_stdout}")
-if linger_stderr != "init: timeout exceeded waiting for lingering processes\n":
- raise Exception(f"unexpected stderr: {linger_stderr}")
-
-check_offset = 0
-
-
-def hakurei_identity(offset):
- return 1+check_offset+offset
-
-
-# Start hakurei permissive defaults outside Wayland session:
-print(machine.succeed("sudo -u alice -i hakurei -v exec -a 0 touch /tmp/pd-bare-ok"))
-machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-bare-ok")
-
-# Verify silent output permissive defaults:
-output = machine.succeed("sudo -u alice -i hakurei exec -a 0 true &>/dev/stdout")
-if output != "":
- raise Exception(f"unexpected output\n{output}")
-
-# Verify silent output permissive defaults signal:
-def silent_output_interrupt(flags):
- swaymsg("exec foot")
- wait_for_window("alice@machine")
- # identity 0 does not have home-manager
- machine.send_chars(f"exec hakurei exec {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n")
- machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-silent-ready")
- machine.succeed("rm /tmp/hakurei.0/tmpdir/0/pd-silent-ready")
- machine.send_key("ctrl-c")
- machine.wait_until_fails("pgrep foot")
- machine.wait_until_fails(f"pgrep -u alice -f 'hakurei exec {flags}-a 0 '")
- output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent")
- if output != "":
- raise Exception(f"unexpected output\n{output}")
-
-
-silent_output_interrupt("")
-silent_output_interrupt("--dbus ") # this one is especially painful as it maintains a helper
-silent_output_interrupt("--wayland -X --dbus --pulse ")
-
-# Verify graceful failure on bad Wayland display name:
-print(machine.fail("sudo -u alice -i hakurei -v exec --wayland true"))
-
-# Start hakurei permissive defaults within Wayland session:
-hakurei('-v exec --wayland --dbus --dbus-log notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-ok')
-machine.wait_for_file("/tmp/dbus-ok")
-collect_state_ui("dbus_notify_exited")
-# not in pid namespace, verify termination
-machine.wait_until_fails("pgrep xdg-dbus-proxy")
-machine.succeed("pkill -9 mako")
-
-# Check revert type selection:
-hakurei("-v exec --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok")
-wait_for_window("p0@machine")
-print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
-hakurei("-v exec --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok")
-wait_for_window("p1@machine")
-print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
-machine.send_chars("exit\n")
-machine.wait_for_file("/tmp/p1-exit-ok")
-# Verify acl is kept alive:
-print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
-machine.send_chars("exit\n")
-machine.wait_for_file("/tmp/p0-exit-ok")
-machine.fail("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")
-
-# Check invalid identifier fd behaviour:
-machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v run --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd')
-machine.wait_for_file("/tmp/invalid-identifier-fd")
-print(machine.succeed('grep "^hakurei: cannot write identifier: bad file descriptor$" /tmp/invalid-identifier-fd'))
-
-# Check interrupt shim behaviour:
-swaymsg("exec sh -c 'ne-foot; echo -n $? > /tmp/monitor-exit-code'")
-wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
-machine.succeed("pkill -INT -f 'hakurei -v run '")
-machine.wait_until_fails("pgrep foot")
-machine.wait_for_file("/tmp/monitor-exit-code")
-interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
-if interrupt_exit_code != 230:
- raise Exception(f"unexpected exit code {interrupt_exit_code}")
-
-# Check interrupt shim behaviour immediate termination:
-swaymsg("exec sh -c 'ne-foot-immediate; echo -n $? > /tmp/monitor-exit-code'")
-wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
-machine.succeed("pkill -INT -f 'hakurei -v run '")
-machine.wait_until_fails("pgrep foot")
-machine.wait_for_file("/tmp/monitor-exit-code")
-interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
-if interrupt_exit_code != 254:
- raise Exception(f"unexpected exit code {interrupt_exit_code}")
-
-# Check shim SIGCONT from unexpected process behaviour:
-swaymsg("exec sh -c 'ne-foot &> /tmp/shim-cont-unexpected-pid'")
-wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
-machine.succeed("pkill -CONT -f 'hakurei shim'")
-machine.succeed("pkill -INT -f 'hakurei -v run '")
-machine.wait_until_fails("pgrep foot")
-machine.wait_for_file("/tmp/shim-cont-unexpected-pid")
-print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid'))
-
-# Check setscheduler:
-sched_unset = int(machine.succeed("sudo -u alice -i hakurei -v exec cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
-if sched_unset != 0:
- raise Exception(f"unexpected unset policy: {sched_unset}")
-sched_idle = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=idle cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
-if sched_idle != 5:
- raise Exception(f"unexpected idle policy: {sched_idle}")
-sched_rr = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=rr cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
-if sched_rr != 2:
- raise Exception(f"unexpected round-robin policy: {sched_idle}")
-
-# Start app (foot) with Wayland enablement:
-swaymsg("exec ne-foot")
-wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
-machine.send_chars("clear; wayland-info && touch /var/tmp/client-ok\n")
-machine.wait_for_file("/var/tmp/client-ok")
-collect_state_ui("foot_wayland")
-check_state("ne-foot", {"wayland": True})
-# Verify lack of acl on XDG_RUNTIME_DIR:
-machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}")
-machine.send_chars("exit\n")
-machine.wait_until_fails("pgrep foot")
-machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}")
-
-# Test pipewire-pulse:
-swaymsg("exec pa-foot")
-wait_for_window(f"u0_a{hakurei_identity(1)}@machine")
-machine.send_chars("clear; pactl info && touch /var/tmp/pulse-ok\n")
-machine.wait_for_file("/var/tmp/pulse-ok")
-collect_state_ui("pulse_wayland")
-check_state("pa-foot", {"wayland": True, "pipewire": True})
-machine.fail("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
-machine.send_chars("exit\n")
-machine.wait_until_fails("pgrep foot")
-machine.wait_until_fails("pgrep -x hakurei")
-machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
-# Test PipeWire SecurityContext:
-machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl info")
-machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle")
-# Test PipeWire direct access:
-machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 pw-dump")
-machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pipewire pw-dump")
-
-# Test XWayland (foot does not support X):
-swaymsg("exec x11-alacritty")
-wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
-machine.send_chars("clear; glinfo && touch /var/tmp/x11-ok\n")
-machine.wait_for_file("/var/tmp/x11-ok")
-collect_state_ui("alacritty_x11")
-check_state("x11-alacritty", {"x11": True})
-machine.send_chars("exit\n")
-machine.wait_until_fails("pgrep alacritty")
-
-# Start app (foot) with direct Wayland access:
-swaymsg("exec da-foot")
-wait_for_window(f"u0_a{hakurei_identity(3)}@machine")
-machine.send_chars("clear; wayland-info && touch /var/tmp/direct-ok\n")
-collect_state_ui("foot_direct")
-machine.wait_for_file("/var/tmp/direct-ok")
-check_state("da-foot", {"wayland": True})
-# Verify acl on XDG_RUNTIME_DIR:
-print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(3) + 10000}"))
-machine.send_chars("exit\n")
-machine.wait_until_fails("pgrep foot")
-# Verify acl cleanup on XDG_RUNTIME_DIR:
-machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(3) + 10000}")
-
-# Test syscall filter:
-print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
-
-# Start app (foot) with Wayland enablement from a terminal:
-swaymsg("exec foot $SHELL -c '(ne-foot) & disown && exec $SHELL'")
-wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
-machine.send_chars("clear; wayland-info && touch /var/tmp/term-ok\n")
-machine.wait_for_file("/var/tmp/term-ok")
-machine.send_key("alt-h")
-machine.send_chars("clear; hakurei show $(hakurei ps --short) && touch /tmp/ps-show-ok && exec cat\n")
-machine.wait_for_file("/tmp/ps-show-ok")
-collect_state_ui("foot_wayland_term")
-check_state("ne-foot", {"wayland": True})
-machine.send_key("alt-l")
-machine.send_chars("exit\n")
-wait_for_window("alice@machine")
-machine.send_key("ctrl-c")
-machine.wait_until_fails("pgrep foot")
-
-# Exit Sway and verify process exit status 0:
-machine.wait_until_fails("pgrep -x hakurei")
-swaymsg("exit", succeed=False)
-machine.wait_for_file("/tmp/sway-exit-ok")
-
-# Print hakurei share and rundir contents:
-print(machine.succeed("find /tmp/hakurei.0 "
- + "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
- + "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
- + "-print"))
-print(machine.succeed("find /run/user/1000/hakurei"))
-machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
-
-# Verify go test status:
-machine.wait_for_file("/tmp/hakurei-test-done")
-print(machine.succeed("cat /tmp/hakurei-test.log"))
-machine.wait_for_file("/tmp/hakurei-test-ok")
diff --git a/internal/testsuite/testsuite.go b/internal/testsuite/testsuite.go
index ff6c4420..c4ca23e9 100644
--- a/internal/testsuite/testsuite.go
+++ b/internal/testsuite/testsuite.go
@@ -19,6 +19,7 @@ import (
"os/signal"
"os/user"
"path/filepath"
+ "reflect"
"slices"
"strconv"
"strings"
@@ -263,6 +264,42 @@ func FilterTerminated(err error) error {
return e
}
+// MustDecodeFrom runs the specified command and decodes its JSON output,
+// storing the result in the value pointed to by v.
+func MustDecodeFrom[T any](
+ ctx context.Context,
+ cred *syscall.Credential,
+ env []string,
+ v *T,
+ name string,
+ arg ...string,
+) {
+ cmd := exec.CommandContext(ctx, name, arg...)
+ cmd.Env = env
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGKILL,
+ Credential: cred,
+ }
+ cmd.Stderr = os.Stderr
+
+ r, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if err = cmd.Start(); err != nil {
+ log.Fatalf("start %s: %v", name, err)
+ }
+
+ if err = json.NewDecoder(r).Decode(v); err != nil {
+ log.Fatalf("decode %s: %v", name, err)
+ }
+
+ if err = cmd.Wait(); err != nil {
+ log.Fatalf("wait for %s: %v", name, err)
+ }
+}
+
// Poll repeatedly runs command until it succeeds.
func Poll(
d time.Duration,
@@ -473,34 +510,13 @@ func PollSwayWindow(
cred *syscall.Credential,
f func(n *SwayNode) bool,
) *SwayNode {
+ var t SwayNode
for {
time.Sleep(25 * time.Millisecond)
- cmd := exec.CommandContext(ctx, "swaymsg", "-t", "get_tree")
- cmd.Env = []string{SwayEnv}
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Pdeathsig: syscall.SIGKILL,
- Credential: cred,
- }
- cmd.Stderr = os.Stderr
-
- r, err := cmd.StdoutPipe()
- if err != nil {
- log.Fatal(err)
- }
-
- if err = cmd.Start(); err != nil {
- log.Fatalf("poll swaymsg: %v", err)
- }
-
- var t SwayNode
- if err = json.NewDecoder(r).Decode(&t); err != nil {
- log.Fatalf("decode swaymsg: %v", err)
- }
-
- if err = cmd.Wait(); err != nil {
- log.Fatalf("wait for swaymsg: %v", err)
- }
-
+ MustDecodeFrom(
+ ctx, cred, []string{SwayEnv}, &t,
+ "swaymsg", "-t", "get_tree",
+ )
for n := range t.All() {
if f(n) {
return n
@@ -701,6 +717,21 @@ func StartHakurei(
return
}
+ var identifiers []hst.ID
+ MustDecodeFrom(
+ ctx, cred, nil, &identifiers,
+ "hakurei", "--json", "ps", "--short",
+ )
+ if !slices.Contains(identifiers, id) {
+ log.Fatal("instance not present in ps output")
+ }
+
+ var got hst.State
+ MustDecodeFrom(
+ ctx, cred, nil, &got,
+ "hakurei", "--json", "show", id.String(),
+ )
+
var ok bool
entries, copyError := s.All()
for entry := range entries {
@@ -720,6 +751,10 @@ func StartHakurei(
return
}
+ if !reflect.DeepEqual(got, p) {
+ log.Fatalf("show: %#v, want %#v", got, p)
+ }
+
var stat syscall.Stat_t
pid = MustScanFor(func(ps *StatScanner) bool {
select {
diff --git a/internal/workflows/Containerfile b/internal/workflows/Containerfile
index eb858831..82ce8820 100644
--- a/internal/workflows/Containerfile
+++ b/internal/workflows/Containerfile
@@ -8,13 +8,16 @@ RUN add-apt-repository -y ppa:savoury1/pipewire && \
acl \
sway \
xwayland \
+ x11-xserver-utils \
xdg-dbus-proxy \
+ libnotify-bin \
pipewire \
pipewire-pulse \
wireplumber- \
pulseaudio-utils \
foot \
xterm \
+ grim \
fuse3 \
fsmark && \
apt-get clean && rm -rf /var/lib/apt/lists/*
diff --git a/internal/workflows/doc.go b/internal/workflows/doc.go
index 123bf7c1..e220ed67 100644
--- a/internal/workflows/doc.go
+++ b/internal/workflows/doc.go
@@ -38,6 +38,9 @@ On Alpine Linux, this is achieved by:
# Use any other user for rootless mode.
podman_user="act_runner"
+ # Required by cmd/hakurei test suite.
+ rc_ulimit="-r 49"
+
A symlink must then be created at /var/run/docker.sock pointing to the socket
owned by the unprivileged daemon. This should be done in a /etc/local.d script.
diff --git a/internal/workflows/step.go b/internal/workflows/step.go
index 5175d4d5..3458c0ea 100644
--- a/internal/workflows/step.go
+++ b/internal/workflows/step.go
@@ -58,15 +58,3 @@ func newTestsuite(name, prefix string) Step {
Run: prefix + "go run -tags=testsuite ./cmd/" + name,
}
}
-
-// newNixOSTest returns a step for running the named NixOS test.
-func newNixOSTest(name string) Step {
- return Step{
- Name: "Run NixOS test",
- Run: "nix build " +
- "--out-link result " +
- "--print-out-paths " +
- "--print-build-logs " +
- "./cmd/hakurei/testsuite#checks.x86_64-linux." + name,
- }
-}
diff --git a/internal/workflows/test.go b/internal/workflows/test.go
index 5ca795c6..b0819916 100644
--- a/internal/workflows/test.go
+++ b/internal/workflows/test.go
@@ -7,25 +7,27 @@ var _ = (&Workflow{
On: []any{"push"},
Jobs: Map[Job]{
- {"hakurei-legacy", Job{
- Name: "Hakurei (legacy)",
- On: "nix",
+ {"hakurei", Job{
+ Name: "Hakurei",
+ On: "rosa",
Steps: []Step{
checkout,
- newNixOSTest("hakurei"),
- newUploadArtifact("test output", "hakurei-vm-output"),
+ dist,
+ install,
+ newTestsuite("hakurei/testsuite", "unshare -n "),
},
}},
- {"hakurei-race-legacy", Job{
- Name: "Hakurei (legacy with race instrument)",
- On: "nix",
+ {"hakurei-race", Job{
+ Name: "Hakurei (with race instrument)",
+ On: "rosa",
Steps: []Step{
checkout,
- newNixOSTest("race"),
- newUploadArtifact("test output", "hakurei-race-vm-output"),
+ _dist,
+ install,
+ newTestsuite("hakurei/testsuite", "unshare -n "),
},
}},