stdenv: add no-broken-symlinks hook (#370750)

This commit is contained in:
Philip Taron
2025-01-22 17:14:11 -08:00
committed by GitHub
5 changed files with 275 additions and 0 deletions
+10
View File
@@ -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.
@@ -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
# 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
View File
@@ -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
+1
View File
@@ -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 =
+191
View File
@@ -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) ];
};
}