{ 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 = ; } 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 &') ''; })); }; }