aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--pkgs/populate/default.nix67
-rw-r--r--pkgs/populate/flake-jq.nix70
-rw-r--r--tests/default.nix13
-rwxr-xr-xtests/jq.sh122
5 files changed, 206 insertions, 67 deletions
diff --git a/README.md b/README.md
index 713bfad..e70da72 100644
--- a/README.md
+++ b/README.md
@@ -592,6 +592,7 @@ The tests require KVM and nixpkgs with `pkgs.testers.runNixOSTest`
```sh
nix-build tests # all tests
nix-build tests -A flake # the test for one source type
+nix-build tests -A jq # populate's jq programs for flakes, without VMs
```
diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix
index bd7e5e4..f3cf925 100644
--- a/pkgs/populate/default.nix
+++ b/pkgs/populate/default.nix
@@ -259,6 +259,7 @@ 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;
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;
@@ -291,72 +292,6 @@ let
${quote source.flake} ${quote (toString source.name)}
exit 1
'';
- # 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
- # supports neither.
- subdirs = /* jq */ ''
- def subdir:
- .dir // ((.url // "") | capture("[?&]dir=(?<dir>[^&]*)").dir) // "";
- def withoutSubdir:
- del(.__final, .dir)
- | if (.url | type) == "string" then
- .url |= (sub("(?<sep>[?&])dir=[^&]*&?"; "\(.sep)") | sub("[?&]$"; ""))
- else . end;
- def subdirOf($node):
- [$node.locked, $node.original]
- | map(select(. != null) | subdir | select(. != ""))
- | first // "";
- '';
- # jq filter selecting the flake or input in nix flake metadata's output,
- # by the path of input names $input, as what to fetch and the
- # subdirectory containing it: {self, locked, subdir}. Names follow the
- # flake's lock file, follows included; an empty follows path removes an
- # input. The flake itself, and inputs given by paths relative to it, have
- # self set instead of fetchTree arguments in locked.
- selected = /* jq */ ''
- ${subdirs}
- . as $m
- | .locks as $l
- | def ref($path; $depth):
- if $depth > 64 then error("follows nest too deep") else
- reduce $path[] as $name ($l.root;
- (($l.nodes[.].inputs // {})[$name]) as $next
- | if $next == null or $next == [] then error("no input \($name)")
- elif ($next | type) == "array" then ref($next; $depth + 1)
- else $next end)
- end;
- def join($a; $b):
- if $a == "" then $b elif $b == "" then $a else "\($a)/\($b)" end;
- def resolve($key; $depth):
- $l.nodes[$key] as $node
- | if $depth > 64 then error("parents nest too deep")
- elif $key == $l.root then
- { self: true, locked: null, subdir: subdirOf($m) }
- elif $node.parent != null then
- resolve(ref($node.parent; 0); $depth + 1) as $base
- | $base + {
- subdir: join(join($base.subdir;
- $node.locked.path | ltrimstr("./") | rtrimstr("/") | if . == "." then "" else . end);
- subdirOf($node))
- }
- else
- { self: false, locked: ($node.locked | withoutSubdir), subdir: subdirOf($node) }
- end;
- resolve(ref($input; 0); 0)
- '';
- # jq filter telling whether the target can fetch the flake itself, i.e.
- # whether its locked reference doesn't refer to the deploying machine.
- selfIsRemote = /* jq */ ''
- .locked | (.type != "path") and ((.url // "") | startswith("file:") | not)
- '';
- # jq filter replacing self in the selection $selected with the flake's
- # locked reference.
- selfLocked = /* jq */ ''
- ${subdirs}
- .locked as $self
- | $selected + { self: false, locked: ($self | withoutSubdir) }
- '';
# 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
diff --git a/pkgs/populate/flake-jq.nix b/pkgs/populate/flake-jq.nix
new file mode 100644
index 0000000..41edcc5
--- /dev/null
+++ b/pkgs/populate/flake-jq.nix
@@ -0,0 +1,70 @@
+# jq programs populate uses for flake and flake-input sources, in a file of
+# their own, so tests can run them.
+rec {
+ # 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
+ # supports neither.
+ subdirs = /* jq */ ''
+ def subdir:
+ .dir // ((.url // "") | capture("[?&]dir=(?<dir>[^&]*)").dir) // "";
+ def withoutSubdir:
+ del(.__final, .dir)
+ | if (.url | type) == "string" then
+ .url |= (sub("(?<sep>[?&])dir=[^&]*&?"; "\(.sep)") | sub("[?&]$"; ""))
+ else . end;
+ def subdirOf($node):
+ [$node.locked, $node.original]
+ | map(select(. != null) | subdir | select(. != ""))
+ | first // "";
+ '';
+ # jq filter selecting the flake or input in nix flake metadata's output,
+ # by the path of input names $input, as what to fetch and the
+ # subdirectory containing it: {self, locked, subdir}. Names follow the
+ # flake's lock file, follows included; an empty follows path removes an
+ # input. The flake itself, and inputs given by paths relative to it, have
+ # self set instead of fetchTree arguments in locked.
+ selected = /* jq */ ''
+ ${subdirs}
+ . as $m
+ | .locks as $l
+ | def ref($path; $depth):
+ if $depth > 64 then error("follows nest too deep") else
+ reduce $path[] as $name ($l.root;
+ (($l.nodes[.].inputs // {})[$name]) as $next
+ | if $next == null or $next == [] then error("no input \($name)")
+ elif ($next | type) == "array" then ref($next; $depth + 1)
+ else $next end)
+ end;
+ def join($a; $b):
+ if $a == "" then $b elif $b == "" then $a else "\($a)/\($b)" end;
+ def resolve($key; $depth):
+ $l.nodes[$key] as $node
+ | if $depth > 64 then error("parents nest too deep")
+ elif $key == $l.root then
+ { self: true, locked: null, subdir: subdirOf($m) }
+ elif $node.parent != null then
+ resolve(ref($node.parent; 0); $depth + 1) as $base
+ | $base + {
+ subdir: join(join($base.subdir;
+ $node.locked.path | ltrimstr("./") | rtrimstr("/") | if . == "." then "" else . end);
+ subdirOf($node))
+ }
+ else
+ { self: false, locked: ($node.locked | withoutSubdir), subdir: subdirOf($node) }
+ end;
+ resolve(ref($input; 0); 0)
+ '';
+ # jq filter telling whether the target can fetch the flake itself, i.e.
+ # whether its locked reference doesn't refer to the deploying machine.
+ selfIsRemote = /* jq */ ''
+ .locked | (.type != "path") and ((.url // "") | startswith("file:") | not)
+ '';
+ # jq filter replacing self in the selection $selected with the flake's
+ # locked reference.
+ selfLocked = /* jq */ ''
+ ${subdirs}
+ .locked as $self
+ | $selected + { self: false, locked: ($self | withoutSubdir) }
+ '';
+}
diff --git a/tests/default.nix b/tests/default.nix
index ca409b3..a6b9356 100644
--- a/tests/default.nix
+++ b/tests/default.nix
@@ -1,7 +1,8 @@
# NixOS VM tests for populate: one test per source type, one for features
# spanning sources, one for edge cases of flakes, and one for writeDeploy's
# arguments to nixos-rebuild. Build all with nix-build tests, or one with,
-# e.g., nix-build tests -A flake.
+# e.g., nix-build tests -A flake. jq runs populate's jq programs for flakes
+# without VMs.
{ pkgs ? import ../pkgs {} }:
let
@@ -21,7 +22,17 @@ let
agePath = "${pkgs.age}/bin/age";
inherit gitHead gitFirst;
};
+ flakeJq = import ../pkgs/populate/flake-jq.nix;
in
builtins.mapAttrs
(name: scenarios: import ./vm.nix { inherit pkgs name scenarios gitHead; })
scenarios
+ // {
+ # Lock files in the shapes different versions of Nix write.
+ jq = pkgs.runCommand "krops-jq" { nativeBuildInputs = [ pkgs.jq ]; } ''
+ sh ${./jq.sh} ${pkgs.linkFarm "krops-flake-jq" (map
+ (name: { name = "${name}.jq"; path = pkgs.writeText "${name}.jq" flakeJq.${name}; })
+ [ "selected" "selfIsRemote" "selfLocked" ])}
+ touch $out
+ '';
+ }
diff --git a/tests/jq.sh b/tests/jq.sh
new file mode 100755
index 0000000..a62d6cb
--- /dev/null
+++ b/tests/jq.sh
@@ -0,0 +1,122 @@
+#! /bin/sh
+# Runs populate's jq programs for flakes over lock files in the shapes
+# different versions of Nix write, without VMs.
+#
+# usage: jq.sh DIR
+#
+# DIR contains the programs as selected.jq, selfIsRemote.jq, and
+# selfLocked.jq.
+set -efu
+
+programs=$1
+failed=0
+
+# usage: expect WHAT ACTUAL EXPECTED
+expect() {
+ if test "$2" != "$3"; then
+ printf 'FAIL: %s\n expected: %s\n got: %s\n' "$1" "$3" "$2" >&2
+ failed=1
+ fi
+}
+
+# usage: expect_error WHAT ACTUAL MESSAGE
+expect_error() {
+ case $2 in
+ *"$3"*) ;;
+ *)
+ printf 'FAIL: %s\n expected an error: %s\n got: %s\n' "$1" "$3" "$2" >&2
+ failed=1
+ ;;
+ esac
+}
+
+# usage: selection METADATA INPUT
+selection() {
+ printf '%s\n' "$1" |
+ timeout 10 jq -c --argjson input "$2" --from-file "$programs"/selected.jq 2>&1 ||
+ :
+}
+
+# A flake with an input dep, which has an input inner following the flake's
+# input inner, and an input sub, a flake in a subdirectory, written as
+# attribute dir or as query parameter dir of the URL.
+metadata() {
+ cat <<JSON
+{
+ "locked": { "type": "path", "path": "/src/top", "narHash": "sha256-top" },
+ "path": "/nix/store/top-source",
+ "locks": {
+ "root": "root",
+ "version": 7,
+ "nodes": {
+ "root": { "inputs": { "dep": "dep", "inner": "inner", "sub": "sub" } },
+ "dep": {
+ "inputs": { "inner": [ "inner" ] },
+ "locked": { "type": "tarball", "url": "http://x/dep.tar.gz", "narHash": "sha256-dep" }
+ },
+ "inner": {
+ "locked": { "type": "tarball", "url": "http://x/inner.tar.gz", "narHash": "sha256-inner" }
+ },
+ "sub": {
+ "locked": $1,
+ "original": $2
+ }
+ }
+ }
+}
+JSON
+}
+attribute=$(metadata \
+ '{ "type": "tarball", "url": "http://x/mono.tar.gz", "dir": "sub", "narHash": "sha256-mono" }' \
+ '{ "type": "tarball", "url": "http://x/mono.tar.gz", "dir": "sub" }')
+url=$(metadata \
+ '{ "type": "tarball", "url": "http://x/mono.tar.gz?dir=sub", "narHash": "sha256-mono" }' \
+ '{ "type": "tarball", "url": "http://x/mono.tar.gz?dir=sub" }')
+
+mono='{"self":false,"locked":{"type":"tarball","url":"http://x/mono.tar.gz","narHash":"sha256-mono"},"subdir":"sub"}'
+expect "subdirectory as attribute" "$(selection "$attribute" '["sub"]')" "$mono"
+expect "subdirectory in the URL" "$(selection "$url" '["sub"]')" "$mono"
+expect "the flake itself" "$(selection "$attribute" '[]')" \
+ '{"self":true,"locked":null,"subdir":""}'
+expect "name through follows" "$(selection "$attribute" '["dep","inner"]')" \
+ '{"self":false,"locked":{"type":"tarball","url":"http://x/inner.tar.gz","narHash":"sha256-inner"},"subdir":""}'
+expect_error "missing input" "$(selection "$attribute" '["nope"]')" "no input nope"
+
+# An empty follows path removes an input.
+removed=$(printf '%s\n' "$attribute" | jq -c '.locks.nodes.dep.inputs.inner = []')
+expect_error "removed input" "$(selection "$removed" '["dep","inner"]')" "no input inner"
+
+# Inputs following each other.
+cyclic=$(printf '%s\n' "$attribute" | jq -c '
+ .locks.nodes.dep.inputs.inner = ["inner", "x"]
+ | .locks.nodes.inner.inputs = { x: ["dep", "inner"] }
+')
+expect_error "cyclic follows" "$(selection "$cyclic" '["dep","inner"]')" "follows nest too deep"
+
+# An input given by a path relative to the flake, which is in subdirectory
+# sub itself.
+relative=$(printf '%s\n' "$attribute" | jq -c '
+ .locked.dir = "sub"
+ | .locks.nodes.root.inputs.lib = "lib"
+ | .locks.nodes.lib = { locked: { type: "path", path: "./lib" }, parent: [] }
+')
+expect "relative path" "$(selection "$relative" '["lib"]')" \
+ '{"self":true,"locked":null,"subdir":"sub/lib"}'
+
+# usage: remote LOCKED
+remote() {
+ printf '{ "locked": %s }\n' "$1" |
+ jq --from-file "$programs"/selfIsRemote.jq
+}
+expect "path" "$(remote '{ "type": "path", "path": "/src" }')" false
+expect "local Git" "$(remote '{ "type": "git", "url": "file:///src" }')" false
+expect "remote Git" "$(remote '{ "type": "git", "url": "https://x/y" }')" true
+expect "GitHub" "$(remote '{ "type": "github", "owner": "o", "repo": "r" }')" true
+
+expect "locked reference of the flake itself" "$(
+ printf '%s\n' '{ "locked": { "__final": true, "type": "tarball", "url": "http://x/mono.tar.gz?dir=sub", "narHash": "sha256-mono" } }' |
+ jq -c --argjson selected '{"self":true,"locked":null,"subdir":"sub"}' \
+ --from-file "$programs"/selfLocked.jq
+)" "$mono"
+
+exit "$failed"