at 18.03-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 "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}