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

stdenv: add no-broken-symlinks hook #370750

Merged
10 changes: 10 additions & 0 deletions doc/stdenv/stdenv.chapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,16 @@ This setup hook moves any systemd user units installed in the `lib/` subdirector

This hook only runs when compiling for Linux.

### `no-broken-symlinks.sh` {#no-broken-symlinks.sh}

This setup hook checks for, reports, and (by default) fails builds when "broken" symlinks are found. A symlink is considered "broken" if it's dangling (the target doesn't exist) or reflexive (it refers to itself).

This hook can be disabled by setting `dontCheckForBrokenSymlinks`.

::: {.note}
The check for reflexivity is direct and does not account for transitivity, so this hook will not prevent cycles in symlinks.
:::

### `set-source-date-epoch-to-latest.sh` {#set-source-date-epoch-to-latest.sh}

This sets `SOURCE_DATE_EPOCH` to the modification time of the most recent file.
Expand Down
72 changes: 72 additions & 0 deletions pkgs/build-support/setup-hooks/no-broken-symlinks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# shellcheck shell=bash

# Guard against double inclusion.
if (("${noBrokenSymlinksHookInstalled:-0}" > 0)); then
nixInfoLog "skipping because the hook has been propagated more than once"
return 0
fi
declare -ig noBrokenSymlinksHookInstalled=1
ConnorBaker marked this conversation as resolved.
Show resolved Hide resolved

# symlinks are often created in postFixup
# don't use fixupOutputHooks, it is before postFixup
postFixupHooks+=(noBrokenSymlinksInAllOutputs)

# A symlink is "dangling" if it points to a non-existent target.
# A symlink is "reflexive" if it points to itself.
# A symlink is considered "broken" if it is either dangling or reflexive.
noBrokenSymlinks() {
local -r output="${1:?}"
local path
local pathParent
local symlinkTarget
local -i numDanglingSymlinks=0
local -i numReflexiveSymlinks=0

# NOTE(@connorbaker): This hook doesn't check for cycles in symlinks.

if [[ ! -e $output ]]; then
nixWarnLog "skipping non-existent output $output"
return 0
fi
nixInfoLog "running on $output"

# NOTE: path is absolute because we're running `find` against an absolute path (`output`).
while IFS= read -r -d $'\0' path; do
pathParent="$(dirname "$path")"
symlinkTarget="$(readlink "$path")"

# Canonicalize symlinkTarget to an absolute path.
if [[ $symlinkTarget == /* ]]; then
nixInfoLog "symlink $path points to absolute target $symlinkTarget"
else
nixInfoLog "symlink $path points to relative target $symlinkTarget"
# Use --no-symlinks to avoid dereferencing again and --canonicalize-missing to avoid existence
# checks at this step (which can lead to infinite recursion).
symlinkTarget="$(realpath --no-symlinks --canonicalize-missing "$pathParent/$symlinkTarget")"
fi

if [[ $path == "$symlinkTarget" ]]; then
nixErrorLog "the symlink $path is reflexive $symlinkTarget"
numReflexiveSymlinks+=1
elif [[ ! -e $symlinkTarget ]]; then
nixErrorLog "the symlink $path points to a missing target $symlinkTarget"
numDanglingSymlinks+=1
else
nixDebugLog "the symlink $path is irreflexive and points to a target which exists"
fi
done < <(find "$output" -type l -print0)

if ((numDanglingSymlinks > 0 || numReflexiveSymlinks > 0)); then
nixErrorLog "found $numDanglingSymlinks dangling symlinks and $numReflexiveSymlinks reflexive symlinks"
exit 1
fi
return 0
}

noBrokenSymlinksInAllOutputs() {
if [[ -z ${dontCheckForBrokenSymlinks-} ]]; then
for output in $(getAllOutputNames); do
noBrokenSymlinks "${!output}"
done
fi
}
1 change: 1 addition & 0 deletions pkgs/stdenv/generic/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ let
../../build-support/setup-hooks/move-sbin.sh
../../build-support/setup-hooks/move-systemd-user-units.sh
../../build-support/setup-hooks/multiple-outputs.sh
../../build-support/setup-hooks/no-broken-symlinks.sh
../../build-support/setup-hooks/patch-shebangs.sh
../../build-support/setup-hooks/prune-libtool-files.sh
../../build-support/setup-hooks/reproducible-builds.sh
Expand Down
1 change: 1 addition & 0 deletions pkgs/test/stdenv/hooks.nix
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
[[ -e $out/bin/foo ]]
'';
};
no-broken-symlinks = import ./no-broken-symlinks.nix { inherit stdenv lib pkgs; };
# TODO: add multiple-outputs
patch-shebangs = import ./patch-shebangs.nix { inherit stdenv lib pkgs; };
prune-libtool-files =
Expand Down
191 changes: 191 additions & 0 deletions pkgs/test/stdenv/no-broken-symlinks.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
{
lib,
pkgs,
stdenv,
}:

let
inherit (lib.strings) concatStringsSep;
inherit (pkgs) runCommand;
inherit (pkgs.testers) testBuildFailure;

mkDanglingSymlink = absolute: ''
ln -s${if absolute then "r" else ""} "$out/dangling" "$out/dangling-symlink"
'';

mkReflexiveSymlink = absolute: ''
ln -s${if absolute then "r" else ""} "$out/reflexive-symlink" "$out/reflexive-symlink"
'';

mkValidSymlink = absolute: ''
touch "$out/valid"
ln -s${if absolute then "r" else ""} "$out/valid" "$out/valid-symlink"
'';

testBuilder =
{
name,
commands ? [ ],
derivationArgs ? { },
}:
stdenv.mkDerivation (
{
inherit name;
strictDeps = true;
dontUnpack = true;
dontPatch = true;
dontConfigure = true;
dontBuild = true;
installPhase =
''
mkdir -p "$out"

''
+ concatStringsSep "\n" commands;
}
// derivationArgs
);
in
{
fail-dangling-symlink-relative =
runCommand "fail-dangling-symlink-relative"
{
failed = testBuildFailure (testBuilder {
name = "fail-dangling-symlink-relative-inner";
commands = [ (mkDanglingSymlink false) ];
});
}
''
(( 1 == "$(cat "$failed/testBuildFailure.exit")" ))
grep -F 'found 1 dangling symlinks and 0 reflexive symlinks' "$failed/testBuildFailure.log"
touch $out
'';

pass-dangling-symlink-relative-allowed = testBuilder {
name = "pass-dangling-symlink-relative-allowed";
commands = [ (mkDanglingSymlink false) ];
derivationArgs.dontCheckForBrokenSymlinks = true;
};

fail-dangling-symlink-absolute =
runCommand "fail-dangling-symlink-absolute"
{
failed = testBuildFailure (testBuilder {
name = "fail-dangling-symlink-absolute-inner";
commands = [ (mkDanglingSymlink true) ];
});
}
''
(( 1 == "$(cat "$failed/testBuildFailure.exit")" ))
grep -F 'found 1 dangling symlinks and 0 reflexive symlinks' "$failed/testBuildFailure.log"
touch $out
'';

pass-dangling-symlink-absolute-allowed = testBuilder {
name = "pass-dangling-symlink-absolute-allowed";
commands = [ (mkDanglingSymlink true) ];
derivationArgs.dontCheckForBrokenSymlinks = true;
};

fail-reflexive-symlink-relative =
runCommand "fail-reflexive-symlink-relative"
{
failed = testBuildFailure (testBuilder {
name = "fail-reflexive-symlink-relative-inner";
commands = [ (mkReflexiveSymlink false) ];
});
}
''
(( 1 == "$(cat "$failed/testBuildFailure.exit")" ))
grep -F 'found 0 dangling symlinks and 1 reflexive symlinks' "$failed/testBuildFailure.log"
touch $out
'';

pass-reflexive-symlink-relative-allowed = testBuilder {
name = "pass-reflexive-symlink-relative-allowed";
commands = [ (mkReflexiveSymlink false) ];
derivationArgs.dontCheckForBrokenSymlinks = true;
};

fail-reflexive-symlink-absolute =
runCommand "fail-reflexive-symlink-absolute"
{
failed = testBuildFailure (testBuilder {
name = "fail-reflexive-symlink-absolute-inner";
commands = [ (mkReflexiveSymlink true) ];
});
}
''
(( 1 == "$(cat "$failed/testBuildFailure.exit")" ))
grep -F 'found 0 dangling symlinks and 1 reflexive symlinks' "$failed/testBuildFailure.log"
touch $out
'';

pass-reflexive-symlink-absolute-allowed = testBuilder {
name = "pass-reflexive-symlink-absolute-allowed";
commands = [ (mkReflexiveSymlink true) ];
derivationArgs.dontCheckForBrokenSymlinks = true;
};

fail-broken-symlinks-relative =
runCommand "fail-broken-symlinks-relative"
{
failed = testBuildFailure (testBuilder {
name = "fail-broken-symlinks-relative-inner";
commands = [
(mkDanglingSymlink false)
(mkReflexiveSymlink false)
];
});
}
''
(( 1 == "$(cat "$failed/testBuildFailure.exit")" ))
grep -F 'found 1 dangling symlinks and 1 reflexive symlinks' "$failed/testBuildFailure.log"
touch $out
'';

pass-broken-symlinks-relative-allowed = testBuilder {
name = "pass-broken-symlinks-relative-allowed";
commands = [
(mkDanglingSymlink false)
(mkReflexiveSymlink false)
];
derivationArgs.dontCheckForBrokenSymlinks = true;
};

fail-broken-symlinks-absolute =
runCommand "fail-broken-symlinks-absolute"
{
failed = testBuildFailure (testBuilder {
name = "fail-broken-symlinks-absolute-inner";
commands = [
(mkDanglingSymlink true)
(mkReflexiveSymlink true)
];
});
}
''
(( 1 == "$(cat "$failed/testBuildFailure.exit")" ))
grep -F 'found 1 dangling symlinks and 1 reflexive symlinks' "$failed/testBuildFailure.log"
touch $out
'';

pass-broken-symlinks-absolute-allowed = testBuilder {
name = "pass-broken-symlinks-absolute-allowed";
commands = [
(mkDanglingSymlink true)
(mkReflexiveSymlink true)
];
derivationArgs.dontCheckForBrokenSymlinks = true;
};

pass-valid-symlink-relative = testBuilder {
name = "pass-valid-symlink-relative";
commands = [ (mkValidSymlink false) ];
};

pass-valid-symlink-absolute = testBuilder {
name = "pass-valid-symlink-absolute";
commands = [ (mkValidSymlink true) ];
};
}