From 5f73c6b416a152c681e4133d4cf3d181a6981eb5 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 3 Dec 2025 19:45:00 -0500 Subject: [PATCH] Make `nix path-info` follow the JSON guildelines --- doc/manual/rl-next/json-format-changes.md | 22 +++++++++++++++---- src/nix/path-info.cc | 19 +++++++++++++--- tests/functional/binary-cache.sh | 2 +- tests/functional/fixed.sh | 2 +- tests/functional/git-hashing/simple-common.sh | 2 +- tests/functional/impure-derivations.sh | 2 +- tests/functional/nix-profile.sh | 2 +- tests/functional/path-info.sh | 22 +++++++++++++------ tests/functional/signing.sh | 22 +++++++++---------- 9 files changed, 65 insertions(+), 30 deletions(-) diff --git a/doc/manual/rl-next/json-format-changes.md b/doc/manual/rl-next/json-format-changes.md index ce280a305..b5254d236 100644 --- a/doc/manual/rl-next/json-format-changes.md +++ b/doc/manual/rl-next/json-format-changes.md @@ -18,16 +18,30 @@ This is the legacy format, preserved for backwards compatibility: - String-based hash values (e.g., `"narHash": "sha256:FePFYIlM..."`) - String-based content addresses (e.g., `"ca": "fixed:r:sha256:1abc..."`) -- Full store paths in references (e.g., `"/nix/store/abc...-foo"`) +- Full store paths for map keys and references (e.g., `"/nix/store/abc...-foo"`) - Now includes `"storeDir"` field at the top level ### Version 2 (`--json-format 2`) -The new structured format with the following changes: +The new structured format follows the [JSON guidelines](@docroot@/development/json-guideline.md) with the following changes: -- **Store path base names in references**: +- **Nested structure with top-level metadata**: - References use store path base names (e.g., `"abc...-foo"`) instead of full paths. + The output is now wrapped in an object with `version`, `storeDir`, and `info` fields: + + ```json + { + "version": 2, + "storeDir": "/nix/store", + "info": { ... } + } + ``` + + The map from store bath base names to store object info is nested under the `info` field. + +- **Store path base names instead of full paths**: + + Map keys and references use store path base names (e.g., `"abc...-foo"`) instead of full absolute store paths. Combined with `storeDir`, the full path can be reconstructed. - **Structured `ca` field**: diff --git a/src/nix/path-info.cc b/src/nix/path-info.cc index d29055cc6..6bffe2424 100644 --- a/src/nix/path-info.cc +++ b/src/nix/path-info.cc @@ -41,10 +41,14 @@ pathInfoToJSON(Store & store, const StorePathSet & storePaths, bool showClosureS { json::object_t jsonAllObjects = json::object(); + auto makeKey = [&](const StorePath & path) { + return format == PathInfoJsonFormat::V1 ? store.printStorePath(path) : std::string(path.to_string()); + }; + for (auto & storePath : storePaths) { json jsonObject; - std::string key = store.printStorePath(storePath); + std::string key = makeKey(storePath); try { auto info = store.queryPathInfo(storePath); @@ -52,7 +56,7 @@ pathInfoToJSON(Store & store, const StorePathSet & storePaths, bool showClosureS // `storePath` has the representation `-x` rather than // `-` in case of binary-cache stores & `--all` because we don't // know the name yet until we've read the NAR info. - key = store.printStorePath(info->path); + key = makeKey(info->path); jsonObject = info->toJSON(format == PathInfoJsonFormat::V1 ? &store : nullptr, true, format); @@ -87,7 +91,16 @@ pathInfoToJSON(Store & store, const StorePathSet & storePaths, bool showClosureS jsonAllObjects[key] = std::move(jsonObject); } - return jsonAllObjects; + + if (format == PathInfoJsonFormat::V1) { + return jsonAllObjects; + } else { + return { + {"version", format}, + {"storeDir", store.storeDir}, + {"info", std::move(jsonAllObjects)}, + }; + } } struct CmdPathInfo : StorePathsCommand, MixJSON diff --git a/tests/functional/binary-cache.sh b/tests/functional/binary-cache.sh index 9f6a96906..445845bba 100755 --- a/tests/functional/binary-cache.sh +++ b/tests/functional/binary-cache.sh @@ -18,7 +18,7 @@ outPath=$(nix-build dependencies.nix --no-out-link) nix copy --to "file://$cacheDir" "$outPath" -readarray -t paths < <(nix path-info --all --json --json-format 2 --store "file://$cacheDir" | jq 'keys|sort|.[]' -r) +readarray -t paths < <(nix path-info --all --json --json-format 2 --store "file://$cacheDir" | jq '.info|keys|sort|.[]' -r) [[ "${#paths[@]}" -eq 3 ]] for path in "${paths[@]}"; do [[ "$path" =~ -dependencies-input-0$ ]] \ diff --git a/tests/functional/fixed.sh b/tests/functional/fixed.sh index bf60fb66b..3d37f5fbb 100755 --- a/tests/functional/fixed.sh +++ b/tests/functional/fixed.sh @@ -15,7 +15,7 @@ nix-build fixed.nix -A bad --no-out-link && fail "should fail" # a side-effect. [[ -e $path ]] nix path-info --json --json-format 2 "$path" | jq -e \ - '.[].ca == { + '.info.[].ca == { method: "flat", hash: { algorithm: "md5", diff --git a/tests/functional/git-hashing/simple-common.sh b/tests/functional/git-hashing/simple-common.sh index 265775109..fa523af61 100644 --- a/tests/functional/git-hashing/simple-common.sh +++ b/tests/functional/git-hashing/simple-common.sh @@ -50,7 +50,7 @@ try2 () { nix path-info --json --json-format 2 "$path" | jq -e \ --arg algo "$hashAlgo" \ --arg hash "$hashFromGit" \ - '.[].ca == { + '.info.[].ca == { method: "git", hash: { algorithm: $algo, diff --git a/tests/functional/impure-derivations.sh b/tests/functional/impure-derivations.sh index d23f32c8d..3f78036c2 100755 --- a/tests/functional/impure-derivations.sh +++ b/tests/functional/impure-derivations.sh @@ -30,7 +30,7 @@ path1_stuff=$(echo "$json" | jq -r .[].outputs.stuff) [[ $(< "$path1"/n) = 0 ]] [[ $(< "$path1_stuff"/bla) = 0 ]] -nix path-info --json --json-format 2 "$path1" | jq -e '.[].ca | .method == "nar" and .hash.algorithm == "sha256"' +nix path-info --json --json-format 2 "$path1" | jq -e '.info.[].ca | .method == "nar" and .hash.algorithm == "sha256"' path2=$(nix build -L --no-link --json --file ./impure-derivations.nix impure | jq -r .[].outputs.out) [[ $(< "$path2"/n) = 1 ]] diff --git a/tests/functional/nix-profile.sh b/tests/functional/nix-profile.sh index 6e4f11f6a..8934c27fc 100755 --- a/tests/functional/nix-profile.sh +++ b/tests/functional/nix-profile.sh @@ -166,7 +166,7 @@ printf 4.0 > "$flake1Dir"/version printf Utrecht > "$flake1Dir"/who nix profile add "$flake1Dir" [[ $("$TEST_HOME"/.nix-profile/bin/hello) = "Hello Utrecht" ]] -nix path-info --json --json-format 2 "$(realpath "$TEST_HOME"/.nix-profile/bin/hello)" | jq -e '.[].ca | .method == "nar" and .hash.algorithm == "sha256"' +nix path-info --json --json-format 2 "$(realpath "$TEST_HOME"/.nix-profile/bin/hello)" | jq -e '.info.[].ca | .method == "nar" and .hash.algorithm == "sha256"' # Override the outputs. nix profile remove simple flake1 diff --git a/tests/functional/path-info.sh b/tests/functional/path-info.sh index 31ffe0caf..de1f45151 100755 --- a/tests/functional/path-info.sh +++ b/tests/functional/path-info.sh @@ -4,36 +4,44 @@ source common.sh echo foo > "$TEST_ROOT"/foo foo=$(nix store add-file "$TEST_ROOT"/foo) +fooBase=$(basename "$foo") echo bar > "$TEST_ROOT"/bar bar=$(nix store add-file "$TEST_ROOT"/bar) +barBase=$(basename "$bar") echo baz > "$TEST_ROOT"/baz baz=$(nix store add-file "$TEST_ROOT"/baz) +bazBase=$(basename "$baz") nix-store --delete "$baz" diff --unified --color=always \ <(nix path-info --json --json-format 2 "$foo" "$bar" "$baz" | - jq --sort-keys 'map_values(.narHash)') \ + jq --sort-keys '.info | map_values(.narHash)') \ <(jq --sort-keys <<-EOF { - "$foo": { + "$fooBase": { "algorithm": "sha256", "format": "base16", "hash": "42fb4031b525feebe2f8b08e6e6a8e86f34e6a91dd036ada888e311b9cc8e690" }, - "$bar": { + "$barBase": { "algorithm": "sha256", "format": "base16", "hash": "f5f8581aef5fab17100b629cf35aa1d91328d5070b054068f14fa93e7fa3b614" }, - "$baz": null + "$bazBase": null } EOF ) -# Test that storeDir is returned in the JSON output +# Test that storeDir is returned in the JSON output in individual store objects nix path-info --json --json-format 2 "$foo" | jq -e \ - --arg foo "$foo" \ + --arg fooBase "$fooBase" \ --arg storeDir "${NIX_STORE_DIR:-/nix/store}" \ - '.[$foo].storeDir == $storeDir' + '.info[$fooBase].storeDir == $storeDir' + +# And also at the top -evel +echo | nix path-info --json --json-format 2 --stdin | jq -e \ + --arg storeDir "${NIX_STORE_DIR:-/nix/store}" \ + '.storeDir == $storeDir' diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index 180afed82..5a3bdc81d 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -15,10 +15,10 @@ outPath=$(nix-build dependencies.nix --no-out-link --secret-key-files "$TEST_ROO # Verify that the path got signed. info=$(nix path-info --json --json-format 2 "$outPath") -echo "$info" | jq -e '.[] | .ultimate == true' +echo "$info" | jq -e '.info.[] | .ultimate == true' TODO_NixOS # looks like an actual bug? Following line fails on NixOS: -echo "$info" | jq -e '.[] | .signatures.[] | select(startswith("cache1.example.org"))' -echo "$info" | jq -e '.[] | .signatures.[] | select(startswith("cache2.example.org"))' +echo "$info" | jq -e '.info.[] | .signatures.[] | select(startswith("cache1.example.org"))' +echo "$info" | jq -e '.info.[] | .signatures.[] | select(startswith("cache2.example.org"))' # Test "nix store verify". nix store verify -r "$outPath" @@ -40,8 +40,8 @@ nix store verify -r "$outPath" # Verify that the path did not get signed but does have the ultimate bit. info=$(nix path-info --json --json-format 2 "$outPath2") -echo "$info" | jq -e '.[] | .ultimate == true' -echo "$info" | jq -e '.[] | .signatures == []' +echo "$info" | jq -e '.info.[] | .ultimate == true' +echo "$info" | jq -e '.info.[] | .signatures == []' # Test "nix store verify". nix store verify -r "$outPath2" @@ -58,7 +58,7 @@ nix store verify -r "$outPath2" --sigs-needed 1 --trusted-public-keys "$pk1" # Build something content-addressed. outPathCA=$(IMPURE_VAR1=foo IMPURE_VAR2=bar nix-build ./fixed.nix -A good.0 --no-out-link) -nix path-info --json --json-format 2 "$outPathCA" | jq -e '.[].ca | .method == "flat" and .hash.algorithm == "md5"' +nix path-info --json --json-format 2 "$outPathCA" | jq -e '.info.[].ca | .method == "flat" and .hash.algorithm == "md5"' # Content-addressed paths don't need signatures, so they verify # regardless of --sigs-needed. @@ -74,15 +74,15 @@ nix copy --to file://"$cacheDir" "$outPath2" # Verify that signatures got copied. info=$(nix path-info --store file://"$cacheDir" --json --json-format 2 "$outPath2") -echo "$info" | jq -e '.[] | .ultimate == false' -echo "$info" | jq -e '.[] | .signatures.[] | select(startswith("cache1.example.org"))' -echo "$info" | expect 4 jq -e '.[] | .signatures.[] | select(startswith("cache2.example.org"))' +echo "$info" | jq -e '.info.[] | .ultimate == false' +echo "$info" | jq -e '.info.[] | .signatures.[] | select(startswith("cache1.example.org"))' +echo "$info" | expect 4 jq -e '.info.[] | .signatures.[] | select(startswith("cache2.example.org"))' # Verify that adding a signature to a path in a binary cache works. nix store sign --store file://"$cacheDir" --key-file "$TEST_ROOT"/sk2 "$outPath2" info=$(nix path-info --store file://"$cacheDir" --json --json-format 2 "$outPath2") -echo "$info" | jq -e '.[] | .signatures.[] | select(startswith("cache1.example.org"))' -echo "$info" | jq -e '.[] | .signatures.[] | select(startswith("cache2.example.org"))' +echo "$info" | jq -e '.info.[] | .signatures.[] | select(startswith("cache1.example.org"))' +echo "$info" | jq -e '.info.[] | .signatures.[] | select(startswith("cache2.example.org"))' # Copying to a diverted store should fail due to a lack of signatures by trusted keys. chmod -R u+w "$TEST_ROOT"/store0 || true