{
  lib,
  runCommandLocal,
  replaceDirectDependencies,
}:

# Replace some dependencies in the requisites tree of drv, propagating the change all the way up the tree, even within other replacements, 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 rebuilt derivation should be used.
# Each old dependency and the corresponding new dependency MUST have the same-length name, and ideally should have close-to-identical directory layout.
#
# Example: safeFirefox = replaceDependencies {
#   drv = firefox;
#   replacements = [
#     {
#       oldDependency = glibc;
#       newDependency = glibc.overrideAttrs (oldAttrs: {
#         patches = oldAttrs.patches ++ [ ./fix-glibc-hole.patch ];
#       });
#     }
#     {
#       oldDependency = libwebp;
#       newDependency = libwebp.overrideAttrs (oldAttrs: {
#         patches = oldAttrs.patches ++ [ ./fix-libwebp-hole.patch ];
#       });
#     }
#   ];
# };
# This will first rebuild glibc and libwebp with your security patches.
# Then it copies over firefox (and all of its dependencies) without rebuilding further.
# In particular, the glibc dependency of libwebp will be replaced by the patched version as well.
#
# In rare cases, it is possible for the replacement process to cause breakage (for example due to checksum mismatch).
# The cutoffPackages argument can be used to exempt the problematic packages from the replacement process.
{
  drv,
  replacements,
  cutoffPackages ? [ ],
  verbose ? true,
}:

let
  inherit (builtins) unsafeDiscardStringContext appendContext;
  inherit (lib)
    listToAttrs
    isStorePath
    readFile
    attrValues
    mapAttrs
    filter
    hasAttr
    mapAttrsToList
    ;
  inherit (lib.attrsets) mergeAttrsList;

  toContextlessString = x: unsafeDiscardStringContext (toString x);
  warn = if verbose then lib.warn else (x: y: y);

  referencesOf =
    drv:
    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 "    \"$ref_path\""
                  fi
                  count=$(($count - 1))
              done
              echo "  ];"
          done < graph
          echo }) > $out
        ''
      ).outPath;

  realisation =
    drv:
    if isStorePath drv then
      # Input-addressed and fixed-output derivations have their realisation as outPath.
      toContextlessString drv
    else
      # Floating and deferred derivations have a placeholder outPath.
      # The realisation can only be obtained by performing an actual build.
      unsafeDiscardStringContext (
        readFile (
          runCommandLocal "realisation"
            {
              env = {
                inherit drv;
              };
            }
            ''
              echo -n "$drv" > $out
            ''
        )
      );
  rootReferences = referencesOf drv;
  relevantReplacements = filter (
    { oldDependency, newDependency }:
    if toString oldDependency == toString newDependency then
      warn "replaceDependencies: attempting to replace dependency ${oldDependency} of ${drv} with itself"
        # Attempting to replace a dependency by itself is completely useless, and would only lead to infinite recursion.
        # Hence it must not be attempted to apply this replacement in any case.
        false
    else if !hasAttr (realisation oldDependency) rootReferences then
      warn "replaceDependencies: ${drv} does not depend on ${oldDependency}, so it will not be replaced"
        # Strictly speaking, another replacement could introduce the dependency.
        # However, handling this corner case would add significant complexity.
        # So we just leave it to the user to apply the replacement at the correct place, but show a warning to let them know.
        false
    else
      true
  ) replacements;
  targetDerivations = [ drv ] ++ map ({ newDependency, ... }: newDependency) relevantReplacements;
  referencesMemo = listToAttrs (
    map (drv: {
      name = realisation drv;
      value = referencesOf drv;
    }) targetDerivations
  );
  relevantReferences = mergeAttrsList (attrValues referencesMemo);
  # Make sure a derivation is returned even when no replacements are actually applied.
  # Yes, even in the stupid edge case where the root derivation itself is replaced.
  storePathOrKnownTargetDerivationMemo =
    mapAttrs (
      drv: _references:
      # builtins.storePath does not work in pure evaluation mode, even though it is not impure.
      # This reimplementation in Nix works as long as the path is already allowed in the evaluation state.
      # This is always the case here, because all paths come from the closure of the original derivation.
      appendContext drv { ${drv}.path = true; }
    ) relevantReferences
    // listToAttrs (
      map (drv: {
        name = realisation drv;
        value = drv;
      }) targetDerivations
    );

  rewriteMemo =
    # Mind the order of how the three attrsets are merged here.
    # The order of precedence needs to be "explicitly specified replacements" > "rewrite exclusion (cutoffPackages)" > "rewrite".
    # So the attrset merge order is the opposite.
    mapAttrs (
      drv: references:
      let
        rewrittenReferences = filter (dep: dep != drv && toString rewriteMemo.${dep} != dep) references;
        rewrites = listToAttrs (
          map (reference: {
            name = reference;
            value = rewriteMemo.${reference};
          }) rewrittenReferences
        );
      in
      replaceDirectDependencies {
        drv = storePathOrKnownTargetDerivationMemo.${drv};
        replacements = mapAttrsToList (name: value: {
          oldDependency = name;
          newDependency = value;
        }) rewrites;
      }
    ) relevantReferences
    // listToAttrs (
      map (drv: {
        name = realisation drv;
        value = storePathOrKnownTargetDerivationMemo.${realisation drv};
      }) cutoffPackages
    )
    // listToAttrs (
      map (
        { oldDependency, newDependency }:
        {
          name = realisation oldDependency;
          value = rewriteMemo.${realisation newDependency};
        }
      ) relevantReplacements
    );
in
rewriteMemo.${realisation drv}