diff options
Diffstat (limited to 'nixos/maintainers/option-usages.nix')
-rw-r--r-- | nixos/maintainers/option-usages.nix | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/nixos/maintainers/option-usages.nix b/nixos/maintainers/option-usages.nix new file mode 100644 index 00000000000..11247666ecd --- /dev/null +++ b/nixos/maintainers/option-usages.nix @@ -0,0 +1,192 @@ +{ configuration ? import ../lib/from-env.nix "NIXOS_CONFIG" <nixos-config> + +# provide an option name, as a string literal. +, testOption ? null + +# provide a list of option names, as string literals. +, testOptions ? [ ] +}: + +# This file is made to be used as follow: +# +# $ nix-instantiate ./option-usage.nix --argstr testOption service.xserver.enable -A txtContent --eval +# +# or +# +# $ nix-build ./option-usage.nix --argstr testOption service.xserver.enable -A txt -o service.xserver.enable._txt +# +# Other targets exists such as `dotContent`, `dot`, and `pdf`. If you are +# looking for the option usage of multiple options, you can provide a list +# as argument. +# +# $ nix-build ./option-usage.nix --arg testOptions \ +# '["boot.loader.gummiboot.enable" "boot.loader.gummiboot.timeout"]' \ +# -A txt -o gummiboot.list +# +# Note, this script is slow as it has to evaluate all options of the system +# once per queried option. +# +# This nix expression works by doing a first evaluation, which evaluates the +# result of every option. +# +# Then, for each queried option, we evaluate the NixOS modules a second +# time, except that we replace the `config` argument of all the modules with +# the result of the original evaluation, except for the tested option which +# value is replaced by a `throw` statement which is caught by the `tryEval` +# evaluation of each option value. +# +# We then compare the result of the evaluation of the original module, with +# the result of the second evaluation, and consider that the new failures are +# caused by our mutation of the `config` argument. +# +# Doing so returns all option results which are directly using the +# tested option result. + +with import ../../lib; + +let + + evalFun = { + specialArgs ? {} + }: import ../lib/eval-config.nix { + modules = [ configuration ]; + inherit specialArgs; + }; + + eval = evalFun {}; + inherit (eval) pkgs; + + excludedTestOptions = [ + # We cannot evluate _module.args, as it is used during the computation + # of the modules list. + "_module.args" + + # For some reasons which we yet have to investigate, some options cannot + # be replaced by a throw without causing a non-catchable failure. + "networking.bonds" + "networking.bridges" + "networking.interfaces" + "networking.macvlans" + "networking.sits" + "networking.vlans" + "services.openssh.startWhenNeeded" + ]; + + # for some reasons which we yet have to investigate, some options are + # time-consuming to compute, thus we filter them out at the moment. + excludedOptions = [ + "boot.systemd.services" + "systemd.services" + "kde.extraPackages" + ]; + excludeOptions = list: + filter (opt: !(elem (showOption opt.loc) excludedOptions)) list; + + + reportNewFailures = old: new: + let + filterChanges = + filter ({fst, snd}: + !(fst.success -> snd.success) + ); + + keepNames = + map ({fst, snd}: + /* assert fst.name == snd.name; */ snd.name + ); + + # Use tryEval (strict ...) to know if there is any failure while + # evaluating the option value. + # + # Note, the `strict` function is not strict enough, but using toXML + # builtins multiply by 4 the memory usage and the time used to compute + # each options. + tryCollectOptions = moduleResult: + forEach (excludeOptions (collect isOption moduleResult)) (opt: + { name = showOption opt.loc; } // builtins.tryEval (strict opt.value)); + in + keepNames ( + filterChanges ( + zipLists (tryCollectOptions old) (tryCollectOptions new) + ) + ); + + + # Create a list of modules where each module contains only one failling + # options. + introspectionModules = + let + setIntrospection = opt: rec { + name = showOption opt.loc; + path = opt.loc; + config = setAttrByPath path + (throw "Usage introspection of '${name}' by forced failure."); + }; + in + map setIntrospection (collect isOption eval.options); + + overrideConfig = thrower: + recursiveUpdateUntil (path: old: new: + path == thrower.path + ) eval.config thrower.config; + + + graph = + map (thrower: { + option = thrower.name; + usedBy = assert __trace "Investigate ${thrower.name}" true; + reportNewFailures eval.options (evalFun { + specialArgs = { + config = overrideConfig thrower; + }; + }).options; + }) introspectionModules; + + displayOptionsGraph = + let + checkList = + if testOption != null then [ testOption ] + else testOptions; + checkAll = checkList == []; + in + flip filter graph ({option, ...}: + (checkAll || elem option checkList) + && !(elem option excludedTestOptions) + ); + + graphToDot = graph: '' + digraph "Option Usages" { + ${concatMapStrings ({option, usedBy}: + concatMapStrings (user: '' + "${option}" -> "${user}"'' + ) usedBy + ) displayOptionsGraph} + } + ''; + + graphToText = graph: + concatMapStrings ({usedBy, ...}: + concatMapStrings (user: '' + ${user} + '') usedBy + ) displayOptionsGraph; + +in + +rec { + dotContent = graphToDot graph; + dot = pkgs.writeTextFile { + name = "option_usages.dot"; + text = dotContent; + }; + + pdf = pkgs.texFunctions.dot2pdf { + dotGraph = dot; + }; + + txtContent = graphToText graph; + txt = pkgs.writeTextFile { + name = "option_usages.txt"; + text = txtContent; + }; +} |