{ lib, pkgs, ... }: let logBase = "$HOME/.local/state/s6/logs"; normalizeName = name: lib.removeSuffix ".service" (lib.removeSuffix ".target" name); # infers the service type from the arguments and dispatches appropriately genService = { name, run, finish, depends }: let name' = normalizeName name; type = if run != null then "longrun" else "bundle"; in genService' { name = name'; inherit type; # TODO: a bundle can totally have dependencies. i can't just map them *all* to contents. depends = lib.optionals (type == "longrun") depends; finish = finish; longrun-run = run; bundle-contents = lib.optionals (type == "bundle") depends; }; genService' = { name, type, depends, finish, longrun-run, bundle-contents, }: pkgs.symlinkJoin { name = "s6-${name}"; paths = [ (pkgs.writeTextFile { name = "s6-${name}-type"; destination = "/${name}/type"; text = type; }) ] ++ lib.optionals (finish != null) [ (pkgs.writeTextFile { # TODO: use 'writeShellScript'? name = "s6-${name}-finish"; destination = "/${name}/finish"; executable = true; text = '' #!/bin/sh ${finish} ''; }) ] ++ lib.optionals (longrun-run != null) [ (pkgs.writeTextFile { name = "s6-${name}-run"; destination = "/${name}/run"; executable = true; # TODO: consider using `makeWrapper`/`makeBinaryWrapper`? text = '' #!/bin/sh echo "starting: s6-${name}" ${longrun-run} 2>&1 ''; }) ] ++ lib.optionals (bundle-contents != null) [ (pkgs.writeTextFile { name = "s6-${name}-contents"; destination = "/${name}/contents"; text = lib.concatStringsSep "\n" (builtins.map normalizeName bundle-contents); }) ] ++ builtins.map (d: pkgs.writeTextFile { name = "s6-${name}-depends-${d}"; destination = "/${name}/dependencies.d/${normalizeName d}"; text = ""; }) depends; }; # create a directory, containing N subdirectories: # - svc-a/ # - type # - run # - svc-b/ # - type # - run # - ... genServices = svcs: pkgs.symlinkJoin { name = "s6-user-services"; paths = builtins.map genService 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.serviceConfig.ExecStart; finish = service.serviceConfig.ExecStopPost; 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" = { serviceConfig.ExecStart = null; serviceConfig.ExecStopPost = null; wants = []; wantedBy = []; }; "graphical-session.target" = { serviceConfig.ExecStart = null; serviceConfig.ExecStopPost = 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 &') ''; })); }; }