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
8 changes: 8 additions & 0 deletions doc/stdenv/stdenv.chapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,14 @@ 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).

By setting `allowDanglingSymlinks` or `allowReflexiveSymlinks` the hook can be configured to allow symlinks with non-existent targets or symlinks which are reflexive, respectively.

This hook can be disabled entirely by setting `dontCheckForBrokenSymlinks`.

### `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
93 changes: 93 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,93 @@
# 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 errorMessage
local -i numDanglingSymlinks=0
local -i numReflexiveSymlinks=0

# TODO(@connorbaker): This hook doesn't check for cycles in symlinks.
ConnorBaker marked this conversation as resolved.
Show resolved Hide resolved

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"
symlinkTarget="$pathParent/$symlinkTarget"

# Check to make sure the interpolated target doesn't escape the store path of `output`.
# If it does, Nix probably won't be able to resolve or track dependencies.
if [[ $symlinkTarget != "$output" && $symlinkTarget != "$output"/* ]]; then
nixErrorLog "symlink $path points to target $symlinkTarget, which escapes the current store path $output"
exit 1
fi
ConnorBaker marked this conversation as resolved.
Show resolved Hide resolved
fi

if [[ $path == "$symlinkTarget" ]]; then
# symlinkTarget is reflexive
errorMessage="the symlink $path is reflexive $symlinkTarget"
if [[ -z ${allowReflexiveSymlinks-} ]]; then
ConnorBaker marked this conversation as resolved.
Show resolved Hide resolved
nixErrorLog "$errorMessage"
numReflexiveSymlinks+=1
else
nixInfoLog "$errorMessage"
fi

elif [[ ! -e $symlinkTarget ]]; then
# symlinkTarget does not exist
errorMessage="the symlink $path points to a missing target $symlinkTarget"
if [[ -z ${allowDanglingSymlinks-} ]]; then
nixErrorLog "$errorMessage"
numDanglingSymlinks+=1
else
nixInfoLog "$errorMessage"
fi

else
# symlinkTarget exists and is irreflexive
nixInfoLog "the symlink $path is irreflexive and points to a target which exists"
ConnorBaker marked this conversation as resolved.
Show resolved Hide resolved
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
123 changes: 123 additions & 0 deletions pkgs/test/stdenv/no-broken-symlinks.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
lib,
pkgs,
stdenv,
}:

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

mkDanglingSymlink = ''
ln -sr "$out/dangling" "$out/dangling-symlink"
'';
ConnorBaker marked this conversation as resolved.
Show resolved Hide resolved

mkReflexiveSymlink = ''
ln -sr "$out/reflexive-symlink" "$out/reflexive-symlink"
'';
ConnorBaker marked this conversation as resolved.
Show resolved Hide resolved

mkValidSymlink = ''
touch "$out/valid"
ln -sr "$out/valid" "$out/valid-symlink"
'';
ConnorBaker marked this conversation as resolved.
Show resolved Hide resolved

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
{
# Dangling symlinks (allowDanglingSymlinks)
fail-dangling-symlink =
runCommand "fail-dangling-symlink"
{
failed = testBuildFailure (testBuilder {
name = "fail-dangling-symlink-inner";
commands = [ mkDanglingSymlink ];
});
}
''
(( 1 == "$(cat "$failed/testBuildFailure.exit")" ))
grep -F 'found 1 dangling symlinks and 0 reflexive symlinks' "$failed/testBuildFailure.log"
touch $out
'';

pass-dangling-symlink-allowed = testBuilder {
name = "pass-symlink-dangling-allowed";
commands = [ mkDanglingSymlink ];
derivationArgs.allowDanglingSymlinks = true;
};

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

pass-reflexive-symlink-allowed = testBuilder {
name = "pass-reflexive-symlink-allowed";
commands = [ mkReflexiveSymlink ];
derivationArgs.allowReflexiveSymlinks = true;
};

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

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

pass-valid-symlink = testBuilder {
name = "pass-valid-symlink";
commands = [ mkValidSymlink ];
};
}