note that this completely breaks the systemd backend (though easily fixable if wanted)
232 lines
6.8 KiB
Nix
232 lines
6.8 KiB
Nix
{ lib, pkgs, ... }:
|
|
let
|
|
logBase = "$HOME/.local/state/s6/logs";
|
|
maybe = cond: value: if cond then value else null;
|
|
normalizeName = name: lib.removeSuffix ".service" (lib.removeSuffix ".target" name);
|
|
|
|
# create a derivation whose output is the on-disk representation of some attrset.
|
|
# @path: /foo/bar/...
|
|
# @obj: one of:
|
|
# - { file = { text = "foo bar"; executable = true|false; } }
|
|
# - { dir = <obj>; }
|
|
fsItemToDerivation = path: obj: if obj ? dir then
|
|
pkgs.symlinkJoin {
|
|
name = "s6-${path}";
|
|
paths = lib.mapAttrsToList
|
|
(n: v: fsItemToDerivation "${path}/${n}" v)
|
|
obj.dir
|
|
;
|
|
}
|
|
else if obj ? text then
|
|
if obj.text != null then
|
|
pkgs.writeTextFile {
|
|
name = "s6-${path}";
|
|
destination = path;
|
|
text = obj.text;
|
|
}
|
|
else
|
|
pkgs.emptyDirectory
|
|
else if obj ? executable then
|
|
if obj.executable != null then
|
|
pkgs.writeTextFile {
|
|
name = "s6-${path}";
|
|
destination = path;
|
|
executable = true;
|
|
text = obj.executable;
|
|
}
|
|
else
|
|
pkgs.emptyDirectory
|
|
else throw "don't know how to convert fs item at path ${path} to derivation: ${obj}";
|
|
|
|
# call with an AttrSet of fs items:
|
|
# example:
|
|
# ```
|
|
# fsToDerivation {
|
|
# usr.dir = {
|
|
# normal.text = "i'm /usr/normal";
|
|
# exec.executable = ''
|
|
# #!/bin/sh
|
|
# echo "i'm executable"
|
|
# '';
|
|
# lib.dir = { ... };
|
|
# };
|
|
# bin.dir = { ... };
|
|
# }
|
|
fsToDerivation = fs: fsItemToDerivation "/" { dir = fs; };
|
|
|
|
# infers the service type from the arguments and creates an attrset usable by `fsToDerivation`.
|
|
# also configures the service for logging, if applicable.
|
|
serviceToFs = { name, run, finish, depends }: let
|
|
name' = normalizeName name;
|
|
type = if run != null then "longrun" else "bundle";
|
|
logger = serviceToFs' {
|
|
name = "logger:${name'}";
|
|
consumerFor = name';
|
|
run = ''exec s6-log -- T p'${name'}:' "${logBase}/${name'}"'';
|
|
type = "longrun";
|
|
};
|
|
in (serviceToFs' {
|
|
inherit type run finish;
|
|
name = name';
|
|
# TODO: a bundle can have dependencies too!
|
|
depends = lib.optionals (type == "longrun") depends;
|
|
contents = maybe (type == "bundle") depends;
|
|
producerFor = maybe (type == "longrun") "logger:${name'}";
|
|
}) // (lib.optionalAttrs (type == "longrun") logger);
|
|
|
|
serviceToFs'= {
|
|
name,
|
|
type,
|
|
contents ? null,
|
|
run ? null,
|
|
depends ? [],
|
|
finish ? null,
|
|
producerFor ? null,
|
|
consumerFor ? null,
|
|
}: {
|
|
"${name}".dir = {
|
|
"type".text = type;
|
|
# TODO: finish and run should `exec` into their cli
|
|
"finish".executable = maybe (finish != null) ''
|
|
#!/bin/sh
|
|
${finish}
|
|
'';
|
|
"run".executable = maybe (run != null) ''
|
|
#!/bin/sh
|
|
echo "starting: s6-${name}"
|
|
${run} 2>&1
|
|
'';
|
|
"contents".text = maybe (contents != null) (
|
|
lib.concatStringsSep "\n" (builtins.map normalizeName contents)
|
|
);
|
|
# TODO: a bundle can also have dependencies
|
|
"dependencies.d".dir = lib.genAttrs
|
|
(builtins.map normalizeName depends)
|
|
(dep: { text = ""; })
|
|
;
|
|
"consumer-for".text = maybe (consumerFor != null) consumerFor;
|
|
"producer-for".text = maybe (producerFor != null) producerFor;
|
|
};
|
|
};
|
|
|
|
# create a directory, containing N subdirectories:
|
|
# - svc-a/
|
|
# - type
|
|
# - run
|
|
# - svc-b/
|
|
# - type
|
|
# - run
|
|
# - ...
|
|
genServices = svcs: fsToDerivation (lib.foldl'
|
|
(acc: srv: acc // (serviceToFs srv))
|
|
{}
|
|
svcs
|
|
);
|
|
|
|
# output is a directory containing:
|
|
# - db
|
|
# - lock
|
|
# - n
|
|
# - resolve.cdb
|
|
# - servicedirs/
|
|
# - svc-a/
|
|
# - svc-b/
|
|
# - ...
|
|
#
|
|
# this can then be used by s6-rc-init, like:
|
|
# s6-svscan scandir &
|
|
# s6-rc-init -c $compiled -l $PWD/live -d $PWD/scandir
|
|
# s6-rc -l $PWD/live start svc-a
|
|
#
|
|
# N.B.: it seems the $compiled dir needs to be rw, for s6 to write lock files within it.
|
|
# so `cp` and `chmod -R 600` it, first.
|
|
compileServices = sources: with pkgs; stdenv.mkDerivation {
|
|
name = "s6-user-services";
|
|
src = sources;
|
|
nativeBuildInputs = [ s6-rc ];
|
|
buildPhase = ''
|
|
s6-rc-compile $out $src
|
|
'';
|
|
};
|
|
|
|
# transform the `user.services` attrset into a s6 services list.
|
|
s6SvcsFromConfigServices = services: lib.mapAttrsToList
|
|
(name: service: {
|
|
inherit name;
|
|
run = service.command;
|
|
finish = service.cleanupCommand;
|
|
depends = service.wants ++ builtins.attrNames (
|
|
lib.filterAttrs (_: cfg: lib.elem name cfg.wantedBy || lib.elem "${name}.service" cfg.wantedBy) services
|
|
);
|
|
})
|
|
services
|
|
;
|
|
|
|
# in the systemd service management, these targets are implicitly defined and used
|
|
# to accomplish something like run-levels, or service groups.
|
|
# map them onto s6 "bundles". their contents are determined via reverse dependency mapping (`wantedBy` of every other service).
|
|
implicitServices = {
|
|
"default.target" = {
|
|
command = null;
|
|
cleanupCommand = null;
|
|
wants = [];
|
|
wantedBy = [];
|
|
};
|
|
"graphical-session.target" = {
|
|
command = null;
|
|
cleanupCommand = null;
|
|
wants = [];
|
|
wantedBy = [];
|
|
};
|
|
};
|
|
in
|
|
{
|
|
options.sane.users = with lib; mkOption {
|
|
type = types.attrsOf (types.submodule ({ config, ...}: let
|
|
sources = genServices (s6SvcsFromConfigServices (implicitServices // config.services));
|
|
in {
|
|
fs.".config/s6/sources".symlink.target = sources;
|
|
# N.B.: `compiled` needs to be writable (for locks -- maybe i can use symlinks to dodge this someday),
|
|
# so write this nearby and copy it over to `compiled` later
|
|
fs.".config/s6/compiled-static".symlink.target = compileServices sources;
|
|
|
|
fs.".profile".symlink.text = ''
|
|
function startS6() {
|
|
local S6_TARGET="''${1:-default}"
|
|
|
|
local COMPILED="$HOME/.config/s6/compiled"
|
|
local LIVE="$HOME/.config/s6/live"
|
|
local SCANDIR="$HOME/.config/s6/scandir"
|
|
|
|
rm -rf "$SCANDIR"
|
|
mkdir "$SCANDIR"
|
|
s6-svscan "$SCANDIR" &
|
|
local SVSCAN=$!
|
|
|
|
# the scandir is just links back into the compiled dir,
|
|
# so the compiled dir therefore needs to be writable:
|
|
rm -rf "$COMPILED"
|
|
cp --dereference -R "$COMPILED-static" "$COMPILED"
|
|
chmod -R 0700 "$COMPILED"
|
|
|
|
s6-rc-init -c "$COMPILED" -l "$LIVE" -d "$SCANDIR"
|
|
|
|
if [ -n "$S6_TARGET" ]; then
|
|
s6-rc -l "$LIVE" start "$S6_TARGET"
|
|
fi
|
|
|
|
echo 's6 initialized: Ctrl+C to stop'
|
|
wait "$SVSCAN"
|
|
}
|
|
function startS6WithLogging() {
|
|
# TODO: might not want to create log dir here: move to nix fs/persistence.
|
|
mkdir -p "${logBase}"
|
|
startS6 2>&1 | tee /dev/stderr | s6-log -- T "${logBase}/catchall"
|
|
}
|
|
|
|
primarySessionCommands+=('startS6WithLogging &')
|
|
'';
|
|
}));
|
|
};
|
|
}
|