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}
+ '';
+ };
+}