aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md15
-rw-r--r--lib/types/populate.nix4
-rw-r--r--pkgs/populate/default.nix188
-rw-r--r--pkgs/populate/flake-jq.nix19
-rwxr-xr-xtests/fixtures.sh102
-rw-r--r--tests/scenarios.nix118
6 files changed, 387 insertions, 59 deletions
diff --git a/README.md b/README.md
index ae7eca2..9f1b760 100644
--- a/README.md
+++ b/README.md
@@ -476,6 +476,15 @@ Supported attributes:
e.g. `stockholm/nixpkgs`, and resolved as the flake's lock file resolves
them, so an input that follows another one names the other one.
+* `requireInputs` (optional) -
+ boolean that controls whether populating fails if the inputs that the
+ input's own lock file pins can't be delivered, e.g. because the lock file
+ pins something unreachable, or lacks an input its `flake.nix` has. With
+ `false`, populate prints a warning and delivers the input with the inputs
+ Nix locks when it adds what the lock file lacks, or else without its
+ inputs. Evaluating the input as a flake then needs network access.
+ Defaults to `true`.
+
* `fetchOnTarget` (optional) -
boolean that controls whether the target fetches the input itself instead
of receiving it from the deploying machine. The target then uses the
@@ -669,8 +678,10 @@ 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
+change. Inputs that get drawn from other sources don't get delivered with
+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
diff --git a/lib/types/populate.nix b/lib/types/populate.nix
index a278ffd..1a18b62 100644
--- a/lib/types/populate.nix
+++ b/lib/types/populate.nix
@@ -233,6 +233,10 @@
name = lib.mkOption {
type = lib.types.str;
};
+ requireInputs = lib.mkOption {
+ default = true;
+ type = lib.types.bool;
+ };
};
};
git = lib.types.submodule {
diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix
index 002326a..2811e27 100644
--- a/pkgs/populate/default.nix
+++ b/pkgs/populate/default.nix
@@ -550,7 +550,8 @@ let
pop.flake = target: source: pop.flake-input target (source // { name = null; });
pop.flake-input = target: source: let
- inherit (import ./flake-jq.nix) selected selfIsRemote selfLocked;
+ inherit (import ./flake-jq.nix)
+ metadataSubdir selected selfIsRemote selfLocked;
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;
@@ -608,31 +609,81 @@ let
${quote source.url} ${quote (toString source.name)}
exit 1
'';
+ # The inputs of the flake in this source that get drawn from other
+ # sources, as paths of input names.
+ overridden = map (x: x.path) (flakeFollows { type = "flake"; flake = source; });
+ requireInputs = source.requireInputs or true;
# Shell function printing the store path $1, followed by the inputs its
- # flake in the subdirectory $2 pins in its own lock file, using the nix
- # command nix. Without a lock file, locking would change the flake, so
- # there are no such inputs.
- closure = nix: /* sh */ ''
+ # flake in the subdirectory $2 pins in its own lock file, using the
+ # commands nix and mktemp. Without a lock file, locking would change the
+ # flake, so there are no such inputs. Inputs that get drawn from other
+ # sources are left out, with all they pin: Nix archives an empty flake
+ # in their place, so it doesn't fetch them.
+ closure = { nix, mktemp }: /* sh */ ''
+ # Prints the store paths in nix flake archive's output $1.
+ closure_paths() {
+ ARCHIVE=$1 OVERRIDDEN=${quote (toJSON overridden)} \
+ ${nix} eval --impure --raw --expr '
+ let
+ overridden = builtins.fromJSON (builtins.getEnv "OVERRIDDEN");
+ # Inputs given by relative paths are part of their parent.
+ paths = prefix: node: builtins.concatMap
+ (name: let
+ input = node.inputs.''${name};
+ path = prefix ++ [ name ];
+ in
+ if builtins.elem path overridden then []
+ else
+ (if input ? path then [ input.path ] else []) ++
+ paths path input)
+ (builtins.attrNames (node.inputs or {}));
+ in
+ builtins.concatStringsSep "\n"
+ (paths [] (builtins.fromJSON (builtins.getEnv "ARCHIVE")))
+ '
+ echo
+ }
closure() {
printf '%s\n' "$1"
closure_dir=$1''${2:+/$2}
if test -e "$closure_dir"/flake.nix && test -e "$closure_dir"/flake.lock; then
- ARCHIVE=$(
- ${nix} flake archive --json --no-update-lock-file \
- "path:$1''${2:+?dir=$2}"
- ) \
- ${nix} eval --impure --raw --expr '
- let
- # Inputs given by relative paths are part of their parent.
- paths = node: builtins.concatMap
- (input:
- (if input ? path then [ input.path ] else []) ++ paths input)
- (builtins.attrValues (node.inputs or {}));
- in
- builtins.concatStringsSep "\n"
- (paths (builtins.fromJSON (builtins.getEnv "ARCHIVE")))
- '
- echo
+ closure_flake=path:$1''${2:+?dir=$2}
+ set --
+ ${optionalString (overridden != []) /* sh */ ''
+ closure_empty=$(${mktemp} -d)
+ echo '{ outputs = _: {}; }' > "$closure_empty"/flake.nix
+ ${concatMapStrings (path: /* sh */ ''
+ set -- "$@" --override-input ${quote (concatStringsSep "/" path)} "path:$closure_empty"
+ '') overridden}
+ ''}
+ if closure_archive=$(
+ ${nix} flake archive --json --no-update-lock-file "$@" "$closure_flake"
+ ); then
+ closure_paths "$closure_archive"
+ else
+ ${if requireInputs then /* sh */ ''
+ >&2 printf 'error: source %s: the inputs its own lock file pins can'"'"'t be delivered%s\n' \
+ ${quote name} ${quote (optionalString (source.name != null)
+ ". Set its requireInputs to false to deliver it anyway.")}
+ exit 1
+ '' else /* sh */ ''
+ # Without --no-update-lock-file, Nix adds what the lock file
+ # lacks.
+ if closure_archive=$(
+ ${nix} flake archive --json --no-write-lock-file "$@" "$closure_flake"
+ ); then
+ >&2 printf 'warning: source %s: its own lock file needs to be updated; delivering it with the inputs Nix locks now, which evaluating it may not use\n' \
+ ${quote name}
+ closure_paths "$closure_archive"
+ else
+ >&2 printf 'warning: source %s: the inputs its own lock file pins can'"'"'t be delivered; delivering it without them\n' \
+ ${quote name}
+ fi
+ ''}
+ fi
+ ${optionalString (overridden != []) /* sh */ ''
+ rm -R "$closure_empty"
+ ''}
fi
}
'';
@@ -709,6 +760,7 @@ let
${requireFlakes "fetching flake inputs on the target"}
read -r selected
read -r self
+ read -r expected
if test -n "$self" && ! nix-store --check-validity "$self" 2>/dev/null; then
name=$(basename "$self" | cut -c34-)
mv ${quote upload}/tree ${quote upload}/"$name"
@@ -720,16 +772,27 @@ let
exit 1
fi
fi
+ # Fetching contacts where the input comes from even if the store has
+ # what the lock file pins.
+ if test -n "$expected" && ! nix-store --check-validity "$expected" 2>/dev/null; then
+ expected=
+ fi
# The store path and the subdirectory, on separate lines. Inputs
# referring to local paths can only be fetched if the target has them
# as well.
fetched=$(
- FLAKE=${quote source.url} SELECTED=$selected SELF=$self \
+ FLAKE=${quote source.url} INPUT=${quote (toString source.name)} \
+ SELECTED=$selected SELF=$self EXPECTED=$expected \
nix eval --impure --raw --expr '
let
selected = builtins.fromJSON (builtins.getEnv "SELECTED");
+ expected = builtins.getEnv "EXPECTED";
+ selectedPath =
+ if selected.self || expected != "" then null
+ else localPath selected.locked;
path =
if selected.self then builtins.getEnv "SELF"
+ else if expected != "" then expected
else (builtins.fetchTree selected.locked).outPath;
dir = path + (if selected.subdir == "" then "" else "/" + selected.subdir);
lock =
@@ -750,7 +813,9 @@ let
(builtins.attrNames lock.nodes);
key = builtins.head missing;
in
- if missing != [] then
+ if selectedPath != null && builtins.substring 0 1 selectedPath == "/" && !builtins.pathExists selectedPath then
+ throw "flake ''${builtins.getEnv "FLAKE"}: input ''${builtins.getEnv "INPUT"} refers to ''${selectedPath}, which does not exist on the target"
+ else if missing != [] then
throw "flake ''${builtins.getEnv "FLAKE"}: input ''${key} refers to ''${localPath lock.nodes.''${key}.locked}, which does not exist on the target"
else
path + "\n" + selected.subdir
@@ -758,8 +823,10 @@ let
)
path=$(printf '%s\n' "$fetched" | sed -n 1p)
subdir=$(printf '%s\n' "$fetched" | sed -n 2p)
- ${closure "nix"}
- { printf '%s\n' "$subdir"; closure "$path" "$subdir"; } | {
+ ${closure { nix = "nix"; mktemp = "mktemp"; }}
+ # Not in the pipeline, where failing wouldn't fail populate.
+ paths=$(closure "$path" "$subdir")
+ printf '%s\n%s\n' "$subdir" "$paths" | {
${link}
}
'';
@@ -768,33 +835,20 @@ let
set -efu
${requireFlakes "fetching flakes on the target"}
metadata=$(nix flake metadata --json --no-update-lock-file ${quote source.url})
- archive=$(nix flake archive --json --no-update-lock-file ${quote source.url})
- lines=$(
- METADATA=$metadata ARCHIVE=$archive \
- nix eval --impure --raw --expr '
+ # The store path and the subdirectory, on separate lines.
+ fetched=$(
+ METADATA=$metadata nix eval --impure --raw --expr '
let
- # Inputs given by relative paths are part of their parent.
- paths = node:
- (if node ? path then [ node.path ] else []) ++
- builtins.concatMap paths (builtins.attrValues (node.inputs or {}));
metadata = builtins.fromJSON (builtins.getEnv "METADATA");
- archive = builtins.fromJSON (builtins.getEnv "ARCHIVE");
- # The subdirectory, as attribute dir or as query parameter dir
- # of the URL, depending on the version of Nix.
- subdir = ref:
- ref.dir or (let
- match = builtins.match ".*[?&]dir=([^&]*).*" (ref.url or "");
- in
- if match == null then "" else builtins.head match);
- subdirs = builtins.filter (dir: dir != "")
- (map subdir [ (metadata.locked or {}) (metadata.original or {}) ]);
in
- builtins.concatStringsSep "\n"
- ([ (if subdirs == [] then "" else builtins.head subdirs) ] ++
- paths archive)
+ metadata.path + "\n" + (${metadataSubdir}) metadata
'
)
- printf '%s\n' "$lines" | {
+ path=$(printf '%s\n' "$fetched" | sed -n 1p)
+ subdir=$(printf '%s\n' "$fetched" | sed -n 2p)
+ ${closure { nix = "nix"; mktemp = "mktemp"; }}
+ paths=$(closure "$path" "$subdir")
+ printf '%s\n%s\n' "$subdir" "$paths" | {
${link}
}
'';
@@ -855,22 +909,46 @@ let
''}
fi
+ # The store path the lock file pins what's selected to, which is the
+ # one of a directory named source with the locked hash. A store that
+ # has it needs no fetching, which contacts where the input comes from
+ # even then.
+ expected=
+ hash=$(printf '%s\n' "$selected" | ${jq}/bin/jq -r '.locked.narHash // empty')
+ case $hash in
+ sha256-*)
+ expected=$(
+ ${nix}/bin/nix-store --print-fixed-path --recursive sha256 "$(
+ printf %s "''${hash#sha256-}" |
+ ${coreutils}/bin/base64 -d |
+ ${coreutils}/bin/od -An -v -tx1 |
+ ${coreutils}/bin/tr -d ' \n'
+ )" source
+ )
+ ;;
+ esac
+
${if fetchOnTarget then /* sh */ ''
- printf '%s\n%s\n' "$selected" "$self" | {
+ printf '%s\n%s\n%s\n' "$selected" "$self" "$expected" | {
${runShell target fetchAndLink}
}
'' else /* sh */ ''
path=$self
if test -z "$path"; then
- path=$(
- SELECTED=$selected ${nix'} eval --impure --raw --expr '
- (builtins.fetchTree
- (builtins.fromJSON (builtins.getEnv "SELECTED")).locked).outPath
- '
- )
+ if test -n "$expected" &&
+ ${nix}/bin/nix-store --check-validity "$expected" 2>/dev/null; then
+ path=$expected
+ else
+ path=$(
+ SELECTED=$selected ${nix'} eval --impure --raw --expr '
+ (builtins.fetchTree
+ (builtins.fromJSON (builtins.getEnv "SELECTED")).locked).outPath
+ '
+ )
+ fi
fi
subdir=$(printf '%s\n' "$selected" | ${jq}/bin/jq -r .subdir)
- ${closure nix'}
+ ${closure { nix = nix'; mktemp = "${coreutils}/bin/mktemp"; }}
paths=$(closure "$path" "$subdir")
${optionalString (!isLocalTarget target) /* sh */ ''
diff --git a/pkgs/populate/flake-jq.nix b/pkgs/populate/flake-jq.nix
index 41edcc5..63afa9d 100644
--- a/pkgs/populate/flake-jq.nix
+++ b/pkgs/populate/flake-jq.nix
@@ -1,6 +1,21 @@
-# jq programs populate uses for flake and flake-input sources, in a file of
-# their own, so tests can run them.
+# Programs populate uses for flake and flake-input sources, in a file of
+# 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 the subdirectory containing the flake that nix
+ # flake metadata describes, like subdirOf does in jq.
+ metadataSubdir = /* nix */ ''
+ metadata: let
+ subdir = ref:
+ ref.dir or (let
+ match = builtins.match ".*[?&]dir=([^&]*).*" (ref.url or "");
+ in
+ if match == null then "" else builtins.head match);
+ subdirs = builtins.filter (dir: dir != "")
+ (map subdir [ (metadata.locked or {}) (metadata.original or {}) ]);
+ in
+ if subdirs == [] then "" else builtins.head subdirs
+ '';
# jq definitions for subdirectories containing flakes, which flake
# references and lock entries name either as attribute dir or as query
# parameter dir of their URL, depending on the version of Nix. fetchTree
diff --git a/tests/fixtures.sh b/tests/fixtures.sh
index 3839c1d..d9d8b2b 100755
--- a/tests/fixtures.sh
+++ b/tests/fixtures.sh
@@ -359,3 +359,105 @@ cat > /fixtures/patches/bad.patch <<'PATCH'
-nothing like this
+anything
PATCH
+
+# broken: a flake with an input that can't be fetched anymore.
+mkdir /fixtures/www/src/vanishing
+cat > /fixtures/www/src/vanishing/flake.nix <<'NIX'
+{ outputs = _: { who = "vanishing"; }; }
+NIX
+tarball vanishing
+mkdir /fixtures/broken
+cat > /fixtures/broken/flake.nix <<NIX
+{
+ inputs.vanishing.url = "$url/vanishing.tar.gz";
+ outputs = { vanishing, ... }: { msg = "broken:\${vanishing.who}"; };
+}
+NIX
+(cd /fixtures/broken && nix flake lock)
+
+# pinned, usepinned: a tarball flake whose own lock file pins an input that
+# can't be fetched anymore, and a flake that draws that input from its own
+# input instead, so nothing it evaluates needs the pin.
+mkdir /fixtures/www/src/pinned
+cat > /fixtures/www/src/pinned/flake.nix <<NIX
+{
+ inputs.inner.url = "$url/vanishing.tar.gz";
+ outputs = { inner, ... }: { greeting = "pinned+\${inner.who}"; };
+}
+NIX
+(cd /fixtures/www/src/pinned && nix flake lock)
+tarball pinned
+mkdir /fixtures/usepinned
+cat > /fixtures/usepinned/flake.nix <<NIX
+{
+ inputs.inner.url = "$url/inner.tar.gz";
+ inputs.dep.url = "$url/pinned.tar.gz";
+ inputs.dep.inputs.inner.follows = "inner";
+ outputs = { dep, ... }: { msg = "usepinned:\${dep.greeting}"; };
+}
+NIX
+(cd /fixtures/usepinned && nix flake lock)
+rm /fixtures/www/vanishing.tar.gz
+
+# stale, usestale: a tarball flake whose lock file lacks an input its
+# flake.nix has, and a flake with it as input, whose own lock file is
+# complete.
+mkdir /fixtures/www/src/stale
+cat > /fixtures/www/src/stale/flake.nix <<NIX
+{
+ inputs.inner.url = "$url/inner.tar.gz";
+ outputs = { inner, ... }: { greeting = "stale+\${inner.who}"; };
+}
+NIX
+(cd /fixtures/www/src/stale && nix flake lock)
+cat > /fixtures/www/src/stale/flake.nix <<NIX
+{
+ inputs.inner.url = "$url/inner.tar.gz";
+ inputs.extra.url = "$url/inner2.tar.gz";
+ outputs = { inner, extra, ... }: { greeting = "stale+\${inner.who}+\${extra.who}"; };
+}
+NIX
+tarball stale
+mkdir /fixtures/usestale
+cat > /fixtures/usestale/flake.nix <<NIX
+{
+ inputs.dep.url = "$url/stale.tar.gz";
+ outputs = { dep, ... }: { msg = "usestale:\${dep.greeting}"; };
+}
+NIX
+(cd /fixtures/usestale && nix flake lock)
+
+# offline: a flake with an input of its own, which the tests make
+# unreachable between deploys.
+mkdir /fixtures/www/src/gap
+cat > /fixtures/www/src/gap/flake.nix <<'NIX'
+{ outputs = _: { who = "gap"; }; }
+NIX
+tarball gap
+mkdir /fixtures/offline
+cat > /fixtures/offline/flake.nix <<NIX
+{
+ inputs.gap.url = "$url/gap.tar.gz";
+ outputs = { gap, ... }: { msg = "offline:\${gap.who}"; };
+}
+NIX
+(cd /fixtures/offline && nix flake lock)
+
+# c3: a flake with an input, to draw another flake's input from.
+mkdir /fixtures/c3
+cat > /fixtures/c3/flake.nix <<NIX
+{
+ inputs.z.url = "$url/inner.tar.gz";
+ outputs = { z, ... }: { who = "c3(\${z.who})"; };
+}
+NIX
+(cd /fixtures/c3 && nix flake lock)
+
+# filtered-parent, filtered-parent-link: like filtered, but the filter file
+# applies to a subdirectory, which is what gets synced.
+mkdir -p /fixtures/filtered-parent/sub
+printf -- '- garbage\n' > /fixtures/filtered-parent/.rsync-filter
+echo keep > /fixtures/filtered-parent/sub/keep
+echo garbage > /fixtures/filtered-parent/sub/garbage
+ln -s filtered-parent /fixtures/filtered-parent-link
+chown -h nobody /fixtures/filtered-parent-link
diff --git a/tests/scenarios.nix b/tests/scenarios.nix
index af072b9..7743482 100644
--- a/tests/scenarios.nix
+++ b/tests/scenarios.nix
@@ -1104,6 +1104,124 @@ assert !(krops.isLocalTarget (targets.remote "/"));
check = checkFlake { attr = "msg"; expected = "rel:lib"; };
};
})
+ # The target can't fetch an input of the flake. The deploying machine
+ # fetched it when locking, so its fetcher cache has to go.
+ (s "fetch-failure" {
+ prepareDeployer = ''
+ rm -fR /root/.cache/nix
+ '';
+ phases = [{
+ sources.x.flake = { url = "/fixtures/broken"; fetchOnTarget = true; };
+ expect.fail = "vanishing.tar.gz";
+ }];
+ })
+ # The delivered input itself is locked to a path the target lacks.
+ (s "gone-input" {
+ phases = [{
+ sources.x.flake-input = { url = "/fixtures/usegone"; name = "gone"; fetchOnTarget = true; };
+ expect.fail = "input gone refers to /fixtures/gone, which does not exist on the target";
+ }];
+ })
+ # An input that gets drawn from a source isn't needed on the target, with
+ # all it pins.
+ (modes "overridden-roots" ({ draw, mode, ... }: {
+ phases = twice {
+ sources = alt // {
+ x.flake-input = {
+ url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
+ } // draw { inner.follows = "alt"; };
+ };
+ check = checkFlake {
+ attr = "greeting"; expected = "dep+alt";
+ generated = mode == "generated";
+ } + roots 0;
+ };
+ }))
+ # The input's own lock file pins something unreachable, which gets drawn
+ # from a source anyway.
+ (modes "own-lock-unreachable" ({ draw, mode, ... }: {
+ prepareDeployer = ''
+ rm -fR /root/.cache/nix
+ '';
+ phases = twice {
+ sources = alt // {
+ x.flake-input = {
+ url = "/fixtures/usepinned"; name = "dep"; inherit fetchOnTarget;
+ } // draw { inner.follows = "alt"; };
+ };
+ check = checkFlake {
+ attr = "greeting"; expected = "pinned+alt";
+ generated = mode == "generated";
+ };
+ };
+ }))
+ # The input's own lock file pins something unreachable, or lacks an input
+ # its flake.nix has. That fails, unless the input isn't required to come
+ # with its inputs.
+ (s "own-lock-broken" (let
+ x = url: requireInputs: {
+ x.flake-input = { inherit url; name = "dep"; inherit fetchOnTarget requireInputs; };
+ };
+ delivered = ''
+ test -e "$T"/x/flake.nix || fail "input not delivered"
+ '';
+ in {
+ prepareDeployer = ''
+ rm -fR /root/.cache/nix
+ '';
+ phases = [
+ {
+ sources = x "/fixtures/usepinned" true;
+ expect.fail = "error: source x: the inputs its own lock file pins can't be delivered. Set its requireInputs to false to deliver it anyway.";
+ }
+ {
+ sources = x "/fixtures/usepinned" false;
+ check = delivered + roots 0;
+ }
+ {
+ sources = x "/fixtures/usestale" true;
+ expect.fail = "error: source x: the inputs its own lock file pins can't be delivered. Set its requireInputs to false to deliver it anyway.";
+ }
+ {
+ sources = x "/fixtures/usestale" false;
+ check = delivered + roots 2;
+ }
+ {
+ sources = x "/fixtures/usestale" false;
+ check = delivered + roots 2;
+ }
+ ];
+ }))
+ # Redeploying an unchanged input while where it comes from is
+ # unreachable and the fetcher caches are gone: the store has it.
+ (s "offline-redeploy" (let
+ sources.x.flake-input = { url = "/fixtures/offline"; name = "gap"; inherit fetchOnTarget; };
+ check = checkFlake { attr = "who"; expected = "gap"; };
+ in {
+ prepareDeployer = ''
+ if test -e /fixtures/www/gap.tar.gz.off; then
+ mv /fixtures/www/gap.tar.gz.off /fixtures/www/gap.tar.gz
+ fi
+ '';
+ phases = [
+ {
+ inherit sources check;
+ mutate = ''
+ rm -fR /root/.cache/nix
+ '';
+ mutateDeployer = ''
+ rm -fR /root/.cache/nix
+ mv /fixtures/www/gap.tar.gz /fixtures/www/gap.tar.gz.off
+ '';
+ }
+ {
+ inherit sources check;
+ mutateDeployer = ''
+ mv /fixtures/www/gap.tar.gz.off /fixtures/www/gap.tar.gz
+ '';
+ }
+ ];
+ }))
(s "flake-to-file" {
phases = [
{