about summary refs log tree commit diff
path: root/pkgs/build-support/replace-dependencies.nix
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/build-support/replace-dependencies.nix')
-rw-r--r--pkgs/build-support/replace-dependencies.nix193
1 files changed, 193 insertions, 0 deletions
diff --git a/pkgs/build-support/replace-dependencies.nix b/pkgs/build-support/replace-dependencies.nix
new file mode 100644
index 000000000000..fe325b175fe7
--- /dev/null
+++ b/pkgs/build-support/replace-dependencies.nix
@@ -0,0 +1,193 @@
+{
+  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}