5753fc14f5
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;
|
|
}
|