diff --git a/modules/users/default.nix b/modules/users/default.nix index f18c2a9e..2b61814c 100644 --- a/modules/users/default.nix +++ b/modules/users/default.nix @@ -241,6 +241,7 @@ let in { imports = [ + ./s6-rc.nix ./systemd.nix ]; diff --git a/modules/users/s6-rc.nix b/modules/users/s6-rc.nix new file mode 100644 index 00000000..f7bd01e7 --- /dev/null +++ b/modules/users/s6-rc.nix @@ -0,0 +1,129 @@ +{ lib, pkgs, ... }: +let + normalizeName = name: lib.removeSuffix ".service" (lib.removeSuffix ".target" name); + + # infers the service type from the arguments and dispatches appropriately + genService = { name, run, depends }: if run != null then + genService' (normalizeName name) "longrun" depends [ + (pkgs.writeTextFile { + name = "s6-${name}-run"; + destination = "/${normalizeName name}/run"; + text = run; + }) + ] + 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' (normalizeName name) "bundle" [] [ + (pkgs.writeTextFile { + name = "s6-${name}-contents"; + destination = "/${normalizeName name}/contents"; + text = lib.concatStringsSep "\n" (builtins.map normalizeName depends); + }) + ] + ; + 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; + }; + + # 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 + 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) 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; + fs.".config/s6/compiled".symlink.target = compileServices sources; + })); + }; +}