nixpkgs/nixos/maintainers/option-usages.nix
Silvan Mosberger 4f0dadbf38 treewide: format all inactive Nix files
After final improvements to the official formatter implementation,
this commit now performs the first treewide reformat of Nix files using it.
This is part of the implementation of RFC 166.

Only "inactive" files are reformatted, meaning only files that
aren't being touched by any PR with activity in the past 2 months.
This is to avoid conflicts for PRs that might soon be merged.
Later we can do a full treewide reformat to get the rest,
which should not cause as many conflicts.

A CI check has already been running for some time to ensure that new and
already-formatted files are formatted, so the files being reformatted here
should also stay formatted.

This commit was automatically created and can be verified using

    nix-build a08b3a4d19.tar.gz \
      --argstr baseRev b32a094368
    result/bin/apply-formatting $NIXPKGS_PATH
2024-12-10 20:26:33 +01:00

188 lines
5.3 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-usages.nix --argstr testOption service.xserver.enable -A txtContent --eval
#
# or
#
# $ nix-build ./option-usages.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-usages.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;
};
}