depot/third_party/nixpkgs/pkgs/build-support/replace-dependencies.nix
Default email 7e47f3658e Project import generated by Copybara.
GitOrigin-RevId: 1925c603f17fc89f4c8f6bf6f631a802ad85d784
2024-09-26 11:04:55 +00:00

193 lines
6.6 KiB
Nix

{
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}