From 8fab2a5d9bb685bbf190565ea242afe670367bf4 Mon Sep 17 00:00:00 2001 From: Robert Helgesson Date: Thu, 4 May 2017 00:36:39 +0200 Subject: [PATCH] Add basic directed acyclic graph data structure Also make use of this instead of Nixpkgs's strings-with-deps library in activation script generation. --- modules/home-environment.nix | 38 +++++---- modules/lib/dag.nix | 124 ++++++++++++++++++++++++++++ modules/programs/gnome-terminal.nix | 10 ++- modules/systemd.nix | 3 +- 4 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 modules/lib/dag.nix diff --git a/modules/home-environment.nix b/modules/home-environment.nix index 8c3c462e..1e7a4473 100644 --- a/modules/home-environment.nix +++ b/modules/home-environment.nix @@ -1,6 +1,7 @@ { config, lib, pkgs, ... }: with lib; +with import ./lib/dag.nix; let @@ -241,7 +242,11 @@ in // (maybeSet "LC_TIME" cfg.language.time); - home.activation.linkGeneration = + # A dummy entry acting as a boundary between the activation + # script's "check" and the "write" phases. + home.activation.writeBoundary = dagEntryAnywhere ""; + + home.activation.linkGeneration = dagEntryAfter ["writeBoundary"] ( let link = pkgs.writeText "link" '' newGenFiles="$1" @@ -303,27 +308,26 @@ in else echo "Same home files as previous generation ... doing nothing" fi - ''; + '' + ); - home.activation.installPackages = - '' - $DRY_RUN_CMD nix-env -i ${cfg.path} - ''; + home.activation.installPackages = dagEntryAfter ["writeBoundary"] '' + $DRY_RUN_CMD nix-env -i ${cfg.path} + ''; home.activationPackage = let - addHeader = n: v: - v // { - text = '' - echo Activating ${n} - ${v.text} - ''; - }; - toDepString = n: v: if isString v then noDepEntry v else v; - activationWithDeps = - mapAttrs addHeader (mapAttrs toDepString cfg.activation); + mkCmd = res: '' + echo Activating ${res.name} + ${res.data} + ''; + sortedCommands = dagTopoSort cfg.activation; activationCmds = - textClosureMap id activationWithDeps (attrNames activationWithDeps); + if sortedCommands ? result then + concatStringsSep "\n" (map mkCmd sortedCommands.result) + else + abort ("Dependency cycle in activation script: " + + builtins.toJSON sortedCommands); sf = pkgs.writeText "activation-script" '' #!${pkgs.stdenv.shell} diff --git a/modules/lib/dag.nix b/modules/lib/dag.nix new file mode 100644 index 00000000..cb7e6f89 --- /dev/null +++ b/modules/lib/dag.nix @@ -0,0 +1,124 @@ +# A generalization of Nixpkgs's `strings-with-deps.nix`. +# +# The main differences from the Nixpkgs version are +# +# - not specific to strings, i.e., any payload is OK, +# +# - the addition of the function `dagEntryBefore` indicating a +# "wanted by" relationship. + +with import ; +with import ; +with import ; + +rec { + + emptyDag = {}; + + isDag = dag: + let + isEntry = e: (e ? data) && (e ? after) && (e ? before); + in + builtins.isAttrs dag && all (x: x) (mapAttrsToList (n: isEntry) dag); + + # Takes an attribute set containing entries built by + # dagEntryAnywhere, dagEntryAfter, and dagEntryBefore to a + # topologically sorted list of entries. + # + # Internally this function uses the `toposort` function in + # `` and its value is accordingly. + # + # Specifically, the result on success is + # + # { result = [{name = ?; data = ?;} …] } + # + # For example + # + # nix-repl> dagTopoSort { + # a = dagEntryAnywhere "1"; + # b = dagEntryAfter ["a" "c"] "2"; + # c = dagEntryBefore ["d"] "3"; + # d = dagEntryBefore ["e"] "4"; + # e = dagEntryAnywhere "5"; + # } == { + # result = [ + # { data = "1"; name = "a"; } + # { data = "3"; name = "c"; } + # { data = "2"; name = "b"; } + # { data = "4"; name = "d"; } + # { data = "5"; name = "e"; } + # ]; + # } + # true + # + # And the result on error is + # + # { + # cycle = [ {after = ?; name = ?; data = ?} … ]; + # loops = [ {after = ?; name = ?; data = ?} … ]; + # } + # + # For example + # + # nix-repl> dagTopoSort { + # a = dagEntryAnywhere "1"; + # b = dagEntryAfter ["a" "c"] "2"; + # c = dagEntryAfter ["d"] "3"; + # d = dagEntryAfter ["b"] "4"; + # e = dagEntryAnywhere "5"; + # } == { + # cycle = [ + # { after = ["a" "c"]; data = "2"; name = "b"; } + # { after = ["d"]; data = "3"; name = "c"; } + # { after = ["b"]; data = "4"; name = "d"; } + # ]; + # loops = [ + # { after = ["a" "c"]; data = "2"; name = "b"; } + # ]; + # } == {} + # true + dagTopoSort = dag: + let + dagBefore = dag: name: + mapAttrsToList (n: v: n) ( + filterAttrs (n: v: any (a: a == name) v.before) dag + ); + normalizedDag = + mapAttrs (n: v: { + name = n; + data = v.data; + after = v.after ++ dagBefore dag n; + }) dag; + before = a: b: any (c: a.name == c) b.after; + sorted = toposort before (mapAttrsToList (n: v: v) normalizedDag); + in + if sorted ? result then + { result = map (v: { inherit (v) name data; }) sorted.result; } + else + sorted; + + # Applies a function to each element of the given DAG. + dagMap = f: dag: mapAttrs (n: v: v // { data = f n v.data; }) dag; + + # Create a DAG entry with no particular dependency information. + dagEntryAnywhere = data: { + inherit data; + before = []; + after = []; + }; + + dagEntryBetween = before: after: data: { + inherit data before after; + }; + + dagEntryAfter = after: data: { + inherit data after; + before = []; + }; + + dagEntryBefore = before: data: { + inherit data before; + after = []; + }; + +} diff --git a/modules/programs/gnome-terminal.nix b/modules/programs/gnome-terminal.nix index ccb6a57b..bbd2b392 100644 --- a/modules/programs/gnome-terminal.nix +++ b/modules/programs/gnome-terminal.nix @@ -1,6 +1,7 @@ { config, lib, pkgs, ... }: with lib; +with import ../lib/dag.nix; let @@ -177,18 +178,19 @@ in config = mkIf cfg.enable { home.packages = [ pkgs.gnome3.gnome_terminal ]; - home.activation.gnomeTerminal = + # The dconf service needs to be installed and prepared. + home.activation.gnomeTerminal = dagEntryAfter ["installPackages"] ( let sf = pkgs.writeText "gnome-terminal.ini" (toINI (buildIniSet cfg)); dconfPath = "/org/gnome/terminal/legacy/"; in - # The dconf service needs to be installed and prepared. - stringAfter [ "installPackages" ] '' + '' if [[ -v DRY_RUN ]]; then echo ${pkgs.gnome3.dconf}/bin/dconf load ${dconfPath} "<" ${sf} else ${pkgs.gnome3.dconf}/bin/dconf load ${dconfPath} < ${sf} fi - ''; + '' + ); }; } diff --git a/modules/systemd.nix b/modules/systemd.nix index 293b32df..1e6117a1 100644 --- a/modules/systemd.nix +++ b/modules/systemd.nix @@ -1,6 +1,7 @@ { config, lib, pkgs, ... }: with lib; +with import ./lib/dag.nix; let @@ -69,7 +70,7 @@ in (buildServices "timer" config.systemd.user.timers) ); - home.activation.reloadSystemD = '' + home.activation.reloadSystemD = dagEntryAfter ["linkGeneration"] '' function systemdPostReload() { local workDir workDir="$(mktemp -d)"