tvl-depot/nix/dependency-analyzer/default.nix
sterni 3fab3b780b feat(nix/dependency-analyzer): improved directDrvDeps for Nix >= 2.6
This codepath will basically never be used in depot, but I want to add
it as kind of a note to myself. It's kind of a neat feature, although
I'm not quite sure it is going to stick around.

Change-Id: If0e26ef47bdedc6dbf3d048ad4fc9a3a1fd6c5a2
Reviewed-on: https://cl.tvl.fyi/c/depot/+/6833
Reviewed-by: grfn <grfn@gws.fyi>
Tested-by: BuildkiteCI
2022-10-08 10:59:45 +00:00

268 lines
8.8 KiB
Nix

{ lib, depot, pkgs, ... }:
let
inherit (builtins) unsafeDiscardStringContext appendContext;
#
# Utilities
#
# Manipulate string context of the given string so that it only carries a
# `path` reference to itself (so it needs to be a string representation of
# a store path).
#
# This is intended for use on the `drvPath` attribute of derivations which by
# default carries a reference to the corresponding outputs. If we only want to
# read from the `drvPath`, having only a `path` reference makes sure we don't
# need to realise the derivation first.
#
# Type: str -> str
pathContextDrvPath = drvPath:
let
drvPath' = unsafeDiscardStringContext drvPath;
in
appendContext drvPath' { ${drvPath'} = { path = true; }; };
# 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: 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: pathContextDrvPath 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 = pathContextDrvPath 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;
}