aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-13 03:41:54 +0000
committertv <tv@krebsco.de>2026-11-13 03:41:54 +0000
commite90426e9abf1370851cd24557ae7ac897cdef297 (patch)
tree4a7ab50874f6771e137a5799e1e870228484918e
parent0842652921304ba76fbd321756b6b545abc81b01 (diff)
populate: fail on inputs drawn that the flake doesn't have
Nix ignores an input that --override-input names but the flake doesn't have, with a warning, so a misspelt input name in inputs draws nothing, and the source keeps using its own input. Before changing anything, populate locks the flake of each source that draws inputs, with an empty flake in place of each input drawn, which may be unreachable, and fails, naming the source and the input, unless the flake has each input drawn. Below an input drawn from a source, the path continues in the flake of that source. An edge case misspells an input of the source's flake, and one below an input drawn from a source, both ways of drawing inputs.
-rw-r--r--README.md6
-rw-r--r--pkgs/populate/default.nix75
-rw-r--r--pkgs/populate/flake-jq.nix23
-rw-r--r--tests/scenarios.nix24
4 files changed, 125 insertions, 3 deletions
diff --git a/README.md b/README.md
index c917d3f..d88f4e1 100644
--- a/README.md
+++ b/README.md
@@ -704,9 +704,9 @@ a `flake` or `flake-input` source, and neither does what they pin.
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
-`flake = false`.
+contains no `flake.nix`, or if its flake has no such input, before anything
+changes. Sources that inputs get drawn from don't need to be flakes, as long
+as the inputs drawing from them are declared with `flake = false`.
## Tests
diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix
index 71844cb..e8d3bdc 100644
--- a/pkgs/populate/default.nix
+++ b/pkgs/populate/default.nix
@@ -267,6 +267,7 @@ let
# 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
+ inherit (import ./flake-jq.nix) lockHasInput;
sentinel = "${target.path}/.populate";
flakesDir = "${sentinel}/flakes";
derivedDir = "${sentinel}/derived";
@@ -325,6 +326,21 @@ let
else
if parents == [] then path
else parent ++ rest;
+ # The inputs that drawing the input at path of the flake in the source
+ # named name needs to exist, as { name, path } of the source whose flake
+ # has the input at path. Like drawnPath, the part below an input drawn
+ # from a source is the flake of that source's.
+ inputChecks = name: path: let
+ follows = flakeFollows sources.${name};
+ parents = filter
+ (prefix: any (x: x.path == prefix) follows)
+ (map (n: take n path) (range 1 (length path - 1)));
+ parent = last parents;
+ from = (findFirst (x: x.path == parent) null follows).source;
+ in
+ if parents == [] then [ { inherit name path; } ]
+ else [ { inherit name; path = parent; } ]
+ ++ inputChecks from (drop (length parent) path);
# 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 */ ''
@@ -587,6 +603,65 @@ let
exit 1
fi
+ # Nix ignores inputs drawn that the flake doesn't have, with a
+ # warning, so a misspelt input name would draw nothing. Prints the
+ # flake URL of the flake in the source named $1 as delivered.
+ own_flake_url() {
+ case " "${quote (toString (attrNames derived))}" " in
+ *" $1 "*)
+ printf 'path:%s' "$(url_path "$(readlink -f ${quote derivedDir}/"$1"/original)")"
+ ;;
+ *)
+ flake_url "$1"
+ ;;
+ esac
+ }
+ # The lock files of the flakes in the sources, as Nix locks them now,
+ # with an empty flake in place of each input drawn from a source,
+ # which may not be reachable, as the one the flake's lock file pins.
+ locks_dir=$(mktemp -d)
+ echo '{ outputs = _: {}; }' > "$locks_dir"/flake.nix
+ ${concatMapStrings (name: let
+ follows = flakeFollows sources.${name};
+ in /* sh */ ''
+ set --
+ ${concatMapStrings (x: /* sh */ ''
+ set -- "$@" --override-input ${quote (concatStringsSep "/" x.path)} "path:$locks_dir"
+ '') follows}
+ if ! nix flake metadata --json --no-write-lock-file "$@" \
+ "$(own_flake_url ${quote name})" > "$locks_dir"/${quote name}.json; then
+ >&2 printf 'error: source %s draws inputs from other sources, but its flake can'"'"'t be locked\n' \
+ ${quote name}
+ exit 1
+ fi
+ '') (unique (concatMap (name: concatMap (x: map (c: c.name) (inputChecks name x.path)) (flakeFollows sources.${name})) (attrNames drawing)))}
+ # Fails, naming the input, unless the flake in the source named $1
+ # has an input at the path $2, a JSON list of input names. $3 is
+ # the source that the input $4 of the source $5 gets drawn from.
+ require_input() {
+ if ! test "$(
+ METADATA=$(cat "$locks_dir/$1.json") INPUT=$2 nix eval --impure --raw --expr ${quote ''
+ (${lockHasInput})
+ (builtins.fromJSON (builtins.getEnv "METADATA")).locks
+ (builtins.fromJSON (builtins.getEnv "INPUT"))
+ ''}
+ )" = 1; then
+ if test "$1" = "$5"; then
+ >&2 printf 'error: source %s draws input %s from source %s, but its flake has no input %s\n' \
+ "$5" "$4" "$3" "$4"
+ else
+ >&2 printf 'error: source %s draws input %s from source %s, but the flake of source %s has no input %s\n' \
+ "$5" "$4" "$3" "$1" "$(printf %s "$2" | tr -d '[]"' | tr , /)"
+ fi
+ exit 1
+ fi
+ }
+ ${concatMapStrings (name: concatMapStrings (x: concatMapStrings (c: /* sh */ ''
+ require_input ${quote c.name} ${quote (toJSON c.path)} \
+ ${quote x.source} ${quote (concatStringsSep "/" x.path)} ${quote name}
+ '') (inputChecks name x.path)) (flakeFollows sources.${name})) (attrNames drawing)}
+ rm -R "$locks_dir"
+
${optionalString (generated != {}) /* sh */ ''
mkdir -p ${quote flakesDir}
''}
diff --git a/pkgs/populate/flake-jq.nix b/pkgs/populate/flake-jq.nix
index 63afa9d..446b2ef 100644
--- a/pkgs/populate/flake-jq.nix
+++ b/pkgs/populate/flake-jq.nix
@@ -2,6 +2,29 @@
# their own, so tests can run them: jq programs, which run on the deploying
# machine, and a Nix function for the target, which may lack jq.
rec {
+ # Nix function returning 1 if the lock file locks has an input at path, a
+ # list of input names, resolving inputs that follow others, and else 0.
+ lockHasInput = /* nix */ ''
+ locks: path: let
+ nodes = locks.nodes;
+ input = key: name: (nodes.''${key}.inputs or {}).''${name} or null;
+ # The node an input refers to, following follows from the root.
+ node = ref:
+ if ref == null then null
+ else if builtins.isList ref then
+ builtins.foldl'
+ (key: name: if key == null then null else node (input key name))
+ locks.root
+ ref
+ else ref;
+ has = key: path:
+ if path == [] then true
+ else let next = node (input key (builtins.head path)); in
+ next != null && has next (builtins.tail path);
+ in
+ if has locks.root path then "1" else "0"
+ '';
+
# Nix function returning the subdirectory containing the flake that nix
# flake metadata describes, like subdirOf does in jq.
metadataSubdir = /* nix */ ''
diff --git a/tests/scenarios.nix b/tests/scenarios.nix
index 89a3b09..e23d14e 100644
--- a/tests/scenarios.nix
+++ b/tests/scenarios.nix
@@ -1445,6 +1445,30 @@ assert !(krops.isLocalTarget (targets.remote "/"));
'';
}];
})
+ # Misspelt input names draw nothing, so they fail before anything
+ # changes: of the source's flake, and below an input drawn from a source.
+ (modes "inputs-typo" ({ draw, ... }: {
+ phases = [
+ {
+ sources = alt // {
+ x.flake-input = {
+ url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
+ } // draw { innr.follows = "alt"; };
+ };
+ expect.fail = "error: source x draws input innr from source alt, but its flake has no input innr";
+ }
+ {
+ sources = {
+ x.flake = {
+ url = "/fixtures/top"; inherit fetchOnTarget;
+ } // draw { dep = { follows = "b"; inputs.qq.follows = "c2"; }; };
+ b.file = "/fixtures/two";
+ c2.file = "/fixtures/c2";
+ };
+ expect.fail = "error: source x draws input dep/qq from source c2, but the flake of source b has no input qq";
+ }
+ ];
+ }))
# A signal ending populate leaves neither the ssh master nor the
# temporary directory.
(s "interrupt" {