aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md107
-rw-r--r--lib/default.nix17
-rw-r--r--lib/types/populate.nix34
-rw-r--r--pkgs/krops/default.nix2
-rw-r--r--pkgs/populate/default.nix303
-rwxr-xr-xtests/fixtures.sh23
-rw-r--r--tests/scenarios.nix384
7 files changed, 714 insertions, 156 deletions
diff --git a/README.md b/README.md
index 8850fb8..87d5e36 100644
--- a/README.md
+++ b/README.md
@@ -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 = [