at master 5.4 kB view raw
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}