{ runCommandLocal, nix, lib }:

# Replace a single dependency in the requisites tree of drv, propagating
# the change all the way up the tree, without a full rebuild. This can be
# useful, for example, to patch a security hole in libc and still use your
# system safely without rebuilding the world. This should be a short term
# solution, as soon as a rebuild can be done the properly rebuild derivation
# should be used. The old dependency and new dependency MUST have the same-length
# name, and ideally should have close-to-identical directory layout.
#
# Example: safeFirefox = replaceDependency {
#   drv = firefox;
#   oldDependency = glibc;
#   newDependency = overrideDerivation glibc (attrs: {
#     patches  = attrs.patches ++ [ ./fix-glibc-hole.patch ];
#   });
# };
# This will rebuild glibc with your security patch, then copy over firefox
# (and all of its dependencies) without rebuilding further.
{ drv, oldDependency, newDependency, verbose ? true }:

let
  inherit (lib)
    any
    attrNames
    concatStringsSep
    elem
    filter
    filterAttrs
    listToAttrs
    mapAttrsToList
    stringLength
    substring
    ;

  warn = if verbose then builtins.trace else (x: y: y);
  references = import (runCommandLocal "references.nix" { exportReferencesGraph = [ "graph" drv ]; } ''
    (echo {
    while read path
    do
        echo "  \"$path\" = ["
        read count
        read count
        while [ "0" != "$count" ]
        do
            read ref_path
            if [ "$ref_path" != "$path" ]
            then
                echo "    (builtins.storePath (/. + \"$ref_path\"))"
            fi
            count=$(($count - 1))
        done
        echo "  ];"
    done < graph
    echo }) > $out
  '').outPath;

  discard = builtins.unsafeDiscardStringContext;

  oldStorepath = builtins.storePath (discard (toString oldDependency));

  referencesOf = drv: references.${discard (toString drv)};

  dependsOnOldMemo = listToAttrs (map
    (drv: { name = discard (toString drv);
            value = elem oldStorepath (referencesOf drv) ||
                    any dependsOnOld (referencesOf drv);
          }) (attrNames references));

  dependsOnOld = drv: dependsOnOldMemo.${discard (toString drv)};

  drvName = drv:
    discard (substring 33 (stringLength (builtins.baseNameOf drv)) (builtins.baseNameOf drv));

  rewriteHashes = drv: hashes: runCommandLocal (drvName drv) { nixStore = "${nix.out}/bin/nix-store"; } ''
    $nixStore --dump ${drv} | sed 's|${baseNameOf drv}|'$(basename $out)'|g' | sed -e ${
      concatStringsSep " -e " (mapAttrsToList (name: value:
        "'s|${baseNameOf name}|${baseNameOf value}|g'"
      ) hashes)
    } | $nixStore --restore $out
  '';

  rewrittenDeps = listToAttrs [ {name = discard (toString oldDependency); value = newDependency;} ];

  rewriteMemo = listToAttrs (map
    (drv: { name = discard (toString drv);
            value = rewriteHashes (builtins.storePath drv)
              (filterAttrs (n: v: elem (builtins.storePath (discard (toString n))) (referencesOf drv)) rewriteMemo);
          })
    (filter dependsOnOld (attrNames references))) // rewrittenDeps;

  drvHash = discard (toString drv);
in assert (stringLength (drvName (toString oldDependency)) == stringLength (drvName (toString newDependency)));
rewriteMemo.${drvHash} or (warn "replace-dependency.nix: Derivation ${drvHash} does not depend on ${discard (toString oldDependency)}" drv)
