1{ configuration ? import ../lib/from-env.nix "NIXOS_CONFIG" <nixos-config>
2
3# provide an option name, as a string literal.
4, testOption ? null
5
6# provide a list of option names, as string literals.
7, testOptions ? [ ]
8}:
9
10# This file is made to be used as follow:
11#
12# $ nix-instantiate ./option-usage.nix --argstr testOption service.xserver.enable -A txtContent --eval
13#
14# or
15#
16# $ nix-build ./option-usage.nix --argstr testOption service.xserver.enable -A txt -o service.xserver.enable._txt
17#
18# otther target 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.
21#
22# $ nix-build ./option-usage.nix --arg testOptions \
23# '["boot.loader.gummiboot.enable" "boot.loader.gummiboot.timeout"]' \
24# -A txt -o gummiboot.list
25#
26# Note, this script is slow as it has to evaluate all options of the system
27# once per queried option.
28#
29# This nix expression works by doing a first evaluation, which evaluates the
30# result of every option.
31#
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.
37#
38# We then compare the result of the evluation 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.
41#
42# Doing so returns all option results which are directly using the
43# tested option result.
44
45with import ../../lib;
46
47let
48
49 evalFun = {
50 specialArgs ? {}
51 }: import ../lib/eval-config.nix {
52 modules = [ configuration ];
53 inherit specialArgs;
54 };
55
56 eval = evalFun {};
57 inherit (eval) pkgs;
58
59 excludedTestOptions = [
60 # We cannot evluate _module.args, as it is used during the computation
61 # of the modules list.
62 "_module.args"
63
64 # For some reasons which we yet have to investigate, some options cannot
65 # be replaced by a throw without cuasing 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 ];
74
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;
84
85
86 reportNewFailures = old: new:
87 let
88 filterChanges =
89 filter ({fst, snd}:
90 !(fst.success -> snd.success)
91 );
92
93 keepNames =
94 map ({fst, snd}:
95 /* assert fst.name == snd.name; */ snd.name
96 );
97
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 flip map (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 );
113
114
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);
127
128 overrideConfig = thrower:
129 recursiveUpdateUntil (path: old: new:
130 path == thrower.path
131 ) eval.config thrower.config;
132
133
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;
144
145 displayOptionsGraph =
146 let
147 checkList =
148 if !(isNull testOption) then [ testOption ]
149 else testOptions;
150 checkAll = checkList == [];
151 in
152 flip filter graph ({option, usedBy}:
153 (checkAll || elem option checkList)
154 && !(elem option excludedTestOptions)
155 );
156
157 graphToDot = graph: ''
158 digraph "Option Usages" {
159 ${concatMapStrings ({option, usedBy}:
160 concatMapStrings (user: ''
161 "${option}" -> "${user}"''
162 ) usedBy
163 ) displayOptionsGraph}
164 }
165 '';
166
167 graphToText = graph:
168 concatMapStrings ({option, usedBy}:
169 concatMapStrings (user: ''
170 ${user}
171 '') usedBy
172 ) displayOptionsGraph;
173
174in
175
176rec {
177 dotContent = graphToDot graph;
178 dot = pkgs.writeTextFile {
179 name = "option_usages.dot";
180 text = dotContent;
181 };
182
183 pdf = pkgs.texFunctions.dot2pdf {
184 dotGraph = dot;
185 };
186
187 txtContent = graphToText graph;
188 txt = pkgs.writeTextFile {
189 name = "option_usages.txt";
190 text = txtContent;
191 };
192}