From 0a5eef6bc2f150ecfc8712f63cf14d3a9505d845 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 9 Feb 2026 20:26:21 +0100 Subject: [PATCH] Add subcommand 'nix provenance show' --- .../include/nix/fetchers/provenance.hh | 5 + src/libfetchers/provenance.cc | 7 + src/libflake/provenance.cc | 9 ++ src/libstore/provenance.cc | 9 ++ src/libutil/provenance.cc | 8 ++ src/nix/meson.build | 1 + src/nix/provenance-show.md | 28 ++++ src/nix/provenance.cc | 124 ++++++++++++++++++ tests/functional/flakes/provenance.sh | 9 ++ 9 files changed, 200 insertions(+) create mode 100644 src/nix/provenance-show.md create mode 100644 src/nix/provenance.cc diff --git a/src/libfetchers/include/nix/fetchers/provenance.hh b/src/libfetchers/include/nix/fetchers/provenance.hh index bb7847112acc..3fed314cedcb 100644 --- a/src/libfetchers/include/nix/fetchers/provenance.hh +++ b/src/libfetchers/include/nix/fetchers/provenance.hh @@ -11,6 +11,11 @@ struct TreeProvenance : Provenance TreeProvenance(const fetchers::Input & input); + TreeProvenance(ref attrs) + : attrs(std::move(attrs)) + { + } + nlohmann::json to_json() const override; }; diff --git a/src/libfetchers/provenance.cc b/src/libfetchers/provenance.cc index bfbb9133fe8f..5ea6c6213bc3 100644 --- a/src/libfetchers/provenance.cc +++ b/src/libfetchers/provenance.cc @@ -1,5 +1,6 @@ #include "nix/fetchers/provenance.hh" #include "nix/fetchers/attrs.hh" +#include "nix/util/json-utils.hh" #include @@ -24,4 +25,10 @@ nlohmann::json TreeProvenance::to_json() const }; } +Provenance::Register registerTreeProvenance("tree", [](nlohmann::json json) { + auto & obj = getObject(json); + auto & attrsJson = valueAt(obj, "attrs"); + return make_ref(make_ref(attrsJson)); +}); + } // namespace nix diff --git a/src/libflake/provenance.cc b/src/libflake/provenance.cc index cd3441a7a909..865eddea7695 100644 --- a/src/libflake/provenance.cc +++ b/src/libflake/provenance.cc @@ -1,4 +1,5 @@ #include "nix/flake/provenance.hh" +#include "nix/util/json-utils.hh" #include @@ -13,4 +14,12 @@ nlohmann::json FlakeProvenance::to_json() const }; } +Provenance::Register registerFlakeProvenance("flake", [](nlohmann::json json) { + auto & obj = getObject(json); + std::shared_ptr next; + if (auto p = optionalValueAt(obj, "next"); p && !p->is_null()) + next = Provenance::from_json(*p); + return make_ref(next, getString(valueAt(obj, "flakeOutput"))); +}); + } // namespace nix diff --git a/src/libstore/provenance.cc b/src/libstore/provenance.cc index 90d9a17b6a55..914a55e16681 100644 --- a/src/libstore/provenance.cc +++ b/src/libstore/provenance.cc @@ -13,6 +13,15 @@ nlohmann::json BuildProvenance::to_json() const }; } +Provenance::Register registerBuildProvenance("build", [](nlohmann::json json) { + auto & obj = getObject(json); + std::shared_ptr next; + if (auto p = optionalValueAt(obj, "next"); p && !p->is_null()) + next = Provenance::from_json(*p); + return make_ref( + StorePath(getString(valueAt(obj, "drv"))), getString(valueAt(obj, "output")), next); +}); + nlohmann::json CopiedProvenance::to_json() const { return { diff --git a/src/libutil/provenance.cc b/src/libutil/provenance.cc index f9c7ba651059..3130e148f540 100644 --- a/src/libutil/provenance.cc +++ b/src/libutil/provenance.cc @@ -63,4 +63,12 @@ nlohmann::json SubpathProvenance::to_json() const }; } +Provenance::Register registerSubpathProvenance("subpath", [](nlohmann::json json) { + auto & obj = getObject(json); + std::shared_ptr next; + if (auto p = optionalValueAt(obj, "next"); p && !p->is_null()) + next = Provenance::from_json(*p); + return make_ref(next, CanonPath(getString(valueAt(obj, "subpath")))); +}); + } // namespace nix diff --git a/src/nix/meson.build b/src/nix/meson.build index 77e3c05dc5eb..3b343614e421 100644 --- a/src/nix/meson.build +++ b/src/nix/meson.build @@ -94,6 +94,7 @@ nix_sources = [ config_priv_h ] + files( 'path-info.cc', 'prefetch.cc', 'profile.cc', + 'provenance.cc', 'ps.cc', 'realisation.cc', 'registry.cc', diff --git a/src/nix/provenance-show.md b/src/nix/provenance-show.md new file mode 100644 index 000000000000..565ae05bf0b7 --- /dev/null +++ b/src/nix/provenance-show.md @@ -0,0 +1,28 @@ +R""( + +# Examples + +* Show the provenance of a store path: + + ```console + # nix provenance show /run/current-system + /nix/store/k145bdxhdb89i4fkvgdisdz1yh2wiymm-nixos-system-machine-25.05.20251210.d2b1213 + ← copied from cache.flakehub.com + ← built from derivation /nix/store/w3p3xkminq61hs00kihd34w1dglpj5s9-nixos-system-machine-25.05.20251210.d2b1213.drv (output out) + ← instantiated from flake output github:my-org/my-repo/6b03eb949597fe96d536e956a2c14da9901dbd21?dir=machine#nixosConfigurations.machine.config.system.build.toplevel + ``` + +# Description + +Show the provenance chain of one or more store paths. For each store path, this displays where it came from: what binary cache it was copied from, what flake it was built from, and so on. + +The provenance chain shows the history of how the store path came to exist, including: + +- **Copied**: The path was copied from another Nix store, typically a binary cache. +- **Built**: The path was built from a derivation. +- **Flake evaluation**: The derivation was instantiated during the evaluation of a flake output. +- **Fetched**: The path was obtained by fetching a source tree. + +Note: if you want provenance in JSON format, use the `provenance` field returned by `nix path-info --json`. + +)"" diff --git a/src/nix/provenance.cc b/src/nix/provenance.cc new file mode 100644 index 000000000000..e32e2fe7935c --- /dev/null +++ b/src/nix/provenance.cc @@ -0,0 +1,124 @@ +#include "nix/cmd/command.hh" +#include "nix/store/store-api.hh" +#include "nix/store/provenance.hh" +#include "nix/flake/provenance.hh" +#include "nix/fetchers/provenance.hh" +#include "nix/util/provenance.hh" + +#include +#include + +using namespace nix; + +struct CmdProvenance : NixMultiCommand +{ + CmdProvenance() + : NixMultiCommand("provenance", RegisterCommand::getCommandsFor({"provenance"})) + { + } + + std::string description() override + { + return "query and check the provenance of store paths"; + } + + std::optional experimentalFeature() override + { + return Xp::Provenance; + } + + Category category() override + { + return catUtility; + } +}; + +static auto rCmdProvenance = registerCommand("provenance"); + +struct CmdProvenanceShow : StorePathsCommand +{ + std::string description() override + { + return "show the provenance chain of store paths"; + } + + std::string doc() override + { + return +#include "provenance-show.md" + ; + } + + void displayProvenance(Store & store, const StorePath & path, std::shared_ptr provenance) + { + while (provenance) { + if (auto copied = std::dynamic_pointer_cast(provenance)) { + logger->cout("← copied from " ANSI_BOLD "%s" ANSI_NORMAL, copied->from); + provenance = copied->next; + } else if (auto build = std::dynamic_pointer_cast(provenance)) { + logger->cout( + "← built from derivation " ANSI_BOLD "%s" ANSI_NORMAL " (output " ANSI_BOLD "%s" ANSI_NORMAL ")", + store.printStorePath(build->drvPath), + build->output); + provenance = build->next; + } else if (auto flake = std::dynamic_pointer_cast(provenance)) { + // Collapse subpath/tree provenance into the flake provenance for legibility. + auto next = flake->next; + CanonPath flakePath("/flake.nix"); + if (auto subpath = std::dynamic_pointer_cast(next)) { + next = subpath->next; + flakePath = subpath->subpath; + } + if (auto tree = std::dynamic_pointer_cast(next)) { + FlakeRef flakeRef( + fetchers::Input::fromAttrs(fetchSettings, fetchers::jsonToAttrs(*tree->attrs)), + Path(flakePath.parent().value_or(CanonPath::root).rel())); + logger->cout( + "← instantiated from flake output " ANSI_BOLD "%s#%s" ANSI_NORMAL, + flakeRef.to_string(), + flake->flakeOutput); + break; + } else { + logger->cout("← instantiated from flake output " ANSI_BOLD "%s" ANSI_NORMAL, flake->flakeOutput); + provenance = flake->next; + } + } else if (auto tree = std::dynamic_pointer_cast(provenance)) { + auto input = fetchers::Input::fromAttrs(fetchSettings, fetchers::jsonToAttrs(*tree->attrs)); + logger->cout("← from tree " ANSI_BOLD "%s" ANSI_NORMAL, input.to_string()); + break; + } else if (auto subpath = std::dynamic_pointer_cast(provenance)) { + logger->cout("← from file " ANSI_BOLD "%s" ANSI_NORMAL, subpath->subpath.abs()); + provenance = subpath->next; + } else { + // Unknown or unhandled provenance type + auto json = provenance->to_json(); + auto typeIt = json.find("type"); + if (typeIt != json.end() && typeIt->is_string()) + logger->cout("← " ANSI_RED "unknown provenance type '%s'" ANSI_NORMAL, typeIt->get()); + else + logger->cout("← " ANSI_RED "unknown provenance type" ANSI_NORMAL); + break; + } + } + } + + void run(ref store, StorePaths && storePaths) override + { + bool first = true; + + for (auto & storePath : storePaths) { + auto info = store->queryPathInfo(storePath); + if (!first) + logger->cout(""); + first = false; + logger->cout(ANSI_BOLD "%s" ANSI_NORMAL, store->printStorePath(info->path)); + + if (info->provenance) + displayProvenance(*store, storePath, info->provenance); + else + logger->cout(ANSI_RED " (no provenance information available)" ANSI_NORMAL); + } + } +}; + +static auto rCmdProvenanceShow = registerCommand2({"provenance", "show"}); diff --git a/tests/functional/flakes/provenance.sh b/tests/functional/flakes/provenance.sh index 06dc4675b869..4f831b91ffe2 100644 --- a/tests/functional/flakes/provenance.sh +++ b/tests/functional/flakes/provenance.sh @@ -120,6 +120,15 @@ nix copy --from "file://$binaryCache" "$outPath" --no-check-sigs EOF ) ]] +# Test `nix provenance show`. +[[ $(nix provenance show "$outPath") = $(cat <