0f8596ab3f
See `forEach`-introduction commit. ``` rg 'flip map ' --files-with-matches | xargs sed -i 's/flip map /forEach /g' ```
193 lines
5.4 KiB
Nix
193 lines
5.4 KiB
Nix
{ 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;
|
|
};
|
|
}
|