files: respect executable for recursive directories

Apply explicit executable settings to recursive directory leaves so directory sources behave like documented file sources.
This commit is contained in:
Austin Horstman 2026-07-06 11:22:59 -05:00
parent f4d01c1d87
commit 144f4e36d0
2 changed files with 126 additions and 20 deletions

View file

@ -354,6 +354,63 @@ in
# contain the recursion root, not the visited files.
declare -A seenTargets
function setExecutableBit() {
local target="$1"
local executable="$2"
if [[ $executable == inherit ]]; then
# Don't change file mode if it should match the source.
:
elif [[ $executable ]]; then
chmod +x "$target"
else
chmod -x "$target"
fi
}
function insertFileEntry() {
local source="$1"
local target="$2"
local executable="$3"
local isExecutable
[[ -x $source ]] && isExecutable=1 || isExecutable=""
# Link the file into the home file directory if possible,
# i.e., if the executable bit of the source is the same we
# expect for the target. Otherwise, we copy the file and
# set the executable bit to the expected value.
if [[ $executable == inherit || $isExecutable == $executable ]]; then
ln -s "$source" "$target"
else
cp "$source" "$target"
setExecutableBit "$target" "$executable"
fi
}
function setLinkedFileExecutableBit() {
local target="$1"
local executable="$2"
local isExecutable
if [[ -d $target || ! -e $target ]]; then
return
fi
[[ -x $target ]] && isExecutable=1 || isExecutable=""
if [[ $executable == inherit || $isExecutable == $executable ]]; then
return
fi
local tmp
tmp="$(mktemp "$target.XXXXXX")"
cp "$target" "$tmp"
setExecutableBit "$tmp" "$executable"
mv -f "$tmp" "$target"
}
function insertFile() {
local source="$1"
local relTarget="$2"
@ -409,30 +466,19 @@ in
else
lndir -silent "$source" "$target"
fi
if [[ $executable != inherit ]]; then
local linkedFile
while IFS= read -r -d "" linkedFile; do
setLinkedFileExecutableBit "$linkedFile" "$executable"
done < <(find "$target" \( -type f -or -type l \) -print0)
fi
else
ln -s "$source" "$target"
fi
else
[[ -x $source ]] && isExecutable=1 || isExecutable=""
# Link the file into the home file directory if possible,
# i.e., if the executable bit of the source is the same we
# expect for the target. Otherwise, we copy the file and
# set the executable bit to the expected value.
if [[ $executable == inherit || $isExecutable == $executable ]]; then
ln -s "$source" "$target"
else
cp "$source" "$target"
if [[ $executable == inherit ]]; then
# Don't change file mode if it should match the source.
:
elif [[ $executable ]]; then
chmod +x "$target"
else
chmod -x "$target"
fi
fi
insertFileEntry "$source" "$target" "$executable"
fi
}
''

View file

@ -1,11 +1,71 @@
{ pkgs, ... }:
let
recursiveSource = pkgs.runCommand "recursive-executable-source" { } ''
mkdir -p $out/nested
echo source > $out/source
chmod 644 $out/source
echo nested > $out/nested/source
chmod 755 $out/nested/source
ln -s /nonexistent/nowhere $out/dangling
'';
in
{
home.file."executable" = {
text = "";
executable = true;
};
home.file."recursive-executable" = {
source = recursiveSource;
recursive = true;
executable = true;
};
home.file."recursive-non-executable" = {
source = recursiveSource;
recursive = true;
executable = false;
};
nmt.script = ''
assertFileExists home-files/executable
assertFileIsExecutable home-files/executable;
assertFileExists home-files/recursive-executable/source
assertFileIsExecutable home-files/recursive-executable/source
if [[ -L "$TESTED/home-files/recursive-executable/source" ]]; then
fail "Expected recursive-executable/source to be copied so its executable bit can be changed."
fi
assertFileExists home-files/recursive-executable/nested/source
assertFileIsExecutable home-files/recursive-executable/nested/source
assertLinkExists home-files/recursive-executable/nested/source
assertFileExists home-files/recursive-non-executable/source
assertFileIsNotExecutable home-files/recursive-non-executable/source
assertLinkExists home-files/recursive-non-executable/source
assertFileExists home-files/recursive-non-executable/nested/source
assertFileIsNotExecutable home-files/recursive-non-executable/nested/source
if [[ -L "$TESTED/home-files/recursive-non-executable/nested/source" ]]; then
fail "Expected recursive-non-executable/nested/source to be copied so its executable bit can be changed."
fi
# A dangling symlink among the recursively linked leaves has no
# executable bit to fix. It must be left alone rather than crashing the
# build when trying to cp/chmod through it.
assertLinkExists home-files/recursive-executable/dangling
if [[ -e "$TESTED/home-files/recursive-executable/dangling" ]]; then
fail "Expected recursive-executable/dangling to remain a dangling symlink."
fi
assertLinkExists home-files/recursive-non-executable/dangling
if [[ -e "$TESTED/home-files/recursive-non-executable/dangling" ]]; then
fail "Expected recursive-non-executable/dangling to remain a dangling symlink."
fi
'';
}