Turns out that pathContextDrvPath already exists as a builtin which is very convenient. Actually somewhat embarassing that I missed this for so long. Change-Id: Ieb5e113d70dec548b3053911ff9dbe9ed48402be Reviewed-on: https://cl.tvl.fyi/c/depot/+/7050 Tested-by: BuildkiteCI Reviewed-by: tazjin <tazjin@tvl.su>
		
			
				
	
	
		
			258 lines
		
	
	
	
		
			8.5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			258 lines
		
	
	
	
		
			8.5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
{ lib, depot, pkgs, ... }:
 | 
						|
 | 
						|
let
 | 
						|
  inherit (builtins) unsafeDiscardStringContext appendContext;
 | 
						|
 | 
						|
  #
 | 
						|
  # Utilities
 | 
						|
  #
 | 
						|
 | 
						|
  # Determine all paths a derivation depends on, i.e. input derivations and
 | 
						|
  # files imported into the Nix store.
 | 
						|
  #
 | 
						|
  # Implementation for Nix < 2.6 is quite hacky at the moment.
 | 
						|
  #
 | 
						|
  # Type: str -> [str]
 | 
						|
  #
 | 
						|
  # TODO(sterni): clean this up and expose it
 | 
						|
  directDrvDeps =
 | 
						|
    if lib.versionAtLeast builtins.nixVersion "2.6"
 | 
						|
    then
 | 
						|
    # Since https://github.com/NixOS/nix/pull/1643, Nix apparently »preserves
 | 
						|
    # string context« through a readFile invocation. This has the side effect
 | 
						|
    # that it becomes possible to query the actual references a store path has.
 | 
						|
    # Not a 100% sure this is intended, but _very_ convenient for us here.
 | 
						|
      drvPath:
 | 
						|
      # if the passed path is not a derivation we can't necessarily get its
 | 
						|
      # dependencies, since it may not be representable as a Nix string due to
 | 
						|
      # NUL bytes, e.g. compressed patch files imported into the Nix store.
 | 
						|
      if builtins.match "^.+\\.drv$" drvPath == null
 | 
						|
      then [ ]
 | 
						|
      else builtins.attrNames (builtins.getContext (builtins.readFile drvPath))
 | 
						|
    else
 | 
						|
    # For Nix < 2.6 we have to rely on HACK, namely grepping for quoted store
 | 
						|
    # path references in the file. In the future this should be replaced by
 | 
						|
    # a proper derivation parser.
 | 
						|
      drvPath: builtins.concatLists (
 | 
						|
        builtins.filter builtins.isList (
 | 
						|
          builtins.split
 | 
						|
            "\"(${lib.escapeRegex builtins.storeDir}/[[:alnum:]+._?=-]+.drv)\""
 | 
						|
            (builtins.readFile drvPath)
 | 
						|
        )
 | 
						|
      );
 | 
						|
 | 
						|
  # Maps a list of derivation to the list of corresponding `drvPath`s.
 | 
						|
  #
 | 
						|
  # Type: [drv] -> [str]
 | 
						|
  drvsToPaths = drvs:
 | 
						|
    builtins.map (drv: builtins.unsafeDiscardOutputDependency drv.drvPath) drvs;
 | 
						|
 | 
						|
  #
 | 
						|
  # Calculate map of direct derivation dependencies
 | 
						|
  #
 | 
						|
 | 
						|
  # Create the dependency map entry for a given `drvPath` which mainly includes
 | 
						|
  # a list of other `drvPath`s it depends on. Additionally we store whether the
 | 
						|
  # derivation is `known`, i.e. part of the initial list of derivations we start
 | 
						|
  # generating the map from
 | 
						|
  #
 | 
						|
  # Type: bool -> string -> set
 | 
						|
  drvEntry = known: drvPath:
 | 
						|
    let
 | 
						|
      # key may not refer to a store path, …
 | 
						|
      key = unsafeDiscardStringContext drvPath;
 | 
						|
      # but we must read from the .drv file.
 | 
						|
      path = builtins.unsafeDiscardOutputDependency drvPath;
 | 
						|
    in
 | 
						|
    {
 | 
						|
      inherit key;
 | 
						|
      # trick so we can call listToAttrs directly on the result of genericClosure
 | 
						|
      name = key;
 | 
						|
      value = {
 | 
						|
        deps = directDrvDeps path;
 | 
						|
        inherit known;
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
  # Create an attribute set that maps every derivation in the combined
 | 
						|
  # dependency closure of the list of input derivation paths to every of their
 | 
						|
  # direct dependencies. Additionally every entry will have set their `known`
 | 
						|
  # attribute to `true` if it is in the list of input derivation paths.
 | 
						|
  #
 | 
						|
  # Type: [str] -> set
 | 
						|
  plainDrvDepMap = drvPaths:
 | 
						|
    builtins.listToAttrs (
 | 
						|
      builtins.genericClosure {
 | 
						|
        startSet = builtins.map (drvEntry true) drvPaths;
 | 
						|
        operator = { value, ... }: builtins.map (drvEntry false) value.deps;
 | 
						|
      }
 | 
						|
    );
 | 
						|
 | 
						|
  #
 | 
						|
  # Calculate closest known dependencies in the dependency map
 | 
						|
  #
 | 
						|
 | 
						|
  inherit (depot.nix.stateMonad)
 | 
						|
    after
 | 
						|
    bind
 | 
						|
    for_
 | 
						|
    get
 | 
						|
    getAttr
 | 
						|
    run
 | 
						|
    setAttr
 | 
						|
    pure
 | 
						|
    ;
 | 
						|
 | 
						|
  # This is an action in stateMonad which expects the (initial) state to have
 | 
						|
  # been produced by `plainDrvDepMap`. Given a `drvPath`, it calculates a
 | 
						|
  # `knownDeps` list which holds the `drvPath`s of the closest derivation marked
 | 
						|
  # as `known` along every edge. This list is inserted into the dependency map
 | 
						|
  # for `drvPath` and every other derivation in its dependecy closure (unless
 | 
						|
  # the information was already present). This means that the known dependency
 | 
						|
  # information for a derivation never has to be recalculated, as long as they
 | 
						|
  # are part of the same stateful computation.
 | 
						|
  #
 | 
						|
  # The upshot is that after calling `insertKnownDeps drvPath`,
 | 
						|
  # `fmap (builtins.getAttr "knownDeps") (getAttr drvPath)` will always succeed.
 | 
						|
  #
 | 
						|
  # Type: str -> stateMonad drvDepMap null
 | 
						|
  insertKnownDeps = drvPathWithContext:
 | 
						|
    let
 | 
						|
      # We no longer need to read from the store, so context is irrelevant, but
 | 
						|
      # we need to check for attr names which requires the absence of context.
 | 
						|
      drvPath = unsafeDiscardStringContext drvPathWithContext;
 | 
						|
    in
 | 
						|
    bind get (initDepMap:
 | 
						|
      # Get the dependency map's state before we've done anything to obtain the
 | 
						|
      # entry we'll be manipulating later as well as its dependencies.
 | 
						|
      let
 | 
						|
        entryPoint = initDepMap.${drvPath};
 | 
						|
 | 
						|
        # We don't need to recurse if our direct dependencies either have their
 | 
						|
        # knownDeps list already populated or are known dependencies themselves.
 | 
						|
        depsPrecalculated =
 | 
						|
          builtins.partition
 | 
						|
            (dep:
 | 
						|
              initDepMap.${dep}.known
 | 
						|
              || initDepMap.${dep} ? knownDeps
 | 
						|
            )
 | 
						|
            entryPoint.deps;
 | 
						|
 | 
						|
        # If a direct dependency is known, it goes right to our known dependency
 | 
						|
        # list. If it is unknown, we can copy its knownDeps list into our own.
 | 
						|
        initiallyKnownDeps =
 | 
						|
          builtins.concatLists (
 | 
						|
            builtins.map
 | 
						|
              (dep:
 | 
						|
                if initDepMap.${dep}.known
 | 
						|
                then [ dep ]
 | 
						|
                else initDepMap.${dep}.knownDeps
 | 
						|
              )
 | 
						|
              depsPrecalculated.right
 | 
						|
          );
 | 
						|
      in
 | 
						|
 | 
						|
      # If the information was already calculated before, we can exit right away
 | 
						|
      if entryPoint ? knownDeps
 | 
						|
      then pure null
 | 
						|
      else
 | 
						|
        after
 | 
						|
          # For all unknown direct dependencies which don't have a `knownDeps`
 | 
						|
          # list, we call ourselves recursively to populate it. Since this is
 | 
						|
          # done sequentially in the state monad, we avoid recalculating the
 | 
						|
          # list for the same derivation multiple times.
 | 
						|
          (for_
 | 
						|
            depsPrecalculated.wrong
 | 
						|
            insertKnownDeps)
 | 
						|
          # After this we can obtain the updated dependency map which will have
 | 
						|
          # a `knownDeps` list for all our direct dependencies and update the
 | 
						|
          # entry for the input `drvPath`.
 | 
						|
          (bind
 | 
						|
            get
 | 
						|
            (populatedDepMap:
 | 
						|
              (setAttr drvPath (entryPoint // {
 | 
						|
                knownDeps =
 | 
						|
                  lib.unique (
 | 
						|
                    initiallyKnownDeps
 | 
						|
                      ++ builtins.concatLists (
 | 
						|
                      builtins.map
 | 
						|
                        (dep: populatedDepMap.${dep}.knownDeps)
 | 
						|
                        depsPrecalculated.wrong
 | 
						|
                    )
 | 
						|
                  );
 | 
						|
              }))))
 | 
						|
    );
 | 
						|
 | 
						|
  # This function puts it all together and is exposed via `__functor`.
 | 
						|
  #
 | 
						|
  # For a list of `drvPath`s, calculate an attribute set which maps every
 | 
						|
  # `drvPath` to a set of the following form:
 | 
						|
  #
 | 
						|
  #     {
 | 
						|
  #       known = true /* if it is in the list of input derivation paths */;
 | 
						|
  #       deps = [
 | 
						|
  #         /* list of derivation paths it depends on directly */
 | 
						|
  #       ];
 | 
						|
  #       knownDeps = [
 | 
						|
  #         /* list of the closest derivation paths marked as known this
 | 
						|
  #            derivation depends on.
 | 
						|
  #         */
 | 
						|
  #       ];
 | 
						|
  #     }
 | 
						|
  knownDrvDepMap = knownDrvPaths:
 | 
						|
    run
 | 
						|
      (plainDrvDepMap knownDrvPaths)
 | 
						|
      (after
 | 
						|
        (for_
 | 
						|
          knownDrvPaths
 | 
						|
          insertKnownDeps)
 | 
						|
        get);
 | 
						|
 | 
						|
  #
 | 
						|
  # Other things based on knownDrvDepMap
 | 
						|
  #
 | 
						|
 | 
						|
  # Create a SVG visualizing `knownDrvDepMap`. Nodes are identified by derivation
 | 
						|
  # name, so multiple entries can be collapsed if they have the same name.
 | 
						|
  #
 | 
						|
  # Type: [drv] -> drv
 | 
						|
  knownDependencyGraph = name: drvs:
 | 
						|
    let
 | 
						|
      justName = drvPath:
 | 
						|
        builtins.substring
 | 
						|
          (builtins.stringLength builtins.storeDir + 1 + 32 + 1)
 | 
						|
          (builtins.stringLength drvPath)
 | 
						|
          (unsafeDiscardStringContext drvPath);
 | 
						|
 | 
						|
      gv = pkgs.writeText "${name}-dependency-analysis.gv" ''
 | 
						|
        digraph depot {
 | 
						|
        ${
 | 
						|
          (lib.concatStringsSep "\n"
 | 
						|
          (lib.mapAttrsToList (name: value:
 | 
						|
            if !value.known then ""
 | 
						|
            else lib.concatMapStringsSep "\n"
 | 
						|
              (knownDep: "  \"${justName name}\" -> \"${justName knownDep}\"")
 | 
						|
              value.knownDeps
 | 
						|
          )
 | 
						|
          (depot.nix.dependency-analyzer (
 | 
						|
            drvsToPaths drvs
 | 
						|
          ))))
 | 
						|
        }
 | 
						|
        }
 | 
						|
      '';
 | 
						|
    in
 | 
						|
 | 
						|
    pkgs.runCommand "${name}-dependency-analysis.svg"
 | 
						|
      {
 | 
						|
        nativeBuildInputs = [
 | 
						|
          pkgs.buildPackages.graphviz
 | 
						|
        ];
 | 
						|
      }
 | 
						|
      "dot -Tsvg < ${gv} > $out";
 | 
						|
in
 | 
						|
 | 
						|
{
 | 
						|
  __functor = _: knownDrvDepMap;
 | 
						|
 | 
						|
  inherit knownDependencyGraph plainDrvDepMap drvsToPaths;
 | 
						|
}
 |