aboutsummaryrefslogtreecommitdiff
path: root/nixpkgs/nixos/maintainers/option-usages.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/maintainers/option-usages.nix')
-rw-r--r--nixpkgs/nixos/maintainers/option-usages.nix192
1 files changed, 192 insertions, 0 deletions
diff --git a/nixpkgs/nixos/maintainers/option-usages.nix b/nixpkgs/nixos/maintainers/option-usages.nix
new file mode 100644
index 00000000000..11247666ecd
--- /dev/null
+++ b/nixpkgs/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;
+ };
+}