193 lines
6.6 KiB
Nix
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}
|