diff --git a/modules/files.nix b/modules/files.nix index 121b72e24..85ca54c39 100644 --- a/modules/files.nix +++ b/modules/files.nix @@ -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 } '' diff --git a/tests/modules/files/executable.nix b/tests/modules/files/executable.nix index 0e144ed48..6a02010f8 100644 --- a/tests/modules/files/executable.nix +++ b/tests/modules/files/executable.nix @@ -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 ''; }