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 "environment.gnome3.packageSet"
81 "kde.extraPackages"
82 ];
83 excludeOptions = list:
84 filter (opt: !(elem (showOption opt.loc) excludedOptions)) list;
85
86
87 reportNewFailures = old: new:
88 let
89 filterChanges =
90 filter ({fst, snd}:
91 !(fst.success -> snd.success)
92 );
93
94 keepNames =
95 map ({fst, snd}:
96 /* assert fst.name == snd.name; */ snd.name
97 );
98
99 # Use tryEval (strict ...) to know if there is any failure while
100 # evaluating the option value.
101 #
102 # Note, the `strict` function is not strict enough, but using toXML
103 # builtins multiply by 4 the memory usage and the time used to compute
104 # each options.
105 tryCollectOptions = moduleResult:
106 flip map (excludeOptions (collect isOption moduleResult)) (opt:
107 { name = showOption opt.loc; } // builtins.tryEval (strict opt.value));
108 in
109 keepNames (
110 filterChanges (
111 zipLists (tryCollectOptions old) (tryCollectOptions new)
112 )
113 );
114
115
116 # Create a list of modules where each module contains only one failling
117 # options.
118 introspectionModules =
119 let
120 setIntrospection = opt: rec {
121 name = showOption opt.loc;
122 path = opt.loc;
123 config = setAttrByPath path
124 (throw "Usage introspection of '${name}' by forced failure.");
125 };
126 in
127 map setIntrospection (collect isOption eval.options);
128
129 overrideConfig = thrower:
130 recursiveUpdateUntil (path: old: new:
131 path == thrower.path
132 ) eval.config thrower.config;
133
134
135 graph =
136 map (thrower: {
137 option = thrower.name;
138 usedBy = assert __trace "Investigate ${thrower.name}" true;
139 reportNewFailures eval.options (evalFun {
140 specialArgs = {
141 config = overrideConfig thrower;
142 };
143 }).options;
144 }) introspectionModules;
145
146 displayOptionsGraph =
147 let
148 checkList =
149 if !(isNull testOption) then [ testOption ]
150 else testOptions;
151 checkAll = checkList == [];
152 in
153 flip filter graph ({option, usedBy}:
154 (checkAll || elem option checkList)
155 && !(elem option excludedTestOptions)
156 );
157
158 graphToDot = graph: ''
159 digraph "Option Usages" {
160 ${concatMapStrings ({option, usedBy}:
161 concatMapStrings (user: ''
162 "${option}" -> "${user}"''
163 ) usedBy
164 ) displayOptionsGraph}
165 }
166 '';
167
168 graphToText = graph:
169 concatMapStrings ({option, usedBy}:
170 concatMapStrings (user: ''
171 ${user}
172 '') usedBy
173 ) displayOptionsGraph;
174
175in
176
177rec {
178 dotContent = graphToDot graph;
179 dot = pkgs.writeTextFile {
180 name = "option_usages.dot";
181 text = dotContent;
182 };
183
184 pdf = pkgs.texFunctions.dot2pdf {
185 dotGraph = dot;
186 };
187
188 txtContent = graphToText graph;
189 txt = pkgs.writeTextFile {
190 name = "option_usages.txt";
191 text = txtContent;
192 };
193}