jetbrains: 2024.1 -> 2024.2.7 (#351041)
[NixPkgs.git] / nixos / maintainers / option-usages.nix
blobe9bafa21a58ae08b3f70148a3f3017d077b5c295
1 { configuration ? import ../lib/from-env.nix "NIXOS_CONFIG" <nixos-config>
3 # provide an option name, as a string literal.
4 , testOption ? null
6 # provide a list of option names, as string literals.
7 , testOptions ? [ ]
8 }:
10 # This file is made to be used as follow:
12 #   $ nix-instantiate ./option-usages.nix --argstr testOption service.xserver.enable -A txtContent --eval
14 # or
16 #   $ nix-build ./option-usages.nix --argstr testOption service.xserver.enable -A txt -o service.xserver.enable._txt
18 # Other targets exists such as `dotContent`, `dot`, and `pdf`.  If you are
19 # looking for the option usage of multiple options, you can provide a list
20 # as argument.
22 #   $ nix-build ./option-usages.nix --arg testOptions \
23 #      '["boot.loader.gummiboot.enable" "boot.loader.gummiboot.timeout"]' \
24 #      -A txt -o gummiboot.list
26 # Note, this script is slow as it has to evaluate all options of the system
27 # once per queried option.
29 # This nix expression works by doing a first evaluation, which evaluates the
30 # result of every option.
32 # Then, for each queried option, we evaluate the NixOS modules a second
33 # time, except that we replace the `config` argument of all the modules with
34 # the result of the original evaluation, except for the tested option which
35 # value is replaced by a `throw` statement which is caught by the `tryEval`
36 # evaluation of each option value.
38 # We then compare the result of the evaluation of the original module, with
39 # the result of the second evaluation, and consider that the new failures are
40 # caused by our mutation of the `config` argument.
42 # Doing so returns all option results which are directly using the
43 # tested option result.
45 with import ../../lib;
47 let
49   evalFun = {
50     specialArgs ? {}
51   }: import ../lib/eval-config.nix {
52        modules = [ configuration ];
53        inherit specialArgs;
54      };
56   eval = evalFun {};
57   inherit (eval) pkgs;
59   excludedTestOptions = [
60     # We cannot evluate _module.args, as it is used during the computation
61     # of the modules list.
62     "_module.args"
64     # For some reasons which we yet have to investigate, some options cannot
65     # be replaced by a throw without causing a non-catchable failure.
66     "networking.bonds"
67     "networking.bridges"
68     "networking.interfaces"
69     "networking.macvlans"
70     "networking.sits"
71     "networking.vlans"
72     "services.openssh.startWhenNeeded"
73   ];
75   # for some reasons which we yet have to investigate, some options are
76   # time-consuming to compute, thus we filter them out at the moment.
77   excludedOptions = [
78     "boot.systemd.services"
79     "systemd.services"
80     "kde.extraPackages"
81   ];
82   excludeOptions = list:
83     filter (opt: !(elem (showOption opt.loc) excludedOptions)) list;
86   reportNewFailures = old: new:
87     let
88       filterChanges =
89         filter ({fst, snd}:
90           !(fst.success -> snd.success)
91         );
93       keepNames =
94         map ({fst, snd}:
95           /* assert fst.name == snd.name; */ snd.name
96         );
98       # Use  tryEval (strict ...)  to know if there is any failure while
99       # evaluating the option value.
100       #
101       # Note, the `strict` function is not strict enough, but using toXML
102       # builtins multiply by 4 the memory usage and the time used to compute
103       # each options.
104       tryCollectOptions = moduleResult:
105         forEach (excludeOptions (collect isOption moduleResult)) (opt:
106           { name = showOption opt.loc; } // builtins.tryEval (strict opt.value));
107      in
108        keepNames (
109          filterChanges (
110            zipLists (tryCollectOptions old) (tryCollectOptions new)
111          )
112        );
115   # Create a list of modules where each module contains only one failling
116   # options.
117   introspectionModules =
118     let
119       setIntrospection = opt: rec {
120         name = showOption opt.loc;
121         path = opt.loc;
122         config = setAttrByPath path
123           (throw "Usage introspection of '${name}' by forced failure.");
124       };
125     in
126       map setIntrospection (collect isOption eval.options);
128   overrideConfig = thrower:
129     recursiveUpdateUntil (path: old: new:
130       path == thrower.path
131     ) eval.config thrower.config;
134   graph =
135     map (thrower: {
136       option = thrower.name;
137       usedBy = assert __trace "Investigate ${thrower.name}" true;
138         reportNewFailures eval.options (evalFun {
139           specialArgs = {
140             config = overrideConfig thrower;
141           };
142         }).options;
143     }) introspectionModules;
145   displayOptionsGraph =
146      let
147        checkList =
148          if testOption != null then [ testOption ]
149          else testOptions;
150        checkAll = checkList == [];
151      in
152        flip filter graph ({option, ...}:
153          (checkAll || elem option checkList)
154          && !(elem option excludedTestOptions)
155        );
157   graphToDot = graph: ''
158     digraph "Option Usages" {
159       ${concatMapStrings ({option, usedBy}:
160           concatMapStrings (user: ''
161             "${option}" -> "${user}"''
162           ) usedBy
163         ) displayOptionsGraph}
164     }
165   '';
167   graphToText = graph:
168     concatMapStrings ({usedBy, ...}:
169         concatMapStrings (user: ''
170           ${user}
171         '') usedBy
172       ) displayOptionsGraph;
176 rec {
177   dotContent = graphToDot graph;
178   dot = pkgs.writeTextFile {
179     name = "option_usages.dot";
180     text = dotContent;
181   };
183   pdf = pkgs.texFunctions.dot2pdf {
184     dotGraph = dot;
185   };
187   txtContent = graphToText graph;
188   txt = pkgs.writeTextFile {
189     name = "option_usages.txt";
190     text = txtContent;
191   };