Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions pkgs/build-support/trivial-builders/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
let
inherit (lib)
optionalAttrs
optionalString
hasPrefix
warn
map
isList
;
in

Expand Down Expand Up @@ -462,6 +466,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.

Expand All @@ -484,23 +511,37 @@ 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're adding in something more than just a simple for loop, I think this needs a test.

See the file pkgs/build-support/trivial-builders/test/default.nix for the set of tests.

Run the tests using nix-build -A tests.trivial-builders.symlinkJoin.

Take a look at pkgs/test/replace-vars/default.nix for an example of how to write those tests. testers.testEqualContents in particular will be useful, and testBuildFailure for demonstrating what happens when the derivation ought to error.

That'll give you the ability to do demonstrate the following cases:

  1. Direct, normal use sans any path stripping (maybe with a nested list, per the recursive call).
  2. Build failure due to a missing path.
  3. Path stripping with missing paths
  4. Path stripping with failures due to other reasons that are skipped.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path stripping with failures due to other reasons that are skipped.

Error skipping behavior was unintentional, it was added to make command not error, i.e test -d nonexisting && lndir ... would fail because of test -d exit code, and test -d ... && lndir ... || true would fix that error code.
Changed to if test -d ...; then instead. This way the code is even more ugly, but more correct.

It will skip those paths, whose prefix references some file (This test was added too), but should not ignore other I/O errors.

passAsFile = [ "paths" ];
}; # pass the defaults
in
runCommand name args
''
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}
'';
Expand Down
1 change: 1 addition & 0 deletions pkgs/build-support/trivial-builders/test/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand Down
136 changes: 136 additions & 0 deletions pkgs/build-support/trivial-builders/test/symlink-join.nix
Original file line number Diff line number Diff line change
@@ -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
'';
}