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