Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/top-level: improve replaceRuntimeDependencies #257234

Merged
merged 8 commits into from
Sep 24, 2024
83 changes: 55 additions & 28 deletions nixos/modules/system/activation/top-level.nix
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,19 @@ let
else showWarnings config.warnings baseSystem;

# Replace runtime dependencies
system = foldr ({ oldDependency, newDependency }: drv:
pkgs.replaceDependency { inherit oldDependency newDependency drv; }
) baseSystemAssertWarn config.system.replaceRuntimeDependencies;
system = let inherit (config.system.replaceDependencies) replacements cutoffPackages; in
if replacements == [] then
# Avoid IFD if possible, by sidestepping replaceDependencies if no replacements are specified.
baseSystemAssertWarn
else
(pkgs.replaceDependencies.override {
replaceDirectDependencies = pkgs.replaceDirectDependencies.override {
nix = config.nix.package;
};
}) {
drv = baseSystemAssertWarn;
inherit replacements cutoffPackages;
};

systemWithBuildDeps = system.overrideAttrs (o: {
systemBuildClosure = pkgs.closureInfo { rootPaths = [ system.drvPath ]; };
Expand All @@ -87,6 +97,7 @@ in
(mkRemovedOptionModule [ "nesting" "clone" ] "Use `specialisation.«name» = { inheritParentConfig = true; configuration = { ... }; }` instead.")
(mkRemovedOptionModule [ "nesting" "children" ] "Use `specialisation.«name».configuration = { ... }` instead.")
(mkRenamedOptionModule [ "system" "forbiddenDependenciesRegex" ] [ "system" "forbiddenDependenciesRegexes" ])
(mkRenamedOptionModule [ "system" "replaceRuntimeDependencies" ] [ "system" "replaceDependencies" "replacements" ])
];

options = {
Expand Down Expand Up @@ -205,31 +216,47 @@ in
'';
};

system.replaceRuntimeDependencies = mkOption {
default = [];
example = lib.literalExpression "[ ({ original = pkgs.openssl; replacement = pkgs.callPackage /path/to/openssl { }; }) ]";
type = types.listOf (types.submodule (
{ ... }: {
options.original = mkOption {
type = types.package;
description = "The original package to override.";
};

options.replacement = mkOption {
type = types.package;
description = "The replacement package.";
};
})
);
apply = map ({ original, replacement, ... }: {
oldDependency = original;
newDependency = replacement;
});
description = ''
List of packages to override without doing a full rebuild.
The original derivation and replacement derivation must have the same
name length, and ideally should have close-to-identical directory layout.
'';
system.replaceDependencies = {
replacements = mkOption {
default = [];
example = lib.literalExpression "[ ({ oldDependency = pkgs.openssl; newDependency = pkgs.callPackage /path/to/openssl { }; }) ]";
type = types.listOf (types.submodule (
{ ... }: {
imports = [
(mkRenamedOptionModule [ "original" ] [ "oldDependency" ])
(mkRenamedOptionModule [ "replacement" ] [ "newDependency" ])
];

options.oldDependency = mkOption {
type = types.package;
description = "The original package to override.";
};

options.newDependency = mkOption {
type = types.package;
description = "The replacement package.";
};
})
);
apply = map ({ oldDependency, newDependency, ... }: {
inherit oldDependency newDependency;
});
description = ''
List of packages to override without doing a full rebuild.
The original derivation and replacement derivation must have the same
name length, and ideally should have close-to-identical directory layout.
'';
};

cutoffPackages = mkOption {
default = [ config.system.build.initialRamdisk ];
defaultText = literalExpression "[ config.system.build.initialRamdisk ]";
type = types.listOf types.package;
description = ''
Packages to which no replacements should be applied.
The initrd is matched by default, because its structure renders the replacement process ineffective and prone to breakage.
'';
};
};

system.name = mkOption {
Expand Down
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,7 @@ in {
redlib = handleTest ./redlib.nix {};
redmine = handleTest ./redmine.nix {};
renovate = handleTest ./renovate.nix {};
replace-dependencies = handleTest ./replace-dependencies {};
restartByActivationScript = handleTest ./restart-by-activation-script.nix {};
restic-rest-server = handleTest ./restic-rest-server.nix {};
restic = handleTest ./restic.nix {};
Expand Down
19 changes: 19 additions & 0 deletions nixos/tests/replace-dependencies/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import ../make-test-python.nix (
{ pkgs, ... }:
{
name = "replace-dependencies";
meta.maintainers = with pkgs.lib.maintainers; [ alois31 ];

nodes.machine =
{ ... }:
{
nix.settings.experimental-features = [ "ca-derivations" ];
system.extraDependencies = [ pkgs.stdenvNoCC ];
};

testScript = ''
start_all()
machine.succeed("nix-build --option substitute false ${pkgs.path}/nixos/tests/replace-dependencies/guest.nix")
'';
}
)
149 changes: 149 additions & 0 deletions nixos/tests/replace-dependencies/guest.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# This needs to be run in a NixOS test, because Hydra cannot do IFD.
let
pkgs = import ../../.. { };
inherit (pkgs)
runCommand
writeShellScriptBin
replaceDependency
replaceDependencies
;
inherit (pkgs.lib) escapeShellArg;
mkCheckOutput =
name: test: output:
runCommand name { } ''
actualOutput="$(${escapeShellArg "${test}/bin/test"})"
if [ "$(${escapeShellArg "${test}/bin/test"})" != ${escapeShellArg output} ]; then
echo >&2 "mismatched output: expected \""${escapeShellArg output}"\", got \"$actualOutput\""
exit 1
fi
touch "$out"
'';
oldDependency = writeShellScriptBin "dependency" ''
echo "got old dependency"
'';
oldDependency-ca = oldDependency.overrideAttrs { __contentAddressed = true; };
newDependency = writeShellScriptBin "dependency" ''
echo "got new dependency"
'';
newDependency-ca = newDependency.overrideAttrs { __contentAddressed = true; };
basic = writeShellScriptBin "test" ''
${oldDependency}/bin/dependency
'';
basic-ca = writeShellScriptBin "test" ''
${oldDependency-ca}/bin/dependency
'';
transitive = writeShellScriptBin "test" ''
${basic}/bin/test
'';
weirdDependency = writeShellScriptBin "dependency" ''
echo "got weird dependency"
${basic}/bin/test
'';
oldDependency1 = writeShellScriptBin "dependency1" ''
echo "got old dependency 1"
'';
newDependency1 = writeShellScriptBin "dependency1" ''
echo "got new dependency 1"
'';
oldDependency2 = writeShellScriptBin "dependency2" ''
${oldDependency1}/bin/dependency1
echo "got old dependency 2"
'';
newDependency2 = writeShellScriptBin "dependency2" ''
${oldDependency1}/bin/dependency1
echo "got new dependency 2"
'';
deep = writeShellScriptBin "test" ''
${oldDependency2}/bin/dependency2
'';
in
{
replacedependency-basic = mkCheckOutput "replacedependency-basic" (replaceDependency {
drv = basic;
inherit oldDependency newDependency;
}) "got new dependency";

replacedependency-basic-old-ca = mkCheckOutput "replacedependency-basic" (replaceDependency {
drv = basic-ca;
oldDependency = oldDependency-ca;
inherit newDependency;
}) "got new dependency";

replacedependency-basic-new-ca = mkCheckOutput "replacedependency-basic" (replaceDependency {
drv = basic;
inherit oldDependency;
newDependency = newDependency-ca;
}) "got new dependency";

replacedependency-transitive = mkCheckOutput "replacedependency-transitive" (replaceDependency {
drv = transitive;
inherit oldDependency newDependency;
}) "got new dependency";

replacedependency-weird =
mkCheckOutput "replacedependency-weird"
(replaceDependency {
drv = basic;
inherit oldDependency;
newDependency = weirdDependency;
})
''
got weird dependency
got old dependency'';

replacedependencies-precedence = mkCheckOutput "replacedependencies-precedence" (replaceDependencies
{
drv = basic;
replacements = [ { inherit oldDependency newDependency; } ];
cutoffPackages = [ oldDependency ];
}
) "got new dependency";

replacedependencies-self = mkCheckOutput "replacedependencies-self" (replaceDependencies {
drv = basic;
replacements = [
{
inherit oldDependency;
newDependency = oldDependency;
}
];
}) "got old dependency";

replacedependencies-deep-order1 =
mkCheckOutput "replacedependencies-deep-order1"
(replaceDependencies {
drv = deep;
replacements = [
{
oldDependency = oldDependency1;
newDependency = newDependency1;
}
{
oldDependency = oldDependency2;
newDependency = newDependency2;
}
];
})
''
got new dependency 1
got new dependency 2'';

replacedependencies-deep-order2 =
mkCheckOutput "replacedependencies-deep-order2"
(replaceDependencies {
drv = deep;
replacements = [
{
oldDependency = oldDependency2;
newDependency = newDependency2;
}
{
oldDependency = oldDependency1;
newDependency = newDependency1;
}
];
})
''
got new dependency 1
got new dependency 2'';
}
Loading