{ config, pkgs, lib, ... }: with pkgs.lib; let # -------- implementation -------- # cfg = config.s6-rc; logNames = flatten (mapAttrsToList (_: sv: optional (sv.log != null) sv.log) cfg.longruns); loggingRelations = genAttrs logNames (logName: filter (serviceName: cfg.longruns.${serviceName}.log == logName) (attrNames cfg.longruns)); logLongruns = mapAttrs' (logName: producers: nameValuePair "${logName}-log" { run = '' ${pkgs.s6}/bin/s6-setuidgid "${cfg.logUser}" ${pkgs.execline}/bin/exec -c ${pkgs.s6}/bin/s6-log -d 3 -- ${cfg.loggingScript logName} ''; notificationFd = 3; consumerFor = producers; pipelineName = "${logName}-pipeline"; flagEssential = false; timeoutUp = null; timeoutDown = null; dependencies = null; finish = null; timeoutKill = null; timeoutFinish = null; maxDeathTally = null; downSignal = null; producerFor = null; log = null; }) loggingRelations; allLongruns = cfg.longruns // logLongruns; makeCommonConfig = sv: '' mkdir -p $out '' + optionalString sv.flagEssential '' touch $out/flagEssential ''; makeBundleConfig = sv: makeCommonConfig sv + '' echo bundle > $out/type mkdir $out/contents.d '' + concatMapStringsSep "\n" (svName: ''touch "$out/contents.d/${svName}"'') sv.contents + "\n"; makeAtomicConfig = sv: makeCommonConfig sv + optionalString (sv.timeoutUp != null) '' echo "${toString sv.timeoutUp}" > $out/timeout-up '' + optionalString (sv.timeoutDown != null) '' echo "${toString sv.timeoutDown}" > $out/timeout-down '' + optionalString (sv.dependencies != null) ( '' mkdir $out/dependencies.d '' + concatMapStringsSep "\n" (svName: ''touch "$out/dependencies.d/${svName}"'') sv.dependencies + "\n" ); makeOneshotConfig = sv: let upScript = pkgs.writeText "up-script" sv.up; downScript = mapNullable (pkgs.writeText "down-script") sv.down; in makeAtomicConfig sv + '' echo oneshot > $out/type cp ${upScript} $out/up '' + optionalString (downScript != null) '' cp ${downScript} $out/down ''; makeLongrunConfig = sv: let writeLongrunScript = name: text: pkgs.writeScript name '' #!${pkgs.execline}/bin/execlineb -P ${pkgs.execline}/bin/fdmove -c 2 1 ${text} ''; runScript = writeLongrunScript "run-script" sv.run; finishScript = mapNullable (writeLongrunScript "finish-script") sv.finish; in makeAtomicConfig sv + '' echo longrun > $out/type cp ${runScript} $out/run '' + optionalString (finishScript != null) '' cp ${finishScript} $out/finish '' + optionalString (sv.notificationFd != null) '' echo "${toString sv.notificationFd}" > $out/notification-fd '' + optionalString (sv.timeoutKill != null) '' echo "${toString sv.timeoutKill}" > $out/timeout-kill '' + optionalString (sv.timeoutFinish != null) '' echo "${toString sv.timeoutFinish}" > $out/timeout-finish '' + optionalString (sv.maxDeathTally != null) '' echo "${toString sv.maxDeathTally}" > $out/max-death-tally '' + optionalString (sv.downSignal != null) '' echo "${sv.downSignal}" > $out/down-signal '' + optionalString (sv.producerFor != null) '' echo "${sv.producerFor}" > $out/producer-for '' + optionalString (sv.consumerFor != null) '' echo "${concatStringsSep "\n" sv.consumerFor}" > $out/consumer-for '' + optionalString (sv.pipelineName != null) '' echo "${sv.pipelineName}" > $out/pipeline-name '' + optionalString (sv.log != null) '' echo "${sv.log}-log" > $out/producer-for ''; makeBundleDefinitionDir = name: sv: pkgs.runCommand "s6-rc-bundle-definition-${name}" { } (makeBundleConfig sv); makeOneshotDefinitionDir = name: sv: pkgs.runCommand "s6-rc-oneshot-definition-${name}" { } (makeOneshotConfig sv); makeLongrunDefinitionDir = name: sv: pkgs.runCommand "s6-rc-longrun-definition-${name}" { } (makeLongrunConfig sv); bundleDefinitionDirs = mapAttrs makeBundleDefinitionDir cfg.bundles; oneshotDefinitionDirs = mapAttrs makeOneshotDefinitionDir cfg.oneshots; longrunDefinitionDirs = mapAttrs makeLongrunDefinitionDir allLongruns; allDefinitionDirs = bundleDefinitionDirs // oneshotDefinitionDirs // longrunDefinitionDirs; serviceSourceDir = pkgs.linkFarm "s6-rc-service-source" (mapAttrsToList (svName: definitionDir: { name = svName; path = definitionDir; }) allDefinitionDirs); compiledDatabase = pkgs.runCommand "s6-rc-compiled-database" { } '' ${pkgs.s6-rc}/bin/s6-rc-compile $out ${serviceSourceDir} ''; # -------- assertions -------- # assertions = [ { assertion = length (attrNames allDefinitionDirs) == length (attrNames cfg.bundles) + length (attrNames cfg.oneshots) + length (attrNames cfg.longruns) + length (attrNames logLongruns); message = "no two services can have the same name, even if they are of different types"; } ] ++ mapAttrsToList (name: def: { assertion = def.producerFor == null || def.log == null; message = "in `longruns.${name}`: `producerFor` and `log` are mutually exclusive"; }) cfg.longruns; failedAssertions = map (x: x.message) (filter (x: !x.assertion) assertions); checkAssertions = if failedAssertions != [ ] then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}" else true; in { # -------- interface -------- # options.s6-rc = with types; let mkNullableOption = { type, ... } @ o: mkOption o // { type = nullOr type; default = null; }; # see: https://skarnet.org/software/s6-rc/s6-rc-compile.html#source commonOptions = { flagEssential = mkOption { type = bool; default = false; }; }; bundleOptions = { contents = mkOption { type = listOf str; }; } // commonOptions; atomicOptions = { timeoutUp = mkNullableOption { type = ints.unsigned; }; timeoutDown = mkNullableOption { type = ints.unsigned; }; dependencies = mkNullableOption { type = listOf str; }; } // commonOptions; oneshotOptions = { up = mkOption { type = str; }; down = mkNullableOption { type = str; }; } // atomicOptions; longrunOptions = { run = mkOption { type = str; }; finish = mkNullableOption { type = str; }; notificationFd = mkNullableOption { type = ints.unsigned; }; timeoutKill = mkNullableOption { type = ints.unsigned; }; timeoutFinish = mkNullableOption { type = ints.unsigned; }; maxDeathTally = mkNullableOption { type = ints.between 0 4096; }; downSignal = mkNullableOption { type = str; }; producerFor = mkNullableOption { type = str; }; consumerFor = mkNullableOption { type = listOf str; }; pipelineName = mkNullableOption { type = str; }; # this is not an actual option in s6-rc; it creates a logger service # named "-log" and adds the necessary "producer-for" and # "consumer-for" files. log = mkNullableOption { type = str; }; } // atomicOptions; in { logUser = mkOption { type = str; default = "log"; }; logGroup = mkOption { type = str; default = "log"; }; logDir = mkOption { type = path; default = "/log"; }; loggingScript = mkOption { type = anything; default = name: "T s1000000 n20 ${cfg.logDir}/${name}"; }; bundles = mkOption { type = with types; attrsOf (submodule { options = bundleOptions; }); default = { }; }; oneshots = mkOption { type = with types; attrsOf (submodule { options = oneshotOptions; }); default = { }; }; longruns = mkOption { type = with types; attrsOf (submodule { options = longrunOptions; }); default = { }; }; # -------- output -------- # serviceSourceDir = mkOption { type = types.raw; }; compiledDatabase = mkOption { type = types.raw; }; }; config.s6-rc = lib.mkIf checkAssertions { inherit serviceSourceDir compiledDatabase; }; }