diff options
Diffstat (limited to 'pkgs/build-support/replace-dependencies.nix')
-rw-r--r-- | pkgs/build-support/replace-dependencies.nix | 193 |
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} |