diff options
| -rw-r--r-- | README.md | 107 | ||||
| -rw-r--r-- | lib/default.nix | 17 | ||||
| -rw-r--r-- | lib/types/populate.nix | 34 | ||||
| -rw-r--r-- | pkgs/krops/default.nix | 2 | ||||
| -rw-r--r-- | pkgs/populate/default.nix | 303 | ||||
| -rwxr-xr-x | tests/fixtures.sh | 23 | ||||
| -rw-r--r-- | tests/scenarios.nix | 384 |
7 files changed, 714 insertions, 156 deletions
@@ -172,13 +172,13 @@ build` step. Name of the source containing the flake to deploy, optionally followed by `#` and the name of the NixOS configuration, e.g. `"myconf#myhost"`. If the source -draws inputs from other sources, its generated flake gets deployed, see -[Flake inputs from sources](#flake-inputs-from-sources). This makes -`nixos-rebuild` use `--flake`, and requires Nix with flake support (Nix 2.4 or -later) on the build target. Without the name of the NixOS configuration, -`nixos-rebuild` uses the host name of the build target. Without `flake`, -`nixos-rebuild` gets `--no-flake` if `/etc/nixos/flake.nix` exists on the -build target, so it deploys `NIX_PATH` instead of that flake. +draws inputs from other sources through a generated flake, that flake gets +deployed, see [Flake inputs from sources](#flake-inputs-from-sources). This +makes `nixos-rebuild` use `--flake`, and requires Nix with flake support (Nix +2.4 or later) on the build target. Without the name of the NixOS +configuration, `nixos-rebuild` uses the host name of the build target. +Without `flake`, `nixos-rebuild` gets `--no-flake` if `/etc/nixos/flake.nix` +exists on the build target, so it deploys `NIX_PATH` instead of that flake. ### `force` (optional, defaults to false) @@ -347,6 +347,12 @@ Supported attributes: inputs of the flake in this source to draw from other sources, see [Flake inputs from sources](#flake-inputs-from-sources). +* `rewriteLock` (optional) - + boolean that controls whether inputs get drawn from other sources by + rewriting the lock file of the flake in this source, or else through a + generated flake, see [Flake inputs from sources](#flake-inputs-from-sources). + Defaults to `true`. + ### `flake` @@ -389,6 +395,27 @@ Supported attributes: inputs of the flake in this source to draw from other sources, see [Flake inputs from sources](#flake-inputs-from-sources). +* `materialize` (optional) - + boolean that controls whether the target path becomes a writable copy + instead of a link into the store. The copy gets made again only if what + it derives from changes: the delivered store path, the patches, or which + inputs get drawn from which sources. Until then, changes made to it on the + target stay. Nix copies it into the store whenever evaluating a flake + changed it, which is noticeable for big flakes like nixpkgs. For a flake + in a subdirectory, only that subdirectory gets copied. Defaults to + `false`. + +* `patches` (optional) - + list of paths of patches on the deploying machine, which get applied in + order with `patch -p1` on the target, in the directory the target path + shows. Populating fails if a patch doesn't apply. Defaults to `[]`. + +* `rewriteLock` (optional) - + boolean that controls whether inputs get drawn from other sources by + rewriting the lock file of the flake in this source, or else through a + generated flake, see [Flake inputs from sources](#flake-inputs-from-sources). + Defaults to `true`. + Instead of an attribute set, the flake reference can be given as a string. The flake commands on the deploying machine run using krops' own Nix, so @@ -439,6 +466,27 @@ Supported attributes: inputs of the flake in this source to draw from other sources, see [Flake inputs from sources](#flake-inputs-from-sources). +* `materialize` (optional) - + boolean that controls whether the target path becomes a writable copy + instead of a link into the store. The copy gets made again only if what + it derives from changes: the delivered store path, the patches, or which + inputs get drawn from which sources. Until then, changes made to it on the + target stay. Nix copies it into the store whenever evaluating a flake + changed it, which is noticeable for big flakes like nixpkgs. For a flake + in a subdirectory, only that subdirectory gets copied. Defaults to + `false`. + +* `patches` (optional) - + list of paths of patches on the deploying machine, which get applied in + order with `patch -p1` on the target, in the directory the target path + shows. Populating fails if a patch doesn't apply. Defaults to `[]`. + +* `rewriteLock` (optional) - + boolean that controls whether inputs get drawn from other sources by + rewriting the lock file of the flake in this source, or else through a + generated flake, see [Flake inputs from sources](#flake-inputs-from-sources). + Defaults to `true`. + Without `fetchOnTarget`, the same requirements as for the `flake` source type apply. @@ -477,6 +525,12 @@ Supported attributes: inputs of the flake in this source to draw from other sources, see [Flake inputs from sources](#flake-inputs-from-sources). +* `rewriteLock` (optional) - + boolean that controls whether inputs get drawn from other sources by + rewriting the lock file of the flake in this source, or else through a + generated flake, see [Flake inputs from sources](#flake-inputs-from-sources). + Defaults to `true`. + ### `pass` @@ -568,20 +622,32 @@ like `NIX_PATH` does for Nix expressions. } ``` -For each source that draws inputs from other sources, populate generates a -flake in `.populate/flakes/<name>`, which re-exports the outputs of the flake -in the source, with those inputs drawn from the other sources. The source -itself stays as it is, so `NIX_PATH` still refers to it. When a generated -flake draws an input from a source that has a generated flake itself, it uses -that one, so in the example above, myconf's crate2nix uses nixpkgs from the -`nixpkgs` source as well. `writeDeploy`'s `flake` parameter uses a source's -generated flake automatically. +Populate rewrites the lock file of the flake in each source that draws +inputs from other sources, using `nix flake lock --override-input`, so the +flake in the source itself uses the other sources. `file` and `git` sources +get their lock file rewritten in place. `flake` and `flake-input` sources +get delivered as a copy with the rewritten lock file, which gets added to the +store, unless materialized, so the target path links to that instead of the +delivered store path, as it does for patched ones. A source drawing an input +from a source that draws inputs itself uses that source as rewritten, so in +the example above, myconf's crate2nix uses nixpkgs from the `nixpkgs` source as +well. + +With `rewriteLock = false`, and for `symlink` sources, which point at other +sources, the source stays as it is, and populate generates a flake in +`.populate/flakes/<name>` instead, which re-exports the outputs of the flake +in the source, with those inputs drawn from the other sources. A source +drawing an input from a source with a generated flake uses that flake. +`writeDeploy`'s `flake` parameter uses a source's generated flake +automatically. Sources in a Git work tree are referred to as `git+file:` (with submodules if -there are any), all other sources as `path:`, with symlinks resolved. -Generated flakes get locked on every run, so their lock files match the -sources, and get removed once their sources no longer draw inputs from other -sources. This requires Nix with flake support (Nix 2.4 or later) on the +there are any), all other sources as `path:`, with symlinks resolved. Lock +files get rewritten or generated on every run, so they match the sources. +Copies of `flake` and `flake-input` sources get made again only if the +delivered store path, the patches, or the sources they draw inputs from +change. Generated flakes and copies get removed once their sources no longer +need them. This requires Nix with flake support (Nix 2.4 or later) on the target. Populating fails if a source that draws inputs from other sources contains no `flake.nix`. Sources that inputs get drawn from don't need to be flakes, as long as the inputs drawing from them are declared with @@ -594,7 +660,8 @@ flakes, as long as the inputs drawing from them are declared with for features spanning sources, such as flake inputs from sources, one for edge cases of flakes, such as unusual lock files, and one for the arguments `writeDeploy` passes to `nixos-rebuild`. The tests deploy every combination -of a source type's options to the deploying machine itself, to a remote target +of a source type's options, except for patches, which the features test +covers, to the deploying machine itself, to a remote target as `root`, and to a remote target using `sudo`, and check the result after deploying and after deploying again. The tests require KVM and nixpkgs with `pkgs.testers.runNixOSTest` diff --git a/lib/default.nix b/lib/default.nix index f720a08..0a99f97 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -42,6 +42,23 @@ let { in flatten [] (source.${source.type}.inputs or {}); + # Tells whether a source's inputs get drawn from other sources by a + # generated flake in .populate/flakes, instead of by rewriting the lock + # file of the flake in the source. Symlink sources point at other + # sources, whose lock files stay as they are. + hasGeneratedFlake = source: + lib.flakeFollows source != [] && + (source.type == "symlink" || !(source.${source.type}.rewriteLock or true)); + + # Tells whether a flake or flake-input source gets delivered as a copy, + # which gets patched, gets its lock file rewritten, or is materialized. + derivesFlake = source: let + source' = source.${source.type}; + in + lib.elem source.type [ "flake" "flake-input" ] && + (source'.materialize or false || source'.patches or [] != [] || + (source'.rewriteLock or true && lib.flakeFollows source != [])); + maybeHostName = default: let # We're parsing /etc/hostname here because reading # /proc/sys/kernel/hostname yields "" diff --git a/lib/types/populate.nix b/lib/types/populate.nix index 4523dad..a278ffd 100644 --- a/lib/types/populate.nix +++ b/lib/types/populate.nix @@ -125,6 +125,10 @@ path = lib.mkOption { type = lib.types.absolute-pathname; }; + rewriteLock = lib.mkOption { + default = true; + type = lib.types.bool; + }; useChecksum = lib.mkOption { default = false; type = lib.types.bool; @@ -185,6 +189,19 @@ default = {}; type = flake-inputs; }; + materialize = lib.mkOption { + default = false; + type = lib.types.bool; + }; + patches = lib.mkOption { + apply = map toString; + default = []; + type = lib.types.listOf lib.types.path; + }; + rewriteLock = lib.mkOption { + default = true; + type = lib.types.bool; + }; }; }; flake-input = lib.types.submodule { @@ -200,6 +217,19 @@ default = {}; type = flake-inputs; }; + materialize = lib.mkOption { + default = false; + type = lib.types.bool; + }; + patches = lib.mkOption { + apply = map toString; + default = []; + type = lib.types.listOf lib.types.path; + }; + rewriteLock = lib.mkOption { + default = true; + type = lib.types.bool; + }; name = lib.mkOption { type = lib.types.str; }; @@ -224,6 +254,10 @@ ref = lib.mkOption { type = lib.types.str; # TODO lib.types.git.ref }; + rewriteLock = lib.mkOption { + default = true; + type = lib.types.bool; + }; url = lib.mkOption { type = lib.types.str; # TODO lib.types.git.url }; diff --git a/pkgs/krops/default.nix b/pkgs/krops/default.nix index 46ca66f..3aa68cf 100644 --- a/pkgs/krops/default.nix +++ b/pkgs/krops/default.nix @@ -124,7 +124,7 @@ in if flake == null then null else if !(source ? ${name}) then throw "krops: flake ${name} is not a source" - else if lib.flakeFollows source.${name} != [] then + else if lib.hasGeneratedFlake source.${name} then ".populate/flakes/${flake}" else flake; diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix index 905e6e4..a7e351a 100644 --- a/pkgs/populate/default.nix +++ b/pkgs/populate/default.nix @@ -49,14 +49,30 @@ let fi ''; - # Generates a flake for each source that draws inputs from other sources, in - # .populate/flakes/<name>. Each flake re-exports the outputs of the flake in - # its source, with those inputs drawn from the other sources' flakes, and - # gets locked on every run, so its lock file matches the sources. + # Draws inputs of flakes in sources from other sources, and derives what the + # target paths of flake and flake-input sources that get patched, get their + # lock files rewritten, or are materialized get. Sources get processed in + # the order they draw inputs from each other. + # + # Inputs get drawn by rewriting the lock file of the flake in the source + # with nix flake lock --override-input: in place for file and git sources, + # and in a copy for flake and flake-input sources. Sources with rewriteLock + # set to false, and symlink sources, get a generated flake in + # .populate/flakes/<name> instead, which re-exports the outputs of the flake + # in the source, with those inputs drawn from the other sources. Lock files + # get rewritten or generated on every run, so they match the sources. flakes = target: sources: let - flakesDir = "${target.path}/.populate/flakes"; - generated = filterAttrs (_: source: flakeFollows source != []) sources; - names = attrNames generated; + sentinel = "${target.path}/.populate"; + flakesDir = "${sentinel}/flakes"; + derivedDir = "${sentinel}/derived"; + patchesDir = "${sentinel}/patches"; + drawing = filterAttrs (_: source: flakeFollows source != []) sources; + generated = filterAttrs (_: hasGeneratedFlake) sources; + derived = filterAttrs (_: derivesFlake) sources; + patched = filterAttrs + (_: source: source.${source.type}.patches or [] != []) + sources; + names = attrNames (drawing // derived); # Source names become input names in generated flakes, where Nix accepts # fewer names than populate. Others get replaced by one derived from # them. @@ -64,14 +80,15 @@ let if builtins.match "[a-zA-Z][a-zA-Z0-9_-]*" name != null && name != "self" then name else "source-${substring 0 16 (builtins.hashString "sha256" name)}"; - drawnFrom = name: map (x: x.source) (flakeFollows generated.${name}); + drawnFrom = name: map (x: x.source) (flakeFollows sources.${name}); sorted = toposort (a: b: elem a (drawnFrom b)) names; - # Generated flakes refer to each other's generated flakes, so they don't - # need to repeat overrides. + # Shell word for the URL of the flake that inputs drawn from a source use: + # its generated flake, if it has one, so overrides don't need to be + # repeated, or else the flake in the source. url = name: if generated ? ${name} - then "path:${flakesDir}/${name}" - else "$(flake_url ${quote name})"; + then quote "path:${flakesDir}/${name}" + else ''"$(flake_url ${quote name})"''; check = name: source: if source == name then throw "krops: source ${name} draws inputs from itself" @@ -79,6 +96,147 @@ let throw "krops: source ${name} draws inputs from missing source ${source}" else source; + # Sets the positional parameters to nix flake lock's arguments for + # drawing the inputs of the flake in source name from other sources. + overrideArgs = name: /* sh */ '' + set -- + ${concatMapStrings (x: /* sh */ '' + set -- "$@" --override-input ${quote (concatStringsSep "/" x.path)} ${url (check name x.source)} + '') (flakeFollows sources.${name})} + ''; + # Rewrites the lock file of the flake in a file or git source in place. + rewrite = name: /* sh */ '' + ( + ${overrideArgs name} + nix flake lock "$@" "$(flake_url ${quote name})" >&2 + ) + ''; + # Derives what the target path of a flake or flake-input source gets from + # what got delivered to .populate/derived/<name>/original: a copy, which + # gets patched and gets its lock file rewritten. Unless materialized, + # the copy gets added to the store, and the target path becomes a link to + # it. Copies get made again only if what they derive from changes: + # materialized ones if the delivered store path, the patches, or what + # inputs get drawn from which sources changes, and others also if the + # sources they draw inputs from change. + derive = name: let + source = sources.${name}; + source' = source.${source.type}; + rewriteLock = source'.rewriteLock && drawing ? ${name}; + targetPath = "${target.path}/${name}"; + dir = "${derivedDir}/${name}"; + spec = toJSON { + inherit (source') materialize; + inherit rewriteLock; + follows = flakeFollows source; + }; + # Globbing is off, so patches get listed with ls, in order. + applyPatches = optionalString (source'.patches != []) /* sh */ '' + ls ${quote "${patchesDir}/${name}"} | while read -r patch; do + if ! patch -p1 -s --batch --no-backup-if-mismatch \ + -i ${quote "${patchesDir}/${name}"}/"$patch" >&2; then + >&2 printf 'error: patch %s does not apply to source %s\n' \ + "$(printf %s "$patch" | cut -c6-)" ${quote name} + exit 1 + fi + done + ''; + # Removes the work directory, which an interrupted run may have left + # behind. + removeWork = /* sh */ '' + if test -e ${quote "${dir}/work"}; then + chmod -R u+w ${quote "${dir}/work"} + rm -fR ${quote "${dir}/work"} + fi + ''; + in /* sh */ '' + ( + original=$(readlink -f ${quote "${dir}/original"}) + path=$(printf '%s\n' "$original" | cut -d/ -f1-4) + subdir=''${original#"$path"} + subdir=''${subdir#/} + key=$( + { + printf '%s\n' "$original" ${quote spec} + ${optionalString (source'.patches != []) /* sh */ '' + ls ${quote "${patchesDir}/${name}"} | while read -r patch; do + sha256sum ${quote "${patchesDir}/${name}"}/"$patch" + done + ''} + ${optionalString (rewriteLock && !source'.materialize) /* sh */ '' + ${overrideArgs name} + printf '%s\n' "$@" + # Sources outside the store can change without changing their + # path. + for arg; do + case $arg in + path:/nix/store/*) ;; + path:*|git+file:*) + arg=''${arg#*:} + nix-hash --type sha256 "''${arg%%\?*}" + ;; + esac + done + ''} + } | sha256sum | cut -c-64 + ) + ${removeWork} + ${if source'.materialize then /* sh */ '' + if ! test -e ${quote "${dir}/key"} || + test "$(cat ${quote "${dir}/key"})" != "$key" || + test -L ${quote targetPath} || + ! test -d ${quote targetPath}; then + cp -R "$original" ${quote "${dir}/work"} + chmod -R u+w ${quote "${dir}/work"} + ( + cd ${quote "${dir}/work"} + ${applyPatches} + ) + rm -fR ${quote targetPath} + mv ${quote "${dir}/work"} ${quote targetPath} + printf '%s\n' "$key" > ${quote "${dir}/key"} + fi + ${optionalString rewriteLock /* sh */ '' + ${overrideArgs name} + nix flake lock "$@" "$(flake_url ${quote name})" >&2 + ''} + '' else /* sh */ '' + if test -e ${quote "${dir}/key"} && + test "$(cat ${quote "${dir}/key"})" = "$key" && + result=$(readlink ${quote "${dir}/result"}) && + nix-store --check-validity "$result" 2>/dev/null; then + : + else + # The copy keeps the name of the delivered store path. + work=${quote "${dir}/work"}/$(basename "$path" | cut -c34-) + mkdir ${quote "${dir}/work"} + cp -R "$path" "$work" + chmod -R u+w "$work" + ( + cd "$work"/"$subdir" + ${applyPatches} + ) + ${optionalString rewriteLock /* sh */ '' + ${overrideArgs name} + # Not just the path, which Nix takes for the enclosing Git + # work tree, if any. + nix flake lock "$@" "path:$work''${subdir:+?dir=$subdir}" >&2 + ''} + result=$(nix-store --add-fixed --recursive sha256 "$work") + ${removeWork} + nix-store --realise "$result" \ + --add-root ${quote "${dir}/result"} --indirect >/dev/null + printf '%s\n' "$key" > ${quote "${dir}/key"} + fi + if test -d ${quote targetPath} && ! test -L ${quote targetPath}; then + rm -fR ${quote targetPath} + fi + # .populate/derived/<name>/result roots the store path. + ln -fns "$result''${subdir:+/$subdir}" ${quote targetPath} + ''} + ) + ''; + # Generates the flake of a source in .populate/flakes/<name>. flake = name: let follows = flakeFollows generated.${name}; drawn = unique (map (x: check name x.source) follows); @@ -131,13 +289,36 @@ let nix flake lock ${quote "path:${flakesDir}/${name}"} >&2 ) ''; + process = name: + optionalString (derived ? ${name}) (derive name) + + (if generated ? ${name} then flake name + else if drawing ? ${name} && !(derived ? ${name}) then rewrite name + else ""); + # Removes the entries of directory dir that aren't in names. + removeOthers = dir: names: /* sh */ '' + if test -d ${quote dir}; then + ls -A ${quote dir} | while read -r name; do + case ${quote " ${toString names} "} in + *" $name "*) ;; + *) + chmod -R u+w ${quote dir}/"$name" + rm -fR ${quote dir}/"$name" + ;; + esac + done + if test -z "$(ls -A ${quote dir})"; then + rmdir ${quote dir} + fi + fi + ''; in if sorted ? cycle then throw "krops: sources draw inputs from each other: ${toString sorted.cycle}" else runShell target /* sh */ '' set -efu ${optionalString (names != []) /* sh */ '' - ${requireFlakes "drawing flake inputs from sources"} + ${optionalString (drawing != {}) + (requireFlakes "drawing flake inputs from sources")} # Prints the flake URL of the source named $1. flake_url() { @@ -158,42 +339,43 @@ let } # Inputs can only be drawn into flakes, so check all sources before - # generating anything. + # changing anything. missing= - for name in ${concatMapStringsSep " " quote names}; do - if ! test -e ${quote target.path}/"$name"/flake.nix; then - missing="$missing $name" + ${concatMapStrings (name: /* sh */ '' + if ! test -e ${quote ( + if derived ? ${name} + then "${derivedDir}/${name}/original" + else "${target.path}/${name}" + )}/flake.nix; then + missing="$missing "${quote name} fi - done + '') (attrNames drawing)} if test -n "$missing"; then >&2 printf 'error: source %s draws flake inputs from other sources, but contains no flake.nix\n' $missing exit 1 fi - ${sentinelDirectory "${target.path}/.populate"} - mkdir -p ${quote flakesDir} - ${concatMapStrings flake sorted.result} + ${optionalString (generated != {}) /* sh */ '' + ${sentinelDirectory sentinel} + mkdir -p ${quote flakesDir} + ''} + ${concatMapStrings process 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}"} + rm -fR ${quote "${sentinel}/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 - case ${quote " ${toString names} "} in - *" $name "*) ;; - *) rm -fR ${quote flakesDir}/"$name" ;; - esac - done + if test -d ${quote "${sentinel}/flake-inputs"} && + test -z "$(ls -A ${quote "${sentinel}/flake-inputs"})"; then + rmdir ${quote "${sentinel}/flake-inputs"} fi + # Remove what sources left behind that don't need it anymore. + ${removeOthers flakesDir (attrNames generated)} + ${removeOthers derivedDir (attrNames derived)} + ${removeOthers patchesDir (attrNames patched)} ''; # Removes entries of the target directory that aren't sources, garbage @@ -263,6 +445,31 @@ let nix' = "${nix}/bin/nix --extra-experimental-features ${quote "nix-command flakes"}"; inputPath = if source.name == null then [] else splitString "/" source.name; fetchOnTarget = source.fetchOnTarget or false; + name = baseNameOf target.path; + sentinel = "${dirOf target.path}/.populate"; + # A source that gets patched, gets its lock file rewritten, or is + # materialized gets linked in .populate/derived/<name> instead of at the + # target path, and the flakes step derives what the target path gets. + derived = derivesFlake { type = "flake"; flake = source; }; + linkPath = + if derived then "${sentinel}/derived/${name}/original" else target.path; + patches = source.patches or []; + # Uploads the patches to .populate/patches/<name>, numbered in order. + uploadPatches = optionalString (patches != []) /* sh */ '' + rm -fR "$PWD"/patches + mkdir "$PWD"/patches + ${concatImapStrings (i: patch: /* sh */ '' + cp ${quote patch} "$PWD"/patches/${quote "${fixedWidthNumber 4 i}-${baseNameOf patch}"} + '') patches} + ${runShell target /* sh */ '' + set -efu + ${sentinelDirectory sentinel} + mkdir -p ${quote "${sentinel}/patches"} + ''} + ${rsync' (target // { path = "${sentinel}/patches/${name}"; }) + (rsyncDefaultConfig // { filterFiles = false; useChecksum = true; }) + /* sh */ ''"$PWD"/patches''} + ''; # Tells whether a flake reference names a scheme that doesn't refer to the # deploying machine, so the target can fetch the flake on its own. # Indirect references get resolved on the deploying machine. @@ -320,12 +527,11 @@ let fi } ''; - sentinel = "${dirOf target.path}/.populate"; # Directory of garbage collector roots for the input's own inputs. - inputsRoots = "${sentinel}/flake-inputs/${baseNameOf target.path}"; + inputsRoots = "${sentinel}/flake-inputs/${name}"; # Directory receiving the flake itself if the target can't fetch it. uploads = "${sentinel}/flake-uploads"; - upload = "${uploads}/${baseNameOf target.path}"; + upload = "${uploads}/${name}"; removeUpload = /* sh */ '' if test -e ${quote upload}; then chmod -R u+w ${quote upload} @@ -339,26 +545,30 @@ let ''; # Reads the subdirectory containing the flake or input, which may be # empty, followed by store paths from stdin. Links the first store path, - # or the subdirectory therein, at the target path, and registers it and + # or the subdirectory therein, at the link path, and registers it and # the remaining ones as garbage collector roots. Roots get added before # obsolete ones get removed, so the inputs stay rooted. link = /* sh */ '' set -efu read -r dir read -r path + ${optionalString derived /* sh */ '' + ${sentinelDirectory sentinel} + mkdir -p ${quote (dirOf linkPath)} + ''} # nix-store --add-root only replaces links into the store. - if test -L ${quote target.path}; then - case $(readlink ${quote target.path}) in + if test -L ${quote linkPath}; then + case $(readlink ${quote linkPath}) in /nix/store/*) ;; - *) rm ${quote target.path} ;; + *) rm ${quote linkPath} ;; esac - elif test -e ${quote target.path}; then - rm -fR ${quote target.path} + elif test -e ${quote linkPath}; then + rm -fR ${quote linkPath} fi - nix-store --realise "$path" --add-root ${quote target.path} --indirect >/dev/null + nix-store --realise "$path" --add-root ${quote linkPath} --indirect >/dev/null # The garbage collector follows the link into the store path. if test -n "$dir"; then - ln -fns "$path/$dir" ${quote target.path} + ln -fns "$path/$dir" ${quote linkPath} fi ${sentinelDirectory sentinel} @@ -495,9 +705,10 @@ let if !fetchOnTarget && !isLocalTarget target && target.sudo then throw "krops: source ${target.path}: copying flakes to targets with sudo is not supported" else if fetchOnTarget && source.name == null && isRemoteRef source.url then - runShell target archiveAndLink + uploadPatches + runShell target archiveAndLink else /* sh */ '' set -efu + ${uploadPatches} ${cached "metadata" /* sh */ '' ${nix'} flake metadata --json --no-update-lock-file ${quote source.url} diff --git a/tests/fixtures.sh b/tests/fixtures.sh index b57dcc5..3839c1d 100755 --- a/tests/fixtures.sh +++ b/tests/fixtures.sh @@ -336,3 +336,26 @@ cat > /fixtures/pipe <<'SH' echo piped SH chmod +x /fixtures/pipe + +# patches: hello and hello2 change dep's greeting, hello2 only after hello; +# sub changes the flake in mono's subdirectory; bad applies to nothing. +mkdir -p /fixtures/patches +# Prints a patch of file $1 in the directory $2, replacing $3 by $4. +patch_of() { + sed "s/$3/$4/" "$2/$1" > /fixtures/patches/new + diff -u --label a/"$1" --label b/"$1" "$2/$1" /fixtures/patches/new || test $? = 1 + rm /fixtures/patches/new +} +patch_of flake.nix /fixtures/www/src/dep 'dep+' 'hello+' > /fixtures/patches/hello.patch +mkdir /fixtures/patches/dep-hello +sed 's/dep+/hello+/' /fixtures/www/src/dep/flake.nix > /fixtures/patches/dep-hello/flake.nix +patch_of flake.nix /fixtures/patches/dep-hello 'hello+' 'hello2+' > /fixtures/patches/hello2.patch +rm -R /fixtures/patches/dep-hello +patch_of flake.nix /fixtures/www/src/mono/sub '"sub"' '"patched"' > /fixtures/patches/sub.patch +cat > /fixtures/patches/bad.patch <<'PATCH' +--- a/flake.nix ++++ b/flake.nix +@@ -1 +1 @@ +-nothing like this ++anything +PATCH diff --git a/tests/scenarios.nix b/tests/scenarios.nix index f8e103e..2815c3f 100644 --- a/tests/scenarios.nix +++ b/tests/scenarios.nix @@ -64,6 +64,21 @@ let alt = { alt.file = "/fixtures/alt"; }; + # How a source draws inputs from other sources: not at all, by rewriting + # the lock file of its flake, or by a generated flake. + inputModes = [ "none" "rewrite" "generated" ]; + + # The options of a source drawing inputs in mode. + drawInputs = mode: inputs: optionalAttrs (mode != "none") ({ + inherit inputs; + } // optionalAttrs (mode == "generated") { + rewriteLock = false; + }); + + # The flake of source x that has its inputs drawn in mode. + flakeOf = mode: + if mode == "generated" then ''"$T"/.populate/flakes/x'' else ''"$T"/x''; + # Builds a scenario from a name, a target kind, and phases with sources. # Isolated scenarios start with no target directories and a collected store # on both machines, so no other scenario's store paths or roots stand in for @@ -150,16 +165,22 @@ let targetKinds; # Checks that the source at $T/x is a link to a store path, or to the - # subdirectory sub therein, and evaluates the attribute attr of the flake - # there, through the link, or of its generated flake, after collecting - # garbage. - checkFlake = { attr, expected, subdir ? false, generated ? false }: '' + # subdirectory sub therein, or a writable directory if materialized, and + # evaluates the attribute attr of the flake there, or of its generated + # flake, after collecting garbage. + checkFlake = { + attr, expected, subdir ? false, generated ? false, materialized ? false, + }: (if materialized then '' + test -d "$T"/x && ! test -L "$T"/x || fail "x isn't a directory" + test -w "$T"/x/flake.nix || fail "flake.nix isn't writable" + '' else '' link=$(readlink "$T"/x) ${if subdir then '' expect_eq subdirectory "$(basename "$link")" sub link=$(dirname "$link") '' else ""} expect_eq "store directory" "$(dirname "$link")" /nix/store + '') + '' test ! -e "$T"/.populate/flake-uploads || fail "upload directory remains" gc expect_eq ${attr} "$(nix_eval ${ @@ -219,9 +240,8 @@ assert !(krops.isLocalTarget (targets.remote "/")); inherit (flags) deleteExcluded; exclude = optional (flags.exclude or false) "skip-ex"; filters = optional flags.filters { type = "exclude"; pattern = "skip-f"; }; - inputs = optionalAttrs flags.inputs { dep.follows = "alt"; }; - }; - } // optionalAttrs flags.inputs alt; + } // drawInputs flags.inputs { dep.follows = "alt"; }; + } // optionalAttrs (flags.inputs != "none") alt; check = let excluded = file: set: '' ${if !set then '' @@ -242,10 +262,10 @@ assert !(krops.isLocalTarget (targets.remote "/")); expect_eq stale "$(cat "$T"/x/stale)" stale ''} expect_eq same "$(cat "$T"/x/same)" ${if flags.checksum then "AAAA" else "BBBB"} - '' + optionalString flags.inputs '' + '' + optionalString (flags.inputs != "none") '' # Inputs not drawn from sources aren't delivered, so this needs the # network and can't collect garbage before. - expect_eq msg "$(nix_eval_online "$T"/.populate/flakes/x#msg)" top:alt,sub + expect_eq msg "$(nix_eval_online ${flakeOf flags.inputs}#msg)" top:alt,sub ''; }; }; @@ -254,7 +274,7 @@ assert !(krops.isLocalTarget (targets.remote "/")); checksum = [ false true ]; deleteExcluded = [ false true ]; filters = [ false true ]; - inputs = [ false true ]; + inputs = inputModes; } f ++ [ # The deprecated exclude option, once, because it warns. @@ -264,7 +284,7 @@ assert !(krops.isLocalTarget (targets.remote "/")); deleteExcluded = true; exclude = true; filters = false; - inputs = false; + inputs = "none"; } null; in args // { name = "file-local-exclude"; @@ -306,7 +326,8 @@ assert !(krops.isLocalTarget (targets.remote "/")); flake = matrix "flake" { kind = [ "git" "path" "subdir" "remote" ]; fetchOnTarget = [ false true ]; - inputs = [ false true ]; + inputs = inputModes; + materialize = [ false true ]; } (flags: ctx: let flake = { git = "/fixtures/top"; @@ -315,30 +336,32 @@ assert !(krops.isLocalTarget (targets.remote "/")); remote = "${url}/remote.tar.gz"; }.${flags.kind}; msg = { - git = "top:${if flags.inputs then "alt" else "dep+inner"},sub"; + git = "top:${if flags.inputs != "none" then "alt" else "dep+inner"},sub"; path = msg.git; - remote = "remote:${if flags.inputs then "alt" else "dep+inner"}"; + remote = "remote:${if flags.inputs != "none" then "alt" else "dep+inner"}"; }; in # The flake in a subdirectory has no inputs to draw from sources. - if flags.kind == "subdir" && flags.inputs then null else { + if flags.kind == "subdir" && flags.inputs != "none" then null else { isolate = true; phases = twice ({ sources = { x.flake = { url = flake; - inherit (flags) fetchOnTarget; - inputs = optionalAttrs flags.inputs { dep.follows = "alt"; }; - }; - } // optionalAttrs flags.inputs alt; + inherit (flags) fetchOnTarget materialize; + } // drawInputs flags.inputs { dep.follows = "alt"; }; + } // optionalAttrs (flags.inputs != "none") alt; } // (if copyingWithSudo flags ctx then { expect = "eval-error"; control = withFetchOnTarget; } else { check = if flags.kind == "subdir" then checkFlake { attr = "who"; expected = "sub"; subdir = true; + materialized = flags.materialize; } else checkFlake { - attr = "msg"; expected = msg.${flags.kind}; generated = flags.inputs; + attr = "msg"; expected = msg.${flags.kind}; + generated = flags.inputs == "generated"; + materialized = flags.materialize; }; })); }); @@ -346,10 +369,11 @@ assert !(krops.isLocalTarget (targets.remote "/")); flake-input = concatMap (name: matrix "flake-input-${lib.replaceStrings ["/"] ["."] name}" ({ fetchOnTarget = [ false true ]; + materialize = [ false true ]; } // optionalAttrs (name == "dep") { - inputs = [ false true ]; + inputs = inputModes; }) (flags: ctx: let - inputs = flags.inputs or false; + inputs = flags.inputs or "none"; in { isolate = true; phases = twice ({ @@ -357,10 +381,9 @@ assert !(krops.isLocalTarget (targets.remote "/")); x.flake-input = { url = "/fixtures/top"; inherit name; - inherit (flags) fetchOnTarget; - inputs = optionalAttrs inputs { inner.follows = "alt"; }; - }; - } // optionalAttrs inputs alt; + inherit (flags) fetchOnTarget materialize; + } // drawInputs inputs { inner.follows = "alt"; }; + } // optionalAttrs (inputs != "none") alt; } // (if copyingWithSudo flags ctx then { expect = "eval-error"; control = withFetchOnTarget; @@ -368,11 +391,17 @@ assert !(krops.isLocalTarget (targets.remote "/")); check = { dep = checkFlake { attr = "greeting"; - expected = if inputs then "dep+alt" else "dep+inner"; - generated = inputs; + expected = if inputs != "none" then "dep+alt" else "dep+inner"; + generated = inputs == "generated"; + materialized = flags.materialize; + }; + "dep/inner" = checkFlake { + attr = "who"; expected = "inner"; materialized = flags.materialize; + }; + sub = checkFlake { + attr = "who"; expected = "sub"; subdir = true; + materialized = flags.materialize; }; - "dep/inner" = checkFlake { attr = "who"; expected = "inner"; }; - sub = checkFlake { attr = "who"; expected = "sub"; subdir = true; }; }.${name}; })); })) @@ -382,7 +411,7 @@ assert !(krops.isLocalTarget (targets.remote "/")); shallow = [ false true ]; fetchAlways = [ false true ]; cleanExclude = [ false true ]; - inputs = [ false true ]; + inputs = inputModes; } (flags: _: let check = '' expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitHead} @@ -390,11 +419,12 @@ assert !(krops.isLocalTarget (targets.remote "/")); if flags.shallow then "1" else "2" } test -e "$T"/x/two || fail "two missing" - '' + optionalString flags.inputs '' + '' + optionalString (flags.inputs == "generated") '' grep -q '"git+file:' "$T"/.populate/flakes/x/flake.nix || fail "the Git work tree isn't referred to as git+file:" + '' + optionalString (flags.inputs != "none") '' gc - expect_eq msg "$(nix_eval "$T"/.populate/flakes/x#msg)" git:alt + expect_eq msg "$(nix_eval ${flakeOf flags.inputs}#msg)" git:alt ''; sources = ref: { x.git = { @@ -402,9 +432,8 @@ assert !(krops.isLocalTarget (targets.remote "/")); inherit ref; inherit (flags) shallow fetchAlways; clean.exclude = optional flags.cleanExclude "keepme"; - inputs = optionalAttrs flags.inputs { dep.follows = "alt"; }; - }; - } // optionalAttrs flags.inputs alt; + } // drawInputs flags.inputs { dep.follows = "alt"; }; + } // optionalAttrs (flags.inputs != "none") alt; in { phases = [ # The older commit first, so deploying HEAD changes the work tree, and @@ -501,7 +530,7 @@ assert !(krops.isLocalTarget (targets.remote "/")); # writeDeploy's arguments to nixos-rebuild, which a stand-in records. deploy = matrix "deploy" { - flake = [ "none" "source" "generated" ]; + flake = [ "none" "source" "rewrite" "generated" ]; etcFlake = [ false true ]; buildTarget = [ false true ]; } (flags: ctx: let @@ -517,6 +546,7 @@ assert !(krops.isLocalTarget (targets.remote "/")); ++ { none = []; source = [ "--flake" "${build.path}/x" ]; + rewrite = [ "--flake" "${build.path}/x" ]; # Generated flakes as path:, which an enclosing Git work tree doesn't # capture. generated = [ "--flake" "path:${build.path}/.populate/flakes/x" ]; @@ -544,9 +574,8 @@ assert !(krops.isLocalTarget (targets.remote "/")); x.flake = { url = "/fixtures/top"; fetchOnTarget = ctx.targetKind == "sudo"; - inputs = optionalAttrs (flags.flake == "generated") { - dep.follows = "alt"; - }; + } // drawInputs (if flags.flake == "source" then "none" else flags.flake) { + dep.follows = "alt"; }; }; deploy = { @@ -572,68 +601,84 @@ assert !(krops.isLocalTarget (targets.remote "/")); } // args); copying = targetKind != "sudo"; fetchOnTarget = !copying; - in [ - # A generated flake draws an input from a source with a generated flake. - (s "chain" { + # A scenario for each way of drawing inputs, which f gets as mode, along + # with draw, which adds inputs drawn that way to a source's options, and + # flake, which gives the flake to evaluate for a source. + modes = name: f: map + (mode: s "${name}-${mode}" (f { + draw = drawInputs mode; + flake = n: + if mode == "generated" then ''"$T"/.populate/flakes/${n}'' else ''"$T"/${n}''; + inherit mode; + })) + [ "rewrite" "generated" ]; + in lib.flatten [ + # A source draws an input from a source that draws inputs itself. + (modes "chain" ({ draw, flake, ... }: { phases = twice { sources = alt // { - a.flake = { url = "/fixtures/top"; inherit fetchOnTarget; inputs.dep.follows = "b"; }; + a.flake = { url = "/fixtures/top"; inherit fetchOnTarget; } // draw { dep.follows = "b"; }; b.flake-input = { url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; - inputs.inner.follows = "alt"; - }; + } // draw { inner.follows = "alt"; }; }; check = '' gc - expect_eq msg "$(nix_eval "$T"/.populate/flakes/a#msg)" top:dep+alt,sub + expect_eq msg "$(nix_eval ${flake "a"}#msg)" top:dep+alt,sub ''; }; - }) - # An input of an input drawn from a source gets overridden at that source. - (s "nested" { + })) + # An input of an input drawn from a source gets drawn from another + # source. + (modes "nested" ({ draw, flake, ... }: { phases = twice { sources = alt // { x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; - inputs.dep = { follows = "b"; inputs.inner.follows = "alt"; }; - }; + } // draw { dep = { follows = "b"; inputs.inner.follows = "alt"; }; }; b.flake-input = { url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; }; }; check = '' gc - expect_eq msg "$(nix_eval "$T"/.populate/flakes/x#msg)" top:dep+alt,sub + expect_eq msg "$(nix_eval ${flake "x"}#msg)" top:dep+alt,sub ''; }; - }) - # A generated flake gets locked again when a source it draws from - # changes. - (s "relock" (let - x.file = { path = "/fixtures/filesrc"; inputs.dep.follows = "a"; }; + })) + # The lock file gets rewritten or generated again when a source inputs + # get drawn from changes. + (modes "relock" ({ draw, flake, ... }: let + x.file = { path = "/fixtures/filesrc"; } // draw { dep.follows = "a"; }; in { phases = [ { sources = { inherit x; a.file = "/fixtures/alt"; }; check = '' - expect_eq msg "$(nix_eval_online "$T"/.populate/flakes/x#msg)" top:alt,sub + expect_eq msg "$(nix_eval_online ${flake "x"}#msg)" top:alt,sub ''; } { sources = { inherit x; a.file = "/fixtures/alt2"; }; check = '' - expect_eq msg "$(nix_eval_online "$T"/.populate/flakes/x#msg)" top:alt2,sub + expect_eq msg "$(nix_eval_online ${flake "x"}#msg)" top:alt2,sub ''; } ]; })) - # A generated flake gets removed once its source draws no inputs anymore. - (s "removal" { + # What drawing inputs needs gets removed once a source draws no inputs + # anymore. + (modes "removal" ({ draw, mode, ... }: let + state = { + rewrite = "derived/x/result"; + generated = "flakes/x/flake.lock"; + }.${mode}; + in { phases = [ { sources = alt // { - x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; inputs.dep.follows = "alt"; }; + x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; } // draw { dep.follows = "alt"; }; }; check = '' - test -e "$T"/.populate/flakes/x/flake.lock || fail "generated flake missing" + test -e "$T"/.populate/${state} || fail "${state} missing" ''; } { @@ -641,11 +686,158 @@ assert !(krops.isLocalTarget (targets.remote "/")); x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; }; }; check = '' - test ! -e "$T"/.populate/flakes/x || fail "generated flake remains" + test ! -e "$T"/.populate/${dirOf state} || fail "${dirOf state} remains" + '' + checkFlake { attr = "msg"; expected = "top:dep+inner,sub"; }; + } + ]; + })) + # Patches get applied in order to a copy, which becomes a store path and + # gets made again only when the patches change. + (s "patches" (let + x = patches: { + x.flake-input = { + url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget patches; + }; + }; + both = x [ "/fixtures/patches/hello.patch" "/fixtures/patches/hello2.patch" ]; + in { + phases = [ + { + sources = both; + check = checkFlake { attr = "greeting"; expected = "hello2+inner"; }; + # The key changes whenever the copy gets made again, which + # the store path doesn't show, being the same. + mutate = '' + readlink "$T"/x > "$T".first + stat -c %y "$T"/.populate/derived/x/key > "$T".key + ''; + } + { + sources = both; + check = checkFlake { attr = "greeting"; expected = "hello2+inner"; } + '' + expect_eq "store path" "$(readlink "$T"/x)" "$(cat "$T".first)" + expect_eq "copy made at" "$(stat -c %y "$T"/.populate/derived/x/key)" "$(cat "$T".key)" + ''; + } + { + sources = x [ "/fixtures/patches/hello.patch" ]; + check = checkFlake { attr = "greeting"; expected = "hello+inner"; } + '' + test "$(readlink "$T"/x)" != "$(cat "$T".first)" || fail "store path unchanged" + expect_eq patches "$(ls "$T"/.populate/patches/x)" 0001-hello.patch + ''; + } + { + sources = x []; + check = checkFlake { attr = "greeting"; expected = "dep+inner"; } + '' + test ! -e "$T"/.populate/patches || fail "patches remain" + test ! -e "$T"/.populate/derived || fail "derived remains" ''; } ]; + })) + (s "patch-fails" { + phases = [{ + sources.x.flake-input = { + url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; + patches = [ "/fixtures/patches/bad.patch" ]; + }; + expect.fail = "error: patch bad.patch does not apply to source x"; + }]; }) + # Patches apply in the subdirectory the target path shows. + (s "patch-subdir" { + phases = twice { + sources.x.flake-input = { + url = "/fixtures/top"; name = "sub"; inherit fetchOnTarget; + patches = [ "/fixtures/patches/sub.patch" ]; + }; + check = checkFlake { attr = "who"; expected = "patched"; subdir = true; }; + }; + }) + # A copy with a rewritten lock file gets made again only when a source + # it draws inputs from changes. + (s "rewrite-cache" (let + x.flake-input = { + url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; + inputs.inner.follows = "a"; + }; + in { + phases = [ + { + sources = { inherit x; a.file = "/fixtures/alt"; }; + check = checkFlake { attr = "greeting"; expected = "dep+alt"; }; + # The key changes whenever the copy gets made again, which + # the store path doesn't show, being the same. + mutate = '' + readlink "$T"/x > "$T".first + stat -c %y "$T"/.populate/derived/x/key > "$T".key + ''; + } + { + sources = { inherit x; a.file = "/fixtures/alt"; }; + check = checkFlake { attr = "greeting"; expected = "dep+alt"; } + '' + expect_eq "store path" "$(readlink "$T"/x)" "$(cat "$T".first)" + expect_eq "copy made at" "$(stat -c %y "$T"/.populate/derived/x/key)" "$(cat "$T".key)" + ''; + } + { + sources = { inherit x; a.file = "/fixtures/alt2"; }; + check = checkFlake { attr = "greeting"; expected = "dep+alt2"; } + '' + test "$(readlink "$T"/x)" != "$(cat "$T".first)" || fail "store path unchanged" + ''; + } + ]; + })) + # A materialized input is a writable copy, patched and with a rewritten + # lock file, which gets made again only when what it derives from + # changes, and becomes a link again when not materialized anymore. + (s "materialize" (let + x = patches: alt // { + x.flake-input = { + url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget patches; + materialize = true; + inputs.inner.follows = "alt"; + }; + }; + hello = [ "/fixtures/patches/hello.patch" ]; + in { + phases = [ + { + sources = x hello; + check = '' + test ! -L "$T"/x || fail "x is a link" + test -w "$T"/x/flake.nix || fail "flake.nix isn't writable" + gc + expect_eq greeting "$(nix_eval "$T"/x#greeting)" hello+alt + ''; + mutate = '' + echo local > "$T"/x/local + ''; + } + { + sources = x hello; + check = '' + expect_eq local "$(cat "$T"/x/local)" local + ''; + } + { + sources = x (hello ++ [ "/fixtures/patches/hello2.patch" ]); + check = '' + test ! -e "$T"/x/local || fail "local remains" + gc + expect_eq greeting "$(nix_eval "$T"/x#greeting)" hello2+alt + ''; + } + { + sources = alt // { + x.flake-input = { url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; }; + }; + check = checkFlake { attr = "greeting"; expected = "dep+inner"; } + '' + test ! -e "$T"/.populate/derived || fail "derived remains" + ''; + } + ]; + })) # Roots follow the delivered input. (s "retarget" { phases = [ @@ -780,7 +972,18 @@ assert !(krops.isLocalTarget (targets.remote "/")); roots = n: '' expect_eq roots "$(ls "$T"/.populate/flake-inputs/x 2>/dev/null | wc -l)" ${toString n} ''; - in [ + # A scenario for each way of drawing inputs, which f gets as mode, along + # with draw, which adds inputs drawn that way to a source's options, and + # flake, which gives the flake to evaluate for a source. + modes = name: f: map + (mode: s "${name}-${mode}" (f { + draw = drawInputs mode; + flake = n: + if mode == "generated" then ''"$T"/.populate/flakes/${n}'' else ''"$T"/${n}''; + inherit mode; + })) + [ "rewrite" "generated" ]; + in lib.flatten [ # The registry entry kropsremote, which fixtures.sh adds on the deploying # machine, points at ${url}/remote.tar.gz. The deploying machine has no # global registry, which it couldn't download. @@ -796,17 +999,19 @@ assert !(krops.isLocalTarget (targets.remote "/")); check = checkFlake { attr = "who"; expected = "sub"; subdir = true; }; }; }) - (s "subdir-inputs" { + (modes "subdir-inputs" ({ draw, mode, ... }: { phases = twice { sources = alt // { x.flake = { url = "git+file:/fixtures/monoin?dir=sub"; inherit fetchOnTarget; - inputs.dep.follows = "alt"; - }; + } // draw { dep.follows = "alt"; }; + }; + check = checkFlake { + attr = "msg"; expected = "monoin:alt"; subdir = true; + generated = mode == "generated"; }; - check = checkFlake { attr = "msg"; expected = "monoin:alt"; subdir = true; generated = true; }; }; - }) + })) (s "dirty" { phases = twice { sources.x.flake = { url = "/fixtures/dirty"; inherit fetchOnTarget; }; @@ -942,49 +1147,50 @@ assert !(krops.isLocalTarget (targets.remote "/")); } ]; })) - (s "override-generated" { + # An input of an input drawn from a source that draws inputs itself + # gets drawn from another source. + (modes "override" ({ draw, flake, ... }: { phases = twice { sources = { x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; - inputs.dep = { follows = "b"; inputs.q.follows = "c2"; }; - }; - b.file = { path = "/fixtures/two"; inputs.p.follows = "c1"; }; + } // draw { dep = { follows = "b"; inputs.q.follows = "c2"; }; }; + b.file = { path = "/fixtures/two"; } // draw { p.follows = "c1"; }; c1.file = "/fixtures/c1"; c2.file = "/fixtures/c2"; }; check = '' - expect_eq msg "$(nix_eval_online "$T"/.populate/flakes/x#msg)" top:two:c1+c2,sub + expect_eq msg "$(nix_eval_online ${flake "x"}#msg)" top:two:c1+c2,sub ''; }; - }) - (s "nonflake-input" { + })) + (modes "nonflake-input" ({ draw, flake, ... }: { phases = twice { sources = { - x.file = { path = "/fixtures/usedata"; inputs.data.follows = "data"; }; + x.file = { path = "/fixtures/usedata"; } // draw { data.follows = "data"; }; data.file = "/fixtures/data"; }; check = '' - expect_eq msg "$(nix_eval "$T"/.populate/flakes/x#msg)" data:hello + expect_eq msg "$(nix_eval ${flake "x"}#msg)" data:hello ''; }; - }) + })) # The target directory in a Git work tree. - (s "gitdir" { + (modes "gitdir" ({ draw, flake, ... }: { prepare = '' mkdir -p "$T" git -C "$T" init -q ''; phases = twice { sources = alt // { - x.file = { path = "/fixtures/filesrc"; inputs.dep.follows = "alt"; }; + x.file = { path = "/fixtures/filesrc"; } // draw { dep.follows = "alt"; }; }; check = '' # Not just the path, which Nix takes for the enclosing work tree. - expect_eq msg "$(nix_eval_online path:"$T"/.populate/flakes/x#msg)" top:alt,sub + expect_eq msg "$(nix_eval_online path:${flake "x"}#msg)" top:alt,sub ''; }; - }) + })) # Output of the same length, which rsync's quick check doesn't notice. (s "pipe-change" { prepareDeployer = '' @@ -1011,17 +1217,17 @@ assert !(krops.isLocalTarget (targets.remote "/")); } ]; }) - (s "source-name" { + (modes "source-name" ({ draw, flake, ... }: { phases = twice { sources = { "alt-1.0".file = "/fixtures/alt"; - x.file = { path = "/fixtures/filesrc"; inputs.dep.follows = "alt-1.0"; }; + x.file = { path = "/fixtures/filesrc"; } // draw { dep.follows = "alt-1.0"; }; }; check = '' - expect_eq msg "$(nix_eval_online "$T"/.populate/flakes/x#msg)" top:alt,sub + expect_eq msg "$(nix_eval_online ${flake "x"}#msg)" top:alt,sub ''; }; - }) + })) # An upload an interrupted run left behind. (s "upload-leftover" { phases = [ |
