summary refs log tree commit diff
path: root/nixos/maintainers/option-usages.nix
blob: 11247666ecda9d7d41cde372c93fe0bf7d0efd91 (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
{ configuration ? import ../lib/from-env.nix "NIXOS_CONFIG" <nixos-config>

# provide an option name, as a string literal.
, testOption ? null

# provide a list of option names, as string literals.
, testOptions ? [ ]
}:

# This file is made to be used as follow:
#
#   $ nix-instantiate ./option-usage.nix --argstr testOption service.xserver.enable -A txtContent --eval
#
# or
#
#   $ nix-build ./option-usage.nix --argstr testOption service.xserver.enable -A txt -o service.xserver.enable._txt
#
# Other targets exists such as `dotContent`, `dot`, and `pdf`.  If you are
# looking for the option usage of multiple options, you can provide a list
# as argument.
#
#   $ nix-build ./option-usage.nix --arg testOptions \
#      '["boot.loader.gummiboot.enable" "boot.loader.gummiboot.timeout"]' \
#      -A txt -o gummiboot.list
#
# Note, this script is slow as it has to evaluate all options of the system
# once per queried option.
#
# This nix expression works by doing a first evaluation, which evaluates the
# result of every option.
#
# Then, for each queried option, we evaluate the NixOS modules a second
# time, except that we replace the `config` argument of all the modules with
# the result of the original evaluation, except for the tested option which
# value is replaced by a `throw` statement which is caught by the `tryEval`
# evaluation of each option value.
#
# We then compare the result of the evaluation of the original module, with
# the result of the second evaluation, and consider that the new failures are
# caused by our mutation of the `config` argument.
#
# Doing so returns all option results which are directly using the
# tested option result.

with import ../../lib;

let

  evalFun = {
    specialArgs ? {}
  }: import ../lib/eval-config.nix {
       modules = [ configuration ];
       inherit specialArgs;
     };

  eval = evalFun {};
  inherit (eval) pkgs;

  excludedTestOptions = [
    # We cannot evluate _module.args, as it is used during the computation
    # of the modules list.
    "_module.args"

    # For some reasons which we yet have to investigate, some options cannot
    # be replaced by a throw without causing a non-catchable failure.
    "networking.bonds"
    "networking.bridges"
    "networking.interfaces"
    "networking.macvlans"
    "networking.sits"
    "networking.vlans"
    "services.openssh.startWhenNeeded"
  ];

  # for some reasons which we yet have to investigate, some options are
  # time-consuming to compute, thus we filter them out at the moment.
  excludedOptions = [
    "boot.systemd.services"
    "systemd.services"
    "kde.extraPackages"
  ];
  excludeOptions = list:
    filter (opt: !(elem (showOption opt.loc) excludedOptions)) list;


  reportNewFailures = old: new:
    let
      filterChanges =
        filter ({fst, snd}:
          !(fst.success -> snd.success)
        );

      keepNames =
        map ({fst, snd}:
          /* assert fst.name == snd.name; */ snd.name
        );

      # Use  tryEval (strict ...)  to know if there is any failure while
      # evaluating the option value.
      #
      # Note, the `strict` function is not strict enough, but using toXML
      # builtins multiply by 4 the memory usage and the time used to compute
      # each options.
      tryCollectOptions = moduleResult:
        forEach (excludeOptions (collect isOption moduleResult)) (opt:
          { name = showOption opt.loc; } // builtins.tryEval (strict opt.value));
     in
       keepNames (
         filterChanges (
           zipLists (tryCollectOptions old) (tryCollectOptions new)
         )
       );


  # Create a list of modules where each module contains only one failling
  # options.
  introspectionModules =
    let
      setIntrospection = opt: rec {
        name = showOption opt.loc;
        path = opt.loc;
        config = setAttrByPath path
          (throw "Usage introspection of '${name}' by forced failure.");
      };
    in
      map setIntrospection (collect isOption eval.options);

  overrideConfig = thrower:
    recursiveUpdateUntil (path: old: new:
      path == thrower.path
    ) eval.config thrower.config;


  graph =
    map (thrower: {
      option = thrower.name;
      usedBy = assert __trace "Investigate ${thrower.name}" true;
        reportNewFailures eval.options (evalFun {
          specialArgs = {
            config = overrideConfig thrower;
          };
        }).options;
    }) introspectionModules;

  displayOptionsGraph =
     let
       checkList =
         if testOption != null then [ testOption ]
         else testOptions;
       checkAll = checkList == [];
     in
       flip filter graph ({option, ...}:
         (checkAll || elem option checkList)
         && !(elem option excludedTestOptions)
       );

  graphToDot = graph: ''
    digraph "Option Usages" {
      ${concatMapStrings ({option, usedBy}:
          concatMapStrings (user: ''
            "${option}" -> "${user}"''
          ) usedBy
        ) displayOptionsGraph}
    }
  '';

  graphToText = graph:
    concatMapStrings ({usedBy, ...}:
        concatMapStrings (user: ''
          ${user}
        '') usedBy
      ) displayOptionsGraph;

in

rec {
  dotContent = graphToDot graph;
  dot = pkgs.writeTextFile {
    name = "option_usages.dot";
    text = dotContent;
  };

  pdf = pkgs.texFunctions.dot2pdf {
    dotGraph = dot;
  };

  txtContent = graphToText graph;
  txt = pkgs.writeTextFile {
    name = "option_usages.txt";
    text = txtContent;
  };
}