at 17.09-beta 5.5 kB view raw
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}