symlinkJoin: ability to strip prefix from tree (#345127)

This commit is contained in:
Philip Taron
2025-02-25 17:03:22 -08:00
committed by GitHub
3 changed files with 180 additions and 2 deletions

View File

@@ -3,7 +3,11 @@
let
inherit (lib)
optionalAttrs
optionalString
hasPrefix
warn
map
isList
;
in
@@ -468,6 +472,29 @@ rec {
...
To create a directory structure from a specific subdirectory of input `paths` instead of their full trees,
you can either append the subdirectory path to each input path, or use the `stripPrefix` argument to
remove the common prefix during linking.
Example:
# create symlinks of tmpfiles.d rules from multiple packages
symlinkJoin { name = "tmpfiles.d"; paths = [ pkgs.lvm2 pkgs.nix ]; stripPrefix = "/lib/tmpfiles.d"; }
This creates a derivation with a directory structure like the following:
/nix/store/m5s775yicb763hfa133jwml5hwmwzv14-tmpfiles.d
|-- lvm2.conf -> /nix/store/k6js0l5f0zpvrhay49579fj939j77p2w-lvm2-2.03.29/lib/tmpfiles.d/lvm2.conf
`-- nix-daemon.conf -> /nix/store/z4v2s3s3y79fmabhps5hakb3c5dwaj5a-nix-1.33.7/lib/tmpfiles.d/nix-daemon.conf
By default, packages that don't contain the specified subdirectory are silently skipped.
Set `failOnMissing = true` to make the build fail if any input package is missing the subdirectory
(this is the default behavior when not using stripPrefix).
symlinkJoin and linkFarm are similar functions, but they output
derivations with different structure.
@@ -490,15 +517,29 @@ rec {
"symlinkJoin requires either a `name` OR `pname` and `version`";
"${args_.pname}-${args_.version}"
, paths
, stripPrefix ? ""
, preferLocalBuild ? true
, allowSubstitutes ? false
, postBuild ? ""
, failOnMissing ? stripPrefix == ""
, ...
}:
assert lib.assertMsg (stripPrefix != "" -> (hasPrefix "/" stripPrefix && stripPrefix != "/")) ''
stripPrefix must be either an empty string (disable stripping behavior), or relative path prefixed with /.
Ensure that the path starts with / and specifies path to the subdirectory.
'';
let
args = removeAttrs args_ [ "name" "postBuild" ]
mapPaths = f: paths: map (path:
if path == null then null
else if isList path then mapPaths f path
else f path
) paths;
args = removeAttrs args_ [ "name" "postBuild" "stripPrefix" "paths" "failOnMissing" ]
// {
inherit preferLocalBuild allowSubstitutes;
paths = mapPaths (path: "${path}${stripPrefix}") paths;
passAsFile = [ "paths" ];
}; # pass the defaults
in
@@ -506,7 +547,7 @@ rec {
''
mkdir -p $out
for i in $(cat $pathsPath); do
${lndir}/bin/lndir -silent $i $out
${optionalString (!failOnMissing) "if test -d $i; then "}${lndir}/bin/lndir -silent $i $out${optionalString (!failOnMissing) "; fi"}
done
${postBuild}
'';

View File

@@ -19,6 +19,7 @@ in
recurseIntoAttrs {
concat = callPackage ./concat-test.nix {};
linkFarm = callPackage ./link-farm.nix {};
symlinkJoin = recurseIntoAttrs (callPackage ./symlink-join.nix {});
overriding = callPackage ../test-overriding.nix {};
inherit references;
writeCBin = callPackage ./writeCBin.nix {};

View File

@@ -0,0 +1,136 @@
{
symlinkJoin,
writeTextFile,
runCommand,
testers,
}:
let
inherit (testers) testEqualContents testBuildFailure;
foo = writeTextFile {
name = "foo";
text = "foo";
destination = "/etc/test.d/foo";
};
bar = writeTextFile {
name = "bar";
text = "bar";
destination = "/etc/test.d/bar";
};
baz = writeTextFile {
name = "baz";
text = "baz";
destination = "/var/lib/arbitrary/baz";
};
qux = writeTextFile {
name = "qux";
text = "qux";
};
emulatedSymlinkJoinFooBarStrip = runCommand "symlinkJoin-strip-foo-bar" { } ''
mkdir $out
ln -s ${foo}/etc/test.d/foo $out/
ln -s ${bar}/etc/test.d/bar $out/
'';
in
{
symlinkJoin = testEqualContents {
assertion = "symlinkJoin";
actual = symlinkJoin {
name = "symlinkJoin";
paths = [
foo
bar
baz
];
};
expected = runCommand "symlinkJoin-foo-bar-baz" { } ''
mkdir -p $out/{var/lib/arbitrary,etc/test.d}
ln -s {${foo},${bar}}/etc/test.d/* $out/etc/test.d
ln -s ${baz}/var/lib/arbitrary/baz $out/var/lib/arbitrary/
'';
};
symlinkJoin-strip-paths = testEqualContents {
assertion = "symlinkJoin-strip-paths";
actual = symlinkJoin {
name = "symlinkJoinPrefix";
paths = [
foo
bar
];
stripPrefix = "/etc/test.d";
};
expected = emulatedSymlinkJoinFooBarStrip;
};
symlinkJoin-strip-paths-skip-missing = testEqualContents {
assertion = "symlinkJoin-strip-paths-skip-missing";
actual = symlinkJoin {
name = "symlinkJoinPrefix";
paths = [
foo
bar
baz
];
stripPrefix = "/etc/test.d";
};
expected = emulatedSymlinkJoinFooBarStrip;
};
symlinkJoin-strip-paths-skip-not-directories = testEqualContents {
assertion = "symlinkJoin-strip-paths-skip-not-directories";
actual = symlinkJoin {
name = "symlinkJoinPrefix";
paths = [
foo
bar
qux
];
stripPrefix = "/etc/test.d";
};
expected = emulatedSymlinkJoinFooBarStrip;
};
symlinkJoin-fails-on-missing =
runCommand "symlinkJoin-fails-on-missing"
{
failed = testBuildFailure (symlinkJoin {
name = "symlinkJoin-fail";
paths = [
foo
bar
baz
];
stripPrefix = "/etc/test.d";
failOnMissing = true;
});
}
''
grep -e "-baz/etc/test.d: No such file or directory" $failed/testBuildFailure.log
touch $out
'';
symlinkJoin-fails-on-file =
runCommand "symlinkJoin-fails-on-file"
{
failed = testBuildFailure (symlinkJoin {
name = "symlinkJoin-fail";
paths = [
foo
bar
qux
];
stripPrefix = "/etc/test.d";
failOnMissing = true;
});
}
''
grep -e "-qux/etc/test.d: Not a directory" $failed/testBuildFailure.log
touch $out
'';
}