From 1dd511bd6920b428dbd48f36ac646da8463fcbdd Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 12 Nov 2026 02:33:43 +0000 Subject: populate: replace symlinks before syncing with rsync rsync follows a symlink at the target path, so a source switching from the flake or flake-input type to file, pass, passage, or pipe got written into the store path the link points to, and one switching from the symlink type into what that link points to, e.g. another source. Populate removes a symlink at the target path before syncing with rsync. The roots in .populate/flake-inputs/ of a source that is neither a flake nor a flake-input source anymore get removed as well. An edge case switches a flake source to a file source. --- pkgs/populate/default.nix | 18 ++++++++++++++++++ tests/scenarios.nix | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix index 708e7a3..66c88c1 100644 --- a/pkgs/populate/default.nix +++ b/pkgs/populate/default.nix @@ -158,6 +158,17 @@ let mkdir -p ${quote flakesDir} ${concatMapStrings flake sorted.result} ''} + # Remove roots of flake inputs of sources that are neither flake nor + # flake-input sources anymore. + ${concatMapStrings (name: /* sh */ '' + rm -fR ${quote "${target.path}/.populate/flake-inputs/${name}"} + '') (attrNames (filterAttrs + (_: source: !elem source.type [ "flake" "flake-input" ]) + sources))} + if test -d ${quote "${target.path}/.populate/flake-inputs"} && + test -z "$(ls -A ${quote "${target.path}/.populate/flake-inputs"})"; then + rmdir ${quote "${target.path}/.populate/flake-inputs"} + fi # Remove flakes of sources that no longer draw inputs from other sources. if test -d ${quote flakesDir}; then ls -A ${quote flakesDir} | while read -r name; do @@ -767,6 +778,13 @@ let ''; rsync' = target: config: sourcePath: /* sh */ '' + # rsync writes through a symlink at the target path, e.g. one a symlink or + # flake source left there, into what it points to. + ${runShell target /* sh */ '' + if test -L ${quote target.path}; then + rm ${quote target.path} + fi + ''} source_path=${sourcePath} if test -d "$source_path"; then source_path=$source_path/ diff --git a/tests/scenarios.nix b/tests/scenarios.nix index a738eca..38f8736 100644 --- a/tests/scenarios.nix +++ b/tests/scenarios.nix @@ -808,6 +808,25 @@ assert !(krops.isLocalTarget (targets.remote "/")); check = checkFlake { attr = "msg"; expected = "rel:lib"; }; }; }) + (s "flake-to-file" { + phases = [ + { + sources.x.flake = { flake = "/fixtures/top"; inherit fetchOnTarget; }; + check = roots 3; + mutate = '' + readlink "$T"/x > "$T"/old + ''; + } + { + sources.x.file = "/fixtures/alt"; + check = '' + test ! -L "$T"/x || fail "x is still a link to $(readlink "$T"/x)" + test -e "$T"/x/flake.nix || fail "flake.nix missing" + nix-store --verify-path "$(cat "$T"/old)" || fail "store path changed" + '' + roots 0; + } + ]; + }) ] ++ lib.optional (targetKind == "local") # The target has the path the input is locked to. (s "uselocal" { -- cgit v1.3.1