{ 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, depends }: let name' = normalizeName name; logDir = "${logBase}/${name'}"; # N.B. s6-log will create the logDir. at least, if the parent dir exists (haven't tested without that) logger = genRun "${name'}/log" ''s6-log -- T "${logDir}"''; in if run != null then genService' name' "longrun" depends [ (genRun name' run) logger ] else # TODO: a bundle can totally have dependencies. i can't just map them *all* to contents. # genService' (normalizeName name) "bundle" [] ( # (builtins.map # (d: pkgs.writeTextFile { # name = "s6-${name}-contains-${d}"; # destination = "/${normalizeName name}/contents.d/${normalizeName d}"; # text = ""; # }) # depends # ) ++ [ # # in case the bundle has no contents, ensure `contents.d` still gets made # (pkgs.runCommandLocal "s6-${name}-contains.d" {} '' # mkdir -p $out/"${normalizeName name}"/contents.d # '') # ] # ) genService' name' "bundle" [] [ (pkgs.writeTextFile { name = "s6-${name'}-contents"; destination = "/${name'}/contents"; text = lib.concatStringsSep "\n" (builtins.map normalizeName depends); }) logger ] ; genService' = name: type: depends: others: pkgs.symlinkJoin { name = "s6-${name}"; paths = others ++ [ (pkgs.writeTextFile { name = "s6-${name}-type"; destination = "/${name}/type"; text = type; }) ] ++ builtins.map (d: pkgs.writeTextFile { name = "s6-${name}-depends-${d}"; destination = "/${name}/dependencies.d/${normalizeName d}"; text = ""; }) depends; }; genRun = serviceName: shellCommand: pkgs.writeTextFile { name = "s6-${serviceName}-run"; destination = "/${serviceName}/run"; executable = true; # TODO: consider using `makeWrapper`/`makeBinaryWrapper`? text = '' #!/bin/sh echo "starting: s6-${serviceName}" ${shellCommand} 2>&1 ''; }; # 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; 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; wants = []; wantedBy = []; }; "graphical-session.target" = { serviceConfig.ExecStart = 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}/root" } primarySessionCommands+=('startS6WithLogging &') ''; })); }; }