test: move nix files
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 2m53s
Test / ShareFS (push) Successful in 3m51s
Test / Hakurei (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 5m29s
Test / Hakurei (race detector) (push) Successful in 6m41s
Test / Flake checks (push) Successful in 1m11s

These are too much clutter. Move them to test directory until the test suite replacement is upstreamed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-07-07 18:32:56 +09:00
parent c8e8651694
commit 6d55ee536e
17 changed files with 28 additions and 33 deletions
+1 -1
View File
@@ -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";
};
+49
View File
@@ -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
}
+255
View File
@@ -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
''}";
};
}
);
};
}
+23
View File
@@ -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"; };
}
+1 -1
View File
@@ -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;
+407
View File
@@ -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
);
};
};
}
+1175
View File
File diff suppressed because it is too large Load Diff
+364
View File
@@ -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.
'';
};
};
};
}
+144
View File
@@ -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;
};
}
+1 -1
View File
@@ -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";
};
+44
View File
@@ -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";
};
};
}
+44
View File
@@ -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;
}
+122
View File
@@ -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())
}
}
+60
View File
@@ -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")