diff --git a/modules/home-environment.nix b/modules/home-environment.nix index f12e86d41..c9515d4f3 100644 --- a/modules/home-environment.nix +++ b/modules/home-environment.nix @@ -7,6 +7,7 @@ let cfg = config.home; dag = config.lib.dag; + dagOf = (import ./lib/types.nix { inherit dag lib; }).dagOf; languageSubModule = types.submodule { options = { @@ -234,17 +235,51 @@ in }; home.activation = mkOption { - internal = true; + type = dagOf types.str; default = {}; - type = types.attrs; + example = literalExample '' + { + myActivationAction = config.lib.dag.entryAfter ["writeBoundary"] ''' + $DRY_RUN_CMD ln -s $VERBOSE_ARG \ + ''${builtins.toPath ./link-me-directly} $HOME + '''; + } + ''; description = '' - Activation scripts for the home environment. + The activation scripts blocks to run when activating a Home + Manager generation. Any entry here should be idempotent, + meaning running twice or more times produces the same result + as running it once. + - Any script should respect the DRY_RUN - variable, if it is set then no actual action should be taken. + + If the script block produces any observable side effect, such + as writing or deleting files, then it + must be placed after the special + writeBoundary script block. Prior to the + write boundary one can place script blocks that verifies, but + does not modify, the state of the system and exits if an + unexpected state is found. For example, the + checkLinkTargets script block checks for + collisions between non-managed files and files defined in + home.file. + + + + A script block should respect the DRY_RUN + variable, if it is set then the actions taken by the script + should be logged to standard out and not actually performed. The variable DRY_RUN_CMD is set to - echo if dry run is enabled. Thus, many cases you - can use the idiom $DRY_RUN_CMD rm -rf /. + echo if dry run is enabled. + + + + A script block should also respect the + VERBOSE variable, and if set print + information on standard out that may be useful for debugging + any issue that may arise. The variable + VERBOSE_ARG is set to + if verbose output is enabled. ''; }; diff --git a/modules/lib/types-dag.nix b/modules/lib/types-dag.nix new file mode 100644 index 000000000..4003d7132 --- /dev/null +++ b/modules/lib/types-dag.nix @@ -0,0 +1,96 @@ +{ dag, lib }: + +with lib; + +let + + isDagEntry = e: isAttrs e && (e ? data) && (e ? after) && (e ? before); + + dagContentType = elemType: types.submodule { + options = { + data = mkOption { type = elemType; }; + after = mkOption { type = with types; uniq (listOf str); }; + before = mkOption { type = with types; uniq (listOf str); }; + }; + }; + +in + +{ + # A directed acyclic graph of some inner type. + dagOf = elemType: + let + convertAllToDags = + let + maybeConvert = n: v: + if isDagEntry v + then v + else dag.entryAnywhere v; + in + map (def: def // { value = mapAttrs maybeConvert def.value; }); + + attrEquivalent = types.attrsOf (dagContentType elemType); + in + mkOptionType rec { + name = "dagOf"; + description = "DAG of ${elemType.description}s"; + check = isAttrs; + merge = loc: defs: attrEquivalent.merge loc (convertAllToDags defs); + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); + getSubModules = elemType.getSubModules; + substSubModules = m: dagOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; + }; + + # A directed acyclic graph of some inner type OR a list of that + # inner type. This is a temporary hack for use by the + # `programs.ssh.matchBlocks` and is only guaranteed to be vaguely + # correct! + # + # In particular, adding a dependency on one of the "unnamed-N-M" + # entries generated by a list value is almost guaranteed to destroy + # the list's order. + # + # This function will be removed in version 20.09. + listOrDagOf = elemType: + let + paddedIndexStr = list: i: + let + padWidth = stringLength (toString (length list)); + in + fixedWidthNumber padWidth i; + + convertAllToDags = defs: + let + convertAttrValue = n: v: + if isDagEntry v then v + else dag.entryAnywhere v; + + convertListValue = namePrefix: vs: + let + pad = paddedIndexStr vs; + makeEntry = i: v: + nameValuePair "${namePrefix}.${pad i}" (dag.entryAnywhere v); + in + listToAttrs (imap1 makeEntry vs); + + convertValue = i: value: + if isList value + then convertListValue "unnamed-${paddedIndexStr defs i}" value + else mapAttrs convertAttrValue value; + in + imap1 (i: def: def // { value = convertValue i def.value; }) defs; + + attrEquivalent = types.attrsOf (dagContentType elemType); + in + mkOptionType rec { + name = "dagOf"; + description = "DAG of ${elemType.description}s"; + check = x: isAttrs x || isList x; + merge = loc: defs: attrEquivalent.merge loc (convertAllToDags defs); + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); + getSubModules = elemType.getSubModules; + substSubModules = m: dagOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; + }; +} diff --git a/modules/lib/types.nix b/modules/lib/types.nix index 1b514d20a..da8b7d4f2 100644 --- a/modules/lib/types.nix +++ b/modules/lib/types.nix @@ -1,9 +1,18 @@ -{ lib }: +{ lib, dag ? import ./dag.nix { inherit lib; } }: with lib; +let + + hmLib = import ./default.nix { inherit lib; }; + typesDag = import ./types-dag.nix { inherit dag lib; }; + +in + { + inherit (typesDag) dagOf listOrDagOf; + selectorFunction = mkOptionType { name = "selectorFunction"; description = diff --git a/tests/default.nix b/tests/default.nix index 95a0f16bc..6f2b5ec04 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -34,6 +34,7 @@ import nmt { // import ./modules/services/sxhkd // import ./modules/systemd ) + // import ./lib/types // import ./modules/files // import ./modules/home-environment // import ./modules/misc/fontconfig diff --git a/tests/lib/types/dag-merge-result.txt b/tests/lib/types/dag-merge-result.txt new file mode 100644 index 000000000..9779ef13c --- /dev/null +++ b/tests/lib/types/dag-merge-result.txt @@ -0,0 +1,3 @@ +before:before +between:between +after:after diff --git a/tests/lib/types/dag-merge.nix b/tests/lib/types/dag-merge.nix new file mode 100644 index 000000000..41c8f28b4 --- /dev/null +++ b/tests/lib/types/dag-merge.nix @@ -0,0 +1,39 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + dag = config.lib.dag; + hmTypes = import ../../../modules/lib/types.nix { inherit dag lib; }; + + result = + let + sorted = dag.topoSort config.tested.dag; + data = map (e: "${e.name}:${e.data}") sorted.result; + in + concatStringsSep "\n" data + "\n"; + +in + +{ + options.tested.dag = mkOption { + type = with types; hmTypes.dagOf str; + }; + + config = { + tested = mkMerge [ + { dag.after = "after"; } + { dag.before = dag.entryBefore ["after"] "before"; } + { dag.between = dag.entryBetween ["after"] ["before"] "between"; } + ]; + + home.file."result.txt".text = result; + + nmt.script = '' + assertFileContent \ + home-files/result.txt \ + ${./dag-merge-result.txt} + ''; + }; +} diff --git a/tests/lib/types/default.nix b/tests/lib/types/default.nix new file mode 100644 index 000000000..9fce65f88 --- /dev/null +++ b/tests/lib/types/default.nix @@ -0,0 +1,4 @@ +{ + lib-types-dag-merge = ./dag-merge.nix; + lib-types-list-or-dag-merge = ./list-or-dag-merge.nix; +} diff --git a/tests/lib/types/list-or-dag-merge-result.txt b/tests/lib/types/list-or-dag-merge-result.txt new file mode 100644 index 000000000..5fb67a510 --- /dev/null +++ b/tests/lib/types/list-or-dag-merge-result.txt @@ -0,0 +1,15 @@ +before:before +between:between +after:after +unnamed-1.1:k +unnamed-1.2:l +unnamed-2.01:a +unnamed-2.02:b +unnamed-2.03:c +unnamed-2.04:d +unnamed-2.05:e +unnamed-2.06:f +unnamed-2.07:g +unnamed-2.08:h +unnamed-2.09:i +unnamed-2.10:j diff --git a/tests/lib/types/list-or-dag-merge.nix b/tests/lib/types/list-or-dag-merge.nix new file mode 100644 index 000000000..6ffaa532e --- /dev/null +++ b/tests/lib/types/list-or-dag-merge.nix @@ -0,0 +1,41 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + dag = config.lib.dag; + hmTypes = import ../../../modules/lib/types.nix { inherit dag lib; }; + + result = + let + sorted = dag.topoSort config.tested.dag; + data = map (e: "${e.name}:${e.data}") sorted.result; + in + concatStringsSep "\n" data + "\n"; + +in + +{ + options.tested.dag = mkOption { + type = with types; hmTypes.listOrDagOf str; + }; + + config = { + tested = mkMerge [ + { dag = [ "k" "l" ]; } + { dag = [ "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" ]; } + { dag.after = "after"; } + { dag.before = dag.entryBefore ["after"] "before"; } + { dag.between = dag.entryBetween ["after"] ["before"] "between"; } + ]; + + home.file."result.txt".text = result; + + nmt.script = '' + assertFileContent \ + home-files/result.txt \ + ${./list-or-dag-merge-result.txt} + ''; + }; +}