about summary refs log tree commit diff
path: root/nix/dependency-analyzer/default.nix
blob: 54ff72912ea8ddce526b6dee289c5b8fa68090f7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
{ lib, depot, pkgs, ... }:

let
  inherit (builtins) unsafeDiscardStringContext appendContext;

  #
  # Utilities
  #

  # Determine all paths a derivation depends on, i.e. input derivations and
  # files imported into the Nix store.
  #
  # Implementation for Nix < 2.6 is quite hacky at the moment.
  #
  # Type: str -> [str]
  #
  # TODO(sterni): clean this up and expose it
  directDrvDeps =
    if lib.versionAtLeast builtins.nixVersion "2.6"
    then
    # Since https://github.com/NixOS/nix/pull/1643, Nix apparently »preserves
    # string context« through a readFile invocation. This has the side effect
    # that it becomes possible to query the actual references a store path has.
    # Not a 100% sure this is intended, but _very_ convenient for us here.
      drvPath:
      # if the passed path is not a derivation we can't necessarily get its
      # dependencies, since it may not be representable as a Nix string due to
      # NUL bytes, e.g. compressed patch files imported into the Nix store.
      if builtins.match "^.+\\.drv$" drvPath == null
      then [ ]
      else builtins.attrNames (builtins.getContext (builtins.readFile drvPath))
    else
    # For Nix < 2.6 we have to rely on HACK, namely grepping for quoted store
    # path references in the file. In the future this should be replaced by
    # a proper derivation parser.
      drvPath: builtins.concatLists (
        builtins.filter builtins.isList (
          builtins.split
            "\"(${lib.escapeRegex builtins.storeDir}/[[:alnum:]+._?=-]+.drv)\""
            (builtins.readFile drvPath)
        )
      );

  # Maps a list of derivation to the list of corresponding `drvPath`s.
  #
  # Type: [drv] -> [str]
  drvsToPaths = drvs:
    builtins.map (drv: builtins.unsafeDiscardOutputDependency drv.drvPath) drvs;

  #
  # Calculate map of direct derivation dependencies
  #

  # Create the dependency map entry for a given `drvPath` which mainly includes
  # a list of other `drvPath`s it depends on. Additionally we store whether the
  # derivation is `known`, i.e. part of the initial list of derivations we start
  # generating the map from
  #
  # Type: bool -> string -> set
  drvEntry = known: drvPath:
    let
      # key may not refer to a store path, …
      key = unsafeDiscardStringContext drvPath;
      # but we must read from the .drv file.
      path = builtins.unsafeDiscardOutputDependency drvPath;
    in
    {
      inherit key;
      # trick so we can call listToAttrs directly on the result of genericClosure
      name = key;
      value = {
        deps = directDrvDeps path;
        inherit known;
      };
    };

  # Create an attribute set that maps every derivation in the combined
  # dependency closure of the list of input derivation paths to every of their
  # direct dependencies. Additionally every entry will have set their `known`
  # attribute to `true` if it is in the list of input derivation paths.
  #
  # Type: [str] -> set
  plainDrvDepMap = drvPaths:
    builtins.listToAttrs (
      builtins.genericClosure {
        startSet = builtins.map (drvEntry true) drvPaths;
        operator = { value, ... }: builtins.map (drvEntry false) value.deps;
      }
    );

  #
  # Calculate closest known dependencies in the dependency map
  #

  inherit (depot.nix.stateMonad)
    after
    bind
    for_
    get
    getAttr
    run
    setAttr
    pure
    ;

  # This is an action in stateMonad which expects the (initial) state to have
  # been produced by `plainDrvDepMap`. Given a `drvPath`, it calculates a
  # `knownDeps` list which holds the `drvPath`s of the closest derivation marked
  # as `known` along every edge. This list is inserted into the dependency map
  # for `drvPath` and every other derivation in its dependecy closure (unless
  # the information was already present). This means that the known dependency
  # information for a derivation never has to be recalculated, as long as they
  # are part of the same stateful computation.
  #
  # The upshot is that after calling `insertKnownDeps drvPath`,
  # `fmap (builtins.getAttr "knownDeps") (getAttr drvPath)` will always succeed.
  #
  # Type: str -> stateMonad drvDepMap null
  insertKnownDeps = drvPathWithContext:
    let
      # We no longer need to read from the store, so context is irrelevant, but
      # we need to check for attr names which requires the absence of context.
      drvPath = unsafeDiscardStringContext drvPathWithContext;
    in
    bind get (initDepMap:
      # Get the dependency map's state before we've done anything to obtain the
      # entry we'll be manipulating later as well as its dependencies.
      let
        entryPoint = initDepMap.${drvPath};

        # We don't need to recurse if our direct dependencies either have their
        # knownDeps list already populated or are known dependencies themselves.
        depsPrecalculated =
          builtins.partition
            (dep:
              initDepMap.${dep}.known
              || initDepMap.${dep} ? knownDeps
            )
            entryPoint.deps;

        # If a direct dependency is known, it goes right to our known dependency
        # list. If it is unknown, we can copy its knownDeps list into our own.
        initiallyKnownDeps =
          builtins.concatLists (
            builtins.map
              (dep:
                if initDepMap.${dep}.known
                then [ dep ]
                else initDepMap.${dep}.knownDeps
              )
              depsPrecalculated.right
          );
      in

      # If the information was already calculated before, we can exit right away
      if entryPoint ? knownDeps
      then pure null
      else
        after
          # For all unknown direct dependencies which don't have a `knownDeps`
          # list, we call ourselves recursively to populate it. Since this is
          # done sequentially in the state monad, we avoid recalculating the
          # list for the same derivation multiple times.
          (for_
            depsPrecalculated.wrong
            insertKnownDeps)
          # After this we can obtain the updated dependency map which will have
          # a `knownDeps` list for all our direct dependencies and update the
          # entry for the input `drvPath`.
          (bind
            get
            (populatedDepMap:
              (setAttr drvPath (entryPoint // {
                knownDeps =
                  lib.unique (
                    initiallyKnownDeps
                      ++ builtins.concatLists (
                      builtins.map
                        (dep: populatedDepMap.${dep}.knownDeps)
                        depsPrecalculated.wrong
                    )
                  );
              }))))
    );

  # This function puts it all together and is exposed via `__functor`.
  #
  # For a list of `drvPath`s, calculate an attribute set which maps every
  # `drvPath` to a set of the following form:
  #
  #     {
  #       known = true /* if it is in the list of input derivation paths */;
  #       deps = [
  #         /* list of derivation paths it depends on directly */
  #       ];
  #       knownDeps = [
  #         /* list of the closest derivation paths marked as known this
  #            derivation depends on.
  #         */
  #       ];
  #     }
  knownDrvDepMap = knownDrvPaths:
    run
      (plainDrvDepMap knownDrvPaths)
      (after
        (for_
          knownDrvPaths
          insertKnownDeps)
        get);

  #
  # Other things based on knownDrvDepMap
  #

  # Create a SVG visualizing `knownDrvDepMap`. Nodes are identified by derivation
  # name, so multiple entries can be collapsed if they have the same name.
  #
  # Type: [drv] -> drv
  knownDependencyGraph = name: drvs:
    let
      justName = drvPath:
        builtins.substring
          (builtins.stringLength builtins.storeDir + 1 + 32 + 1)
          (builtins.stringLength drvPath)
          (unsafeDiscardStringContext drvPath);

      gv = pkgs.writeText "${name}-dependency-analysis.gv" ''
        digraph depot {
        ${
          (lib.concatStringsSep "\n"
          (lib.mapAttrsToList (name: value:
            if !value.known then ""
            else lib.concatMapStringsSep "\n"
              (knownDep: "  \"${justName name}\" -> \"${justName knownDep}\"")
              value.knownDeps
          )
          (depot.nix.dependency-analyzer (
            drvsToPaths drvs
          ))))
        }
        }
      '';
    in

    pkgs.runCommand "${name}-dependency-analysis.svg"
      {
        nativeBuildInputs = [
          pkgs.buildPackages.graphviz
        ];
      }
      "dot -Tsvg < ${gv} > $out";
in

{
  __functor = _: knownDrvDepMap;

  inherit knownDependencyGraph plainDrvDepMap drvsToPaths;
}