diff --git a/default.nix b/default.nix index 56cc148..9adbbde 100644 --- a/default.nix +++ b/default.nix @@ -30,15 +30,22 @@ dynamicBefore = nixosBefore.config.dynamicism.finalSettings; nixosAfter = evalNixos { - configuration = lib.mkMerge [ - nixosBefore - (lib.setAttrByPath option (lib.mkOverride (-999) value)) - ]; + configuration = { ... }: { + imports = [ + ./configuration.nix + (lib.setAttrByPath option (lib.mkOverride (-999) value)) + ]; + }; }; - dynamicAfter = nixosAfter.config.dynamicism.finalSettings; + withActivationScripts = evalNixos { + configuration = ({ ... }: { + imports = [ ./configuration.nix ]; + config.environment.systemPackages = [ nixosAfter.config.dynamicism.for.gotosocial.activate ]; + }); + }; in { - + inherit nixosBefore nixosAfter withActivationScripts; }; in dynix.overrideAttrs (final: prev: let @@ -56,5 +63,9 @@ in lib.recursiveUpdate prev { c = self.nixos; nixos-vm = self.nixos.config.system.build.vm; doChange = builtins.seq self.nixos.config.dynamicism doChange; + withVox = self.doChange { + option = lib.splitString "." "services.gotosocial.settings.application-name"; + value = "Vox is an asshole"; + }; }; }) diff --git a/modules/dynamicism/default.nix b/modules/dynamicism/default.nix index 1b243ac..3d1dcaa 100644 --- a/modules/dynamicism/default.nix +++ b/modules/dynamicism/default.nix @@ -13,19 +13,43 @@ let cfg = config.dynamicism; opts = options.dynamicism; subOpts = lib.mapAttrs (_: metaAttr: metaAttr.configuration.options) options.dynamicism.for.valueMeta.attrs; + settingsFormat = pkgs.formats.yaml { }; + + concatFoldl = f: list: lib.foldl' (acc: value: acc ++ (f value)) [ ] list; + recUpdateFoldlAttrs = f: attrs: lib.foldlAttrs (acc: name: value: lib.recursiveUpdate acc (f name value)) { } attrs; finalSettingsFor = { ... }@submod: lib.foldl (acc: optPath: let next = assert lib.isList optPath; lib.setAttrByPath optPath (lib.getAttrFromPath optPath config); in lib.recursiveUpdate acc next) { } submod.source-options; + + ourAssertions = lib.concatAttrValues { + unitsExist = concatFoldl (submod: let + next = lib.map (unit: assert lib.isString unit; { + assertion = config.systemd.units.${unit}.enable or false; + message = '' + '${showOption submod.systemd-services-updated.loc}' specified non-existent unit '${unit}' + ''; + }) submod.systemd-services-updated.value; + in lib.optionals submod.enable.value next) (lib.attrValues subOpts); + + optsExist = concatFoldl (submod: lib.optionals submod.enable.value (lib.map (optPath: { + assertion = lib.hasAttrByPath optPath options; + message = "'${showOption submod.source-options.loc}' specified non-existent option '${showOption optPath}'"; + }) submod.source-options.value)) (lib.attrValues subOpts); + }; in { + # + # Interface. + # options.dynamicism = { for = mkOption { type = t.attrsOf (t.submoduleWith { modules = [ ./submodule.nix ]; shorthandOnlyDefinesConfig = false; + specialArgs = { inherit pkgs; }; }); default = { }; }; @@ -40,39 +64,30 @@ in }; }; - config.assertions = let - unitsExist = lib.foldl' (acc: submod: let - next = lib.map (unit: assert lib.isString unit; { - assertion = config.systemd.units.${unit}.enable or false; - message = '' - '${showOption submod.systemd-services-updated.loc}' specified non-existent unit '${unit}' - ''; - }) submod.systemd-services-updated.value; - in acc ++ lib.optionals submod.enable.value next) [ ] (lib.attrValues subOpts); + # Assertions. + config.assertions = ourAssertions; - optsExist = lib.foldl' (acc: submod: let - next = lib.map (optPath: { - assertion = lib.hasAttrByPath optPath options; - message = "'${showOption submod.source-options.loc}' specified non-existent option '${showOption optPath}'"; - }) submod.source-options.value; - in acc ++ lib.optionals submod.enable.value next) [ ] (lib.attrValues subOpts); - in lib.concatLists [ - unitsExist - optsExist - ]; + # + # Generic implementation. + # + config.dynamicism.finalSettings = lib.asserts.checkAssertWarn ourAssertions [ ] ( + recUpdateFoldlAttrs (name: { ... }@submod: finalSettingsFor submod) config.dynamicism.for + ); - config.dynamicism.for = { - gotosocial = { - source-options = [ - "services.gotosocial.setting" - ]; - systemd-services-updated = [ - "gotosocial.service" - ]; - }; + # Implementations. + config.dynamicism.for.gotosocial = let + cfg = config.dynamicism.for.gotosocial; + in { + source-options = [ + "services.gotosocial.settings" + ]; + + configFile = settingsFormat.generate "gotosocial-override.yml" config.services.gotosocial.settings; + + unitDropins."gotosocial.service" = pkgs.writeText "gotosocial-override.conf" '' + [Service] + ExecStart= + ExecStart=${lib.getExe' pkgs.gotosocial "gotosocial"} --config-path ${cfg.configFile} start + ''; }; - - config.dynamicism.finalSettings = lib.foldlAttrs (acc: name: { ... }@submod: let - next = finalSettingsFor submod; - in lib.recursiveUpdate acc next) { } config.dynamicism.for; } diff --git a/modules/dynamicism/submodule.nix b/modules/dynamicism/submodule.nix index 8f6ff1e..1b9dac9 100644 --- a/modules/dynamicism/submodule.nix +++ b/modules/dynamicism/submodule.nix @@ -1,5 +1,6 @@ { name, + pkgs, lib, config, ... @@ -13,10 +14,26 @@ let mkEnableOption literalExpression ; + inherit (lib.types) + mkOptionType + ; t = lib.types; /** Either a list of strings, or a dotted string that will be split. */ convenientAttrPath = t.coercedTo t.str (lib.splitString ".") (t.listOf t.str); + + executablePathInStore = mkOptionType { + name = "exepath"; + description = "executable path in the Nix store"; + descriptionClass = "noun"; + merge = lib.mergeEqualOption; + functor = lib.defaultFunctor "exepath"; + check = x: if lib.isDerivation x then ( + x.meta.mainProgram or null != null + ) else ( + lib.pathInStore.check x + ); + }; in { options = { @@ -36,12 +53,46 @@ in A list of systemd unit names (including the suffix, e.g. `.service`) that need to be updated. ''; example = literalExpression '' - "gotosocial.service" + [ "gotosocial.service" ] ''; + + default = lib.attrNames config.unitDropins; + }; + + configFile = mkOption { + type = t.pathInStore; + internal = true; + }; + + unitDropins = mkOption { + type = t.attrsOf t.pathInStore; + internal = true; + }; + + activate = mkOption { + type = executablePathInStore; + internal = true; }; }; config = mkIf config.enable { - + activate = pkgs.writeShellApplication { + name = "dynamicism-for-${name}-activate"; + runtimeInputs = [ pkgs.systemd ]; + text = let + doEdits = config.unitDropins + |> lib.mapAttrsToList (service: dropin: '' + cat "${dropin}" | systemctl edit "${service}" --runtime --stdin + ''); + doReloads = config.unitDropins + |> lib.mapAttrsToList (service: _: '' + systemctl reload-or-restart "${service}" + ''); + in [ + doEdits + doReloads + ] |> lib.concatLists + |> lib.concatStringsSep "\n"; + }; }; }