aboutsummaryrefslogtreecommitdiffhomepage
path: root/test
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-07-07 18:32:56 +0900
committerOphestra <cat@gensokyo.uk>2026-07-07 18:56:27 +0900
commit6d55ee536e902d059380f79e870d06547627cc7c (patch)
treeaacf3e3ec42525cf0e7a674aa948359fd0c21ae2 /test
parentc8e8651694415d497b0800319b2ddda361b7651f (diff)
test: move nix files
These are too much clutter. Move them to test directory until the test suite replacement is upstreamed. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'test')
-rw-r--r--test/default.nix2
-rw-r--r--test/flake.lock49
-rw-r--r--test/flake.nix255
-rw-r--r--test/hsu.nix23
-rw-r--r--test/interactive/raceattr.nix2
-rw-r--r--test/nixos.nix407
-rw-r--r--test/options.md1175
-rw-r--r--test/options.nix364
-rw-r--r--test/package.nix144
-rw-r--r--test/sandbox/default.nix2
-rw-r--r--test/sharefs/configuration.nix44
-rw-r--r--test/sharefs/default.nix44
-rw-r--r--test/sharefs/raceattr.go122
-rw-r--r--test/sharefs/test.py60
14 files changed, 2690 insertions, 3 deletions
diff --git a/test/default.nix b/test/default.nix
index 76a490a7..81daa0a2 100644
--- a/test/default.nix
+++ b/test/default.nix
@@ -54,7 +54,7 @@ testers.nixosTest {
# 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: {
+ package = (pkgs.callPackage ./package.nix { }).overrideAttrs (previousAttrs: {
env = previousAttrs.env // {
GOFLAGS = previousAttrs.env.GOFLAGS + " -race";
};
diff --git a/test/flake.lock b/test/flake.lock
new file mode 100644
index 00000000..ce9fe45f
--- /dev/null
+++ b/test/flake.lock
@@ -0,0 +1,49 @@
+{
+ "nodes": {
+ "home-manager": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1780361225,
+ "narHash": "sha256-wnV9ttf4fPWNonBIQmvlrSlNpQYgx5HgWWd007mwIFA=",
+ "owner": "nix-community",
+ "repo": "home-manager",
+ "rev": "e28654b71096e08c019d4861ca26acb646f583d8",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "ref": "release-26.05",
+ "repo": "home-manager",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1780453794,
+ "narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338",
+ "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/test/flake.nix b/test/flake.nix
new file mode 100644
index 00000000..6c8cf100
--- /dev/null
+++ b/test/flake.nix
@@ -0,0 +1,255 @@
+{
+ 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 = [
+ "aarch64-linux"
+ "i686-linux"
+ "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)
+ runCommandLocal
+ callPackage
+ nixfmt
+ deadnix
+ statix
+ ;
+ in
+ {
+ hakurei = callPackage ./. { inherit system self; };
+ race = callPackage ./. {
+ inherit system self;
+ withRace = true;
+ };
+
+ sandbox = callPackage ./sandbox { inherit self; };
+ sandbox-race = callPackage ./sandbox {
+ inherit self;
+ withRace = true;
+ };
+
+ sharefs = callPackage ./sharefs { inherit system self; };
+
+ formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt ]; } ''
+ cd ${./.}
+
+ echo "running nixfmt..."
+ nixfmt --width=256 --check .
+
+ touch $out
+ '';
+
+ lint =
+ runCommandLocal "check-lint"
+ {
+ nativeBuildInputs = [
+ deadnix
+ statix
+ ];
+ }
+ ''
+ cd ${./.}
+
+ echo "running deadnix..."
+ deadnix --fail
+
+ echo "running statix..."
+ statix check .
+
+ touch $out
+ '';
+ }
+ );
+
+ 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_26
+ clang
+
+ # nativeBuildInputs
+ pkg-config
+ wayland-scanner
+ makeBinaryWrapper
+
+ # appPackages
+ glibc
+ xdg-dbus-proxy
+
+ # for check
+ util-linux
+ nettools
+ ;
+ };
+ hsu = pkgs.callPackage ./hsu.nix { inherit (self.packages.${system}) hakurei; };
+ sharefs = pkgs.linkFarm "sharefs" {
+ "bin/sharefs" = "${hakurei}/libexec/sharefs";
+ "bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs";
+ };
+
+ dist =
+ pkgs.runCommand "${hakurei.name}-dist"
+ {
+ buildInputs = hakurei.targetPkgs ++ [
+ pkgs.pkgsStatic.musl
+ ];
+ }
+ ''
+ cd $(mktemp -d) \
+ && cp -r ${hakurei.src}/. . \
+ && chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \
+ && chmod -R +w .
+
+ CC="musl-clang -O3 -Werror -Qunused-arguments" \
+ GOCACHE="$(mktemp -d)" \
+ PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
+ DESTDIR="$out" \
+ ./all.sh
+ '';
+ }
+ );
+
+ devShells = forAllSystems (
+ system:
+ let
+ inherit (self.packages.${system}) hakurei;
+ pkgs = nixpkgsFor.${system};
+ in
+ {
+ default = pkgs.mkShell {
+ buildInputs = hakurei.targetPkgs;
+ hardeningDisable = [ "fortify" ];
+ };
+ withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
+
+ vm =
+ let
+ nixos = nixpkgs.lib.nixosSystem {
+ inherit system;
+ modules = [
+ {
+ environment = {
+ systemPackages = [
+ (pkgs.buildFHSEnv {
+ pname = "hakurei-fhs";
+ inherit (hakurei) version;
+ targetPkgs = _: hakurei.targetPkgs;
+ extraOutputsToInstall = [ "dev" ];
+ profile = ''
+ export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
+ '';
+ })
+ ];
+
+ hakurei =
+ let
+ # this is used for interactive vm testing during development, where tests might be broken
+ package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override {
+ buildGo126Module = previousArgs: pkgs.pkgsStatic.buildGo126Module (previousArgs // { doCheck = false; });
+ };
+ in
+ {
+ inherit package;
+ hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; };
+ };
+ };
+ }
+
+ ./interactive/configuration.nix
+ ./interactive/vm.nix
+ ./interactive/hakurei.nix
+ ./interactive/trace.nix
+ ./interactive/raceattr.nix
+
+ self.nixosModules.hakurei
+ home-manager.nixosModules.home-manager
+ ];
+ };
+ in
+ pkgs.mkShell {
+ buildInputs = [ nixos.config.system.build.vm ];
+ shellHook = "exec run-nixos-vm $@";
+ };
+
+ generateDoc =
+ let
+ inherit (pkgs) lib;
+
+ doc =
+ let
+ eval = lib.evalModules {
+ specialArgs = {
+ inherit pkgs;
+ };
+ modules = [ (import ./options.nix self.packages) ];
+ };
+ cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
+ in
+ pkgs.nixosOptionsDoc { inherit (cleanEval) options; };
+ docText = pkgs.runCommand "hakurei-module-docs.md" { } ''
+ cat ${doc.optionsCommonMark} > $out
+ sed -i '/*Declared by:*/,+1 d' $out
+ '';
+ in
+ pkgs.mkShell {
+ shellHook = ''
+ exec cat ${docText} > options.md
+ '';
+ };
+
+ generateSyscallTable =
+ let
+ GOARCH = {
+ x86_64-linux = "amd64";
+ aarch64-linux = "arm64";
+ };
+ in
+ pkgs.mkShell {
+ shellHook = "exec ${pkgs.writeShellScript "generate-syscall-table" ''
+ set -e
+ ${pkgs.perl}/bin/perl \
+ container/std/mksysnum_linux.pl \
+ ${pkgs.linuxHeaders}/include/asm/unistd_64.h | \
+ ${pkgs.go}/bin/gofmt > \
+ container/std/syscall_linux_${GOARCH.${system}}.go
+ ''}";
+ };
+ }
+ );
+ };
+}
diff --git a/test/hsu.nix b/test/hsu.nix
new file mode 100644
index 00000000..7acb3529
--- /dev/null
+++ b/test/hsu.nix
@@ -0,0 +1,23 @@
+{
+ lib,
+ buildGoModule,
+ hakurei ? abort "hakurei package required",
+}:
+
+buildGoModule {
+ pname = "${hakurei.pname}-hsu";
+ inherit (hakurei) version;
+
+ src = ../cmd/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/test/interactive/raceattr.nix b/test/interactive/raceattr.nix
index d94d7a7b..de54f4cf 100644
--- a/test/interactive/raceattr.nix
+++ b/test/interactive/raceattr.nix
@@ -16,7 +16,7 @@
src = builtins.path {
name = "${pname}-src";
- path = lib.cleanSource ../../cmd/sharefs/test;
+ path = lib.cleanSource ../sharefs;
filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path);
};
vendorHash = null;
diff --git a/test/nixos.nix b/test/nixos.nix
new file mode 100644
index 00000000..49bfffb6
--- /dev/null
+++ b/test/nixos.nix
@@ -0,0 +1,407 @@
+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/test/options.md b/test/options.md
new file mode 100644
index 00000000..6f6acddd
--- /dev/null
+++ b/test/options.md
@@ -0,0 +1,1175 @@
+## environment\.hakurei\.enable
+
+
+
+Whether to enable hakurei\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.package
+
+
+
+The hakurei package to use\.
+
+
+
+*Type:*
+package
+
+
+
+*Default:*
+
+```nix
+<derivation hakurei-static-x86_64-unknown-linux-musl-0.4.3>
+```
+
+
+
+## environment\.hakurei\.apps
+
+Declaratively configured hakurei apps\.
+
+
+
+*Type:*
+attribute set of (submodule)
+
+
+
+*Default:*
+
+```nix
+{ }
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.enablements\.dbus
+
+
+
+Whether to proxy D-Bus\.
+
+
+
+*Type:*
+null or boolean
+
+
+
+*Default:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.enablements\.pipewire
+
+
+
+Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket\.
+
+
+
+*Type:*
+null or boolean
+
+
+
+*Default:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.enablements\.wayland
+
+
+
+Whether to share the Wayland server via security-context-v1\.
+
+
+
+*Type:*
+null or boolean
+
+
+
+*Default:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.enablements\.x11
+
+
+
+Whether to share the X11 socket and allow connection\.
+
+
+
+*Type:*
+null or boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.packages
+
+
+
+List of extra packages to install via home-manager\.
+
+
+
+*Type:*
+list of package
+
+
+
+*Default:*
+
+```nix
+[ ]
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.args
+
+
+
+Custom args\.
+Setting this to null will default to script name\.
+
+
+
+*Type:*
+null or (list of string)
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.command
+
+
+
+Command to run as the target user\.
+Setting this to null will default command to launcher name\.
+Has no effect when script is set\.
+
+
+
+*Type:*
+null or string
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.dbus\.session
+
+
+
+D-Bus session bus custom configuration\.
+Setting this to null will enable built-in defaults\.
+
+
+
+*Type:*
+null or (function that evaluates to a(n) anything)
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.dbus\.system
+
+
+
+D-Bus system bus custom configuration\.
+Setting this to null will disable the system bus proxy\.
+
+
+
+*Type:*
+null or anything
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.devel
+
+
+
+Whether to enable debugging-related kernel interfaces\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.device
+
+
+
+Whether to enable access to all devices\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.env
+
+
+
+Environment variables to set for the initial process in the sandbox\.
+
+
+
+*Type:*
+null or (attribute set of string)
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.extraConfig
+
+
+
+Extra home-manager configuration\.
+
+
+
+*Type:*
+anything
+
+
+
+*Default:*
+
+```nix
+{ }
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.extraPaths
+
+
+
+Extra paths to make available to the container\.
+
+
+
+*Type:*
+list of attribute set of anything
+
+
+
+*Default:*
+
+```nix
+[ ]
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.gpu
+
+
+
+Target process GPU and driver access\.
+Setting this to null will enable GPU whenever X or Wayland is enabled\.
+
+
+
+*Type:*
+null or boolean
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.groups
+
+
+
+List of groups to inherit from the privileged user\.
+
+
+
+*Type:*
+list of string
+
+
+
+*Default:*
+
+```nix
+[ ]
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.hostAbstract
+
+
+
+Whether to enable share abstract unix socket scope\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.hostNet
+
+
+
+Whether to enable share host net namespace\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+true
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.identity
+
+
+
+Application identity\. Identity 0 is reserved for system services\.
+
+
+
+*Type:*
+integer between 1 and 9999 (both inclusive)
+
+
+
+## environment\.hakurei\.apps\.\<name>\.insecureWayland
+
+
+
+Whether to enable direct access to the Wayland socket\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.mapRealUid
+
+
+
+Whether to enable mapping to priv-user uid\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.multiarch
+
+
+
+Whether to enable multiarch kernel-level support\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.name
+
+
+
+Name of the app’s launcher script\.
+
+
+
+*Type:*
+string
+
+
+
+## environment\.hakurei\.apps\.\<name>\.nix
+
+
+
+Whether to enable nix daemon access\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.path
+
+
+
+Custom executable path\.
+Setting this to null will default to the start script\.
+
+
+
+*Type:*
+null or string
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.schedPolicy
+
+
+
+Scheduling policy to set for the container\.
+The zero value retains the current scheduling policy\.
+
+
+
+*Type:*
+null or one of “fifo”, “rr”, “batch”, “idle”, “deadline”, “ext”
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.schedPriority
+
+
+
+Scheduling priority to set for the container\.
+
+
+
+*Type:*
+null or integer between 1 and 99 (both inclusive)
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.script
+
+
+
+Application launch script\.
+
+
+
+*Type:*
+null or string
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.share
+
+
+
+Package containing share files\.
+Setting this to null will default package name to wrapper name\.
+
+
+
+*Type:*
+null or package
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.shareRuntime
+
+
+
+Whether to enable sharing of XDG_RUNTIME_DIR between containers under the same identity\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.shareTmpdir
+
+
+
+Whether to enable sharing of TMPDIR between containers under the same identity\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.shareUid
+
+
+
+Whether to enable sharing identity with another application\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.tty
+
+
+
+Whether to enable access to the controlling terminal\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.useCommonPaths
+
+
+
+Whether to enable common extra paths\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+true
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.userns
+
+
+
+Whether to enable user namespace creation\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.verbose
+
+
+
+Whether to enable launchers with verbose output\.
+
+
+
+*Type:*
+boolean
+
+
+
+*Default:*
+
+```nix
+false
+```
+
+
+
+*Example:*
+
+```nix
+true
+```
+
+
+
+## environment\.hakurei\.apps\.\<name>\.wait_delay
+
+
+
+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\.
+
+
+
+*Type:*
+null or signed integer
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.commonPaths
+
+
+
+Common extra paths to make available to the container\.
+
+
+
+*Type:*
+list of attribute set of anything
+
+
+
+*Default:*
+
+```nix
+[ ]
+```
+
+
+
+## environment\.hakurei\.extraHomeConfig
+
+
+
+Extra home-manager configuration to merge with all target users\.
+
+
+
+*Type:*
+anything
+
+
+
+## environment\.hakurei\.hsuPackage
+
+
+
+The hsu package to use\.
+
+
+
+*Type:*
+package
+
+
+
+*Default:*
+
+```nix
+<derivation hakurei-hsu-0.4.3>
+```
+
+
+
+## environment\.hakurei\.sharefs\.package
+
+
+
+The sharefs package to use\.
+
+
+
+*Type:*
+package
+
+
+
+*Default:*
+
+```nix
+<derivation sharefs>
+```
+
+
+
+## environment\.hakurei\.sharefs\.group
+
+
+
+Name of the group to run the sharefs daemon as\.
+
+
+
+*Type:*
+string
+
+
+
+*Default:*
+
+```nix
+"sharefs"
+```
+
+
+
+## environment\.hakurei\.sharefs\.name
+
+
+
+Host path to mount sharefs on\.
+
+
+
+*Type:*
+string
+
+
+
+*Default:*
+
+```nix
+"/sdcard"
+```
+
+
+
+## environment\.hakurei\.sharefs\.source
+
+
+
+Writable backing directory\. Setting this to null disables sharefs\.
+
+
+
+*Type:*
+null or string
+
+
+
+*Default:*
+
+```nix
+null
+```
+
+
+
+## environment\.hakurei\.sharefs\.user
+
+
+
+Name of the user to run the sharefs daemon as\.
+
+
+
+*Type:*
+string
+
+
+
+*Default:*
+
+```nix
+"sharefs"
+```
+
+
+
+## environment\.hakurei\.shell
+
+
+
+Absolute path to preferred shell\.
+
+
+
+*Type:*
+string
+
+
+
+*Default:*
+
+```nix
+"/run/current-system/sw/bin/bash"
+```
+
+
+
+## environment\.hakurei\.stateDir
+
+
+
+The state directory where app home directories are stored\.
+
+
+
+*Type:*
+string
+
+
+
+## environment\.hakurei\.users
+
+
+
+Users allowed to spawn hakurei apps and their corresponding hakurei identity\.
+
+
+
+*Type:*
+attribute set of integer between 0 and 99 (both inclusive)
+
+
+
+*Default:*
+
+```nix
+{ }
+```
+
+
diff --git a/test/options.nix b/test/options.nix
new file mode 100644
index 00000000..f624b6f5
--- /dev/null
+++ b/test/options.nix
@@ -0,0 +1,364 @@
+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/test/package.nix b/test/package.nix
new file mode 100644
index 00000000..46567b69
--- /dev/null
+++ b/test/package.nix
@@ -0,0 +1,144 @@
+{
+ lib,
+ stdenv,
+ buildGo126Module,
+ 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_26,
+ clang,
+ xorgproto,
+
+ # for check
+ util-linux,
+ nettools,
+
+ glibc, # for ldd
+ withStatic ? stdenv.hostPlatform.isStatic,
+}:
+
+buildGo126Module rec {
+ pname = "hakurei";
+ version = with lib.strings; removePrefix "v" (trim (builtins.readFile ../cmd/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_26
+ 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
+
+ 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_26;
+
+ targetPkgs = [
+ go_1_26
+ clang
+ xorgproto
+ util-linux
+
+ # for go generate
+ wayland-protocols
+ wayland-scanner
+ ]
+ ++ buildInputs
+ ++ nativeBuildInputs;
+ };
+}
diff --git a/test/sandbox/default.nix b/test/sandbox/default.nix
index a0b8d13a..47a59de9 100644
--- a/test/sandbox/default.nix
+++ b/test/sandbox/default.nix
@@ -14,7 +14,7 @@ testers.nixosTest {
# 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: {
+ package = (pkgs.callPackage ../package.nix { }).overrideAttrs (previousAttrs: {
env = previousAttrs.env // {
GOFLAGS = previousAttrs.env.GOFLAGS + " -race";
};
diff --git a/test/sharefs/configuration.nix b/test/sharefs/configuration.nix
new file mode 100644
index 00000000..05d67dcb
--- /dev/null
+++ b/test/sharefs/configuration.nix
@@ -0,0 +1,44 @@
+{ pkgs, ... }:
+{
+ users.users = {
+ alice = {
+ isNormalUser = true;
+ description = "Alice Foobar";
+ password = "foobar";
+ uid = 1000;
+ };
+ };
+
+ home-manager.users.alice.home.stateVersion = "24.11";
+
+ # Automatically login on tty1 as a normal user:
+ services.getty.autologinUser = "alice";
+
+ environment = {
+ # For benchmarking sharefs:
+ systemPackages = [ pkgs.fsmark ];
+ };
+
+ virtualisation = {
+ # Hopefully reduces spurious test failures:
+ memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192;
+
+ diskSize = 6 * 1024;
+
+ qemu.options = [
+ # Increase test performance:
+ "-smp 16"
+ ];
+ };
+
+ environment.hakurei = rec {
+ enable = true;
+ stateDir = "/var/lib/hakurei";
+ sharefs.source = "${stateDir}/sdcard";
+ users.alice = 0;
+
+ extraHomeConfig = {
+ home.stateVersion = "23.05";
+ };
+ };
+}
diff --git a/test/sharefs/default.nix b/test/sharefs/default.nix
new file mode 100644
index 00000000..e963eaa6
--- /dev/null
+++ b/test/sharefs/default.nix
@@ -0,0 +1,44 @@
+{
+ testers,
+
+ system,
+ self,
+}:
+testers.nixosTest {
+ name = "sharefs";
+ nodes.machine =
+ { options, pkgs, ... }:
+ let
+ fhs =
+ let
+ hakurei = options.environment.hakurei.package.default;
+ in
+ pkgs.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:
+ (pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
+ cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
+ ${fhs}/bin/hakurei-fhs -c 'ROSA_SKIP_BINFMT=1 CC="clang -O3 -Werror" go test ./...'
+ '')
+ ];
+
+ imports = [
+ ./configuration.nix
+
+ self.nixosModules.hakurei
+ self.inputs.home-manager.nixosModules.home-manager
+ ];
+ };
+
+ testScript = builtins.readFile ./test.py;
+}
diff --git a/test/sharefs/raceattr.go b/test/sharefs/raceattr.go
new file mode 100644
index 00000000..412cb2b3
--- /dev/null
+++ b/test/sharefs/raceattr.go
@@ -0,0 +1,122 @@
+//go:build raceattr
+
+// The raceattr program reproduces vfs inode file attribute race.
+//
+// Even though libfuse high-level API presents the address of a struct stat
+// alongside struct fuse_context, file attributes are actually inherent to the
+// inode, instead of the specific call from userspace. The kernel implementation
+// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
+// call) impossible or very unlikely to reach userspace via the stat family of
+// syscalls. However, when using default_permissions to have the VFS check
+// permissions, this race still happens, despite the resulting struct stat being
+// correct when overriding the check via capabilities otherwise.
+//
+// This program reproduces the failure, but because of its continuous nature, it
+// is provided independent of the vm integration test suite.
+package main
+
+import (
+ "context"
+ "flag"
+ "log"
+ "os"
+ "os/signal"
+ "runtime"
+ "sync"
+ "sync/atomic"
+ "syscall"
+)
+
+func newStatAs(
+ ctx context.Context, cancel context.CancelFunc,
+ n *atomic.Uint64, ok *atomic.Bool,
+ uid uint32, pathname string,
+ continuous bool,
+) func() {
+ return func() {
+ runtime.LockOSThread()
+ defer cancel()
+
+ if _, _, errno := syscall.Syscall(
+ syscall.SYS_SETUID, uintptr(uid),
+ 0, 0,
+ ); errno != 0 {
+ cancel()
+ log.Printf("cannot set uid to %d: %s", uid, errno)
+ }
+
+ var stat syscall.Stat_t
+ for {
+ if ctx.Err() != nil {
+ return
+ }
+
+ if err := syscall.Lstat(pathname, &stat); err != nil {
+ // SHAREFS_PERM_DIR not world executable, or
+ // SHAREFS_PERM_REG not world readable
+ if !continuous {
+ cancel()
+ }
+ ok.Store(true)
+ log.Printf("uid %d: %v", uid, err)
+ } else if stat.Uid != uid {
+ // appears to be unreachable
+ if !continuous {
+ cancel()
+ }
+ ok.Store(true)
+ log.Printf("got uid %d instead of %d", stat.Uid, uid)
+ }
+ n.Add(1)
+ }
+ }
+}
+
+func main() {
+ log.SetFlags(0)
+ log.SetPrefix("raceattr: ")
+
+ p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
+ u0 := flag.Int("uid0", 1<<10-1, "first uid")
+ u1 := flag.Int("uid1", 1<<10-2, "second uid")
+ count := flag.Int("count", 1, "threads per uid")
+ continuous := flag.Bool("continuous", false, "keep running even after reproduce")
+ flag.Parse()
+
+ if os.Geteuid() != 0 {
+ log.Fatal("this program must run as root")
+ }
+
+ ctx, cancel := signal.NotifyContext(
+ context.Background(),
+ syscall.SIGINT,
+ syscall.SIGTERM,
+ syscall.SIGHUP,
+ )
+
+ if err := os.WriteFile(*p, nil, 0); err != nil {
+ log.Fatal(err)
+ }
+
+ var (
+ wg sync.WaitGroup
+
+ n atomic.Uint64
+ ok atomic.Bool
+ )
+
+ if *count < 1 {
+ *count = 1
+ }
+ for range *count {
+ wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
+ if *u1 >= 0 {
+ wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
+ }
+ }
+
+ wg.Wait()
+ if !*continuous && ok.Load() {
+ log.Printf("reproduced after %d calls", n.Load())
+ }
+}
diff --git a/test/sharefs/test.py b/test/sharefs/test.py
new file mode 100644
index 00000000..4b925c9c
--- /dev/null
+++ b/test/sharefs/test.py
@@ -0,0 +1,60 @@
+start_all()
+machine.wait_for_unit("multi-user.target")
+
+# To check sharefs version:
+print(machine.succeed("sharefs -V"))
+
+# Make sure sharefs started:
+machine.wait_for_unit("sdcard.mount")
+
+machine.succeed("mkdir /mnt")
+def check_bad_opts_output(opts, want, source="/etc", privileged=False):
+ output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
+ if output != want:
+ raise Exception(f"unexpected output: {output}")
+
+# Malformed setuid/setgid representation:
+check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n")
+check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n")
+
+# Bounds check for setuid/setgid:
+check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n")
+check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n")
+check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n")
+check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
+
+# Non-root setuid/setgid:
+check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
+check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
+check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
+check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
+
+# Starting as root without setuid/setgid:
+check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
+check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
+check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
+
+# Make sure nothing actually got mounted:
+machine.fail("umount /mnt")
+machine.succeed("rmdir /mnt")
+
+# Unprivileged mount/unmount:
+machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
+machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
+machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
+machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
+machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
+machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
+
+# Benchmark sharefs:
+machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
+machine.copy_from_vm("/tmp/fs_log.txt", "")
+
+# Check permissions:
+machine.succeed("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check")
+machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check")
+machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark")
+machine.fail("ls /var/lib/hakurei/sdcard/fs_mark")
+
+# Run hakurei tests on sharefs:
+machine.succeed("sudo -u alice -i sharefs-workload-hakurei-tests")