@@ -0,0 +1,193 @@
# These are some helpers for figuring out the derivations attributes of a
# derivation's runtime dependencies.
#
# Example:
# let pkgs = import <nixpkgs> {}; in runtimeReport pkgs.hello
#
# Spoiler: glibc is a runtime dependency. # TODO: link to article
# Let's call these "imports". They're functions used throughout the code.
# Nothing interesting here.
with rec
{ pkgs = import <nixpkgs> { } ;
inherit ( pkgs )
closureInfo
runCommandNoCC
writeText
jq
;
inherit ( pkgs . lib )
concatLists
concatMap
concatStringsSep
filter
isAttrs
isDerivation
isList
isString
mapAttrsToList
;
inherit ( builtins )
genericClosure
hasAttr
toJSON
typeOf
unsafeDiscardStringContext
;
} ;
let
# Create a "runtime report" of the runtime dependencies of `drv`. A "runtime
# report" is made up of smaller "dependency reports". A "dependency report" is
# a string describing the dependency, made from the dependency's derivation
# attributes. Here we use `mkReport` to make the report of any particular
# dependency.
#
# NOTE: we use the following terms:
#
# * "buildtime" to mean basically any derivation involved in the build of
# `drv`.
# * "buildtime-only" for the "buildtime" dependencies that _are not_
# referenced anymore by `drv`'s store entry.
# * "runtime" for the rest.
#
# The "runtime report" is created in two steps:
#
# * Generate reports for all the _buildtime_ dependencies with
# `buildtimeReports`.
# * Filter out the reports for buildtime-only dependencies.
#
# Most of the "buildtime" reports won't even be used, because most buildtime
# dependencies are buildtime-only dependencies. However Nix does not give us a
# way of retrieving the derivation attributes of runtime dependencies, but we
# can twist its arm to:
#
# * Give us the store paths of runtime dependencies (see `cinfo`).
# * Give us the derivation attributes of all the buildtime dependencies (see
# `buildtimeDerivations`).
#
# Here's the hack: `buildtimeReports` tags the reports with the (expected)
# store path of the "buildtime" dependency, which we cross check against the
# list of runtime store paths. If it's a match, we keep it. Otherwise, we
# discard it.
runtimeReport = drv :
runCommandNoCC "${ drv . name } -report" { buildInputs = [ jq ] ; }
# XXX: This is to avoid IFD
''
(
echo " ---------------------------------"
echo " | OFFICIAL REPORT |"
echo " | requested by: the lawyers |"
echo " | written by: yours truly |"
echo " | TOP SECRET - TOP SECRET |"
echo " ---------------------------------"
echo
echo "runtime dependencies of ${ drv . name } :"
# TODO: note: you can use `buildtimeReports` as well
cat ${ buildtimeReports drv } |\
jq -r --slurpfile runtime ${ cinfo drv } \
' # First, we strip away (path-)duplicates.
unique_by(.path)
# Then we map over each build-time derivation and use `select()`
# to keep only the ones that show up in $runtime
| map( # this little beauty checks if "obj.path" is in "runtime"
select(. as $obj | $runtime | any(.[] | . == $obj.path))
| .report)
| .[]'
) > $out
'' ;
# Creates reports for all of `drv`'s buildtime dependencies. Each element in
# the list has two fields:
#
# * path = "/nix/store/..."
# * report = "some report based on the dependency's derivation attributes"
buildtimeReports = drv : writeText "${ drv . name } -runtime" ( toJSON (
map ( obj :
# unsafe: optimization to avoid downloading unused deps
{ # XXX: we discard the context of the dependencies' store paths because
# they're only ever used for lookup. This matters when fetching a
# prebuilt final report -- there's no point downloading all of `drv`'s
# buildtime dependencies.
path = unsafeDiscardStringContext obj . key ;
report = mkReport obj . drv ;
}
)
( buildtimeDerivations drv ) # the heavy lifting is done somewhere else
) ) ;
# Returns a list of all of `drv0`'s inputs, a.k.a. buildtime dependencies.
# Elements in the list has two fields:
#
# * key: the store path of the input.
# * drv: the actual derivation object.
#
# There are no guarantees that this will find _all_ inputs, but it works well
# enough in practice.
#
buildtimeDerivations = drv0 :
let
# We include all the outputs because they each have different outPaths
drvOutputs = drv :
# XXX: some derivations, like stdenv, don't have "outputs"
if hasAttr "outputs" drv
then map ( output : drv . ${ output } ) drv . outputs
else [ drv ] ;
# Recurse into the derivation attributes to find new derivations
drvDeps = attrs :
mapAttrsToList ( k : v :
if isDerivation v then ( drvOutputs v )
else if isList v
then concatMap drvOutputs ( filter isDerivation v )
else [ ]
) attrs ;
in
# Walk through the whole DAG of dependencies, using the `outPath` as an
# index for the elements.
let wrap = drv : { key = drv . outPath ; inherit drv ; } ; in genericClosure
{ startSet = map wrap ( drvOutputs drv0 ) ;
operator = obj : map wrap
( concatLists ( drvDeps obj . drv . drvAttrs ) ) ;
} ;
# make a report. Would could also output a json object and process everything
# later on.
mkReport = drv :
let
license =
if hasAttr "meta" drv && hasAttr "license" drv . meta then
if isList drv . meta . license then
concatStringsSep ", " (
map renderLicense drv . meta . license )
else renderLicense drv . meta . license
else "no license" ;
maintainer =
if hasAttr "meta" drv && hasAttr "maintainers" drv . meta
then concatStringsSep ", " ( map ( m : m . name ) drv . meta . maintainers )
else "nobody" ;
in " - ${ drv . name } (${ license } ) maintained by ${ maintainer } " ;
# Basically pretty prints a license
renderLicense = license :
if isAttrs license then license . shortName
else if isString license then license
else abort "no idea how to handle license of type ${ typeOf license } " ;
# This is a wrapper around nixpkgs' `closureInfo`. It produces a JSON file
# containing a list of the store paths of `drv`'s runtime dependencies.
cinfo = drv : runCommandNoCC "${ drv . name } -cinfo"
{ buildInputs = [ jq ] ; }
# NOTE: we avoid IFD here as well
''
cat ${ closureInfo { rootPaths = [ drv ] ; } } /store-paths |\
grep -v "^$" |\
jq -R -s -c 'split("\n")' |\
jq -c 'map(select( length > 0 ))' > $out
'' ;
in runtimeReport pkgs . hello