194 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			194 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
 | 
						|
#
 | 
						|
# otther target 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 evluation 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 cuasing 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"
 | 
						|
    "environment.gnome3.packageSet"
 | 
						|
    "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:
 | 
						|
        flip map (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 !(isNull testOption) then [ testOption ]
 | 
						|
         else testOptions;
 | 
						|
       checkAll = checkList == [];
 | 
						|
     in
 | 
						|
       flip filter graph ({option, usedBy}:
 | 
						|
         (checkAll || elem option checkList)
 | 
						|
         && !(elem option excludedTestOptions)
 | 
						|
       );
 | 
						|
 | 
						|
  graphToDot = graph: ''
 | 
						|
    digraph "Option Usages" {
 | 
						|
      ${concatMapStrings ({option, usedBy}:
 | 
						|
          concatMapStrings (user: ''
 | 
						|
            "${option}" -> "${user}"''
 | 
						|
          ) usedBy
 | 
						|
        ) displayOptionsGraph}
 | 
						|
    }
 | 
						|
  '';
 | 
						|
 | 
						|
  graphToText = graph:
 | 
						|
    concatMapStrings ({option, 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;
 | 
						|
  };
 | 
						|
}
 |