Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/libfetchers/include/nix/fetchers/provenance.hh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ struct TreeProvenance : Provenance

TreeProvenance(const fetchers::Input & input);

TreeProvenance(ref<nlohmann::json> attrs)
: attrs(std::move(attrs))
{
}

nlohmann::json to_json() const override;
};

Expand Down
7 changes: 7 additions & 0 deletions src/libfetchers/provenance.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "nix/fetchers/provenance.hh"
#include "nix/fetchers/attrs.hh"
#include "nix/util/json-utils.hh"

#include <nlohmann/json.hpp>

Expand All @@ -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<TreeProvenance>(make_ref<nlohmann::json>(attrsJson));
});

} // namespace nix
9 changes: 9 additions & 0 deletions src/libflake/provenance.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "nix/flake/provenance.hh"
#include "nix/util/json-utils.hh"

#include <nlohmann/json.hpp>

Expand All @@ -13,4 +14,12 @@ nlohmann::json FlakeProvenance::to_json() const
};
}

Provenance::Register registerFlakeProvenance("flake", [](nlohmann::json json) {
auto & obj = getObject(json);
std::shared_ptr<const Provenance> next;
if (auto p = optionalValueAt(obj, "next"); p && !p->is_null())
next = Provenance::from_json(*p);
return make_ref<FlakeProvenance>(next, getString(valueAt(obj, "flakeOutput")));
});

} // namespace nix
9 changes: 9 additions & 0 deletions src/libstore/provenance.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ nlohmann::json BuildProvenance::to_json() const
};
}

Provenance::Register registerBuildProvenance("build", [](nlohmann::json json) {
auto & obj = getObject(json);
std::shared_ptr<const Provenance> next;
if (auto p = optionalValueAt(obj, "next"); p && !p->is_null())
next = Provenance::from_json(*p);
return make_ref<BuildProvenance>(
StorePath(getString(valueAt(obj, "drv"))), getString(valueAt(obj, "output")), next);
});

nlohmann::json CopiedProvenance::to_json() const
{
return {
Expand Down
8 changes: 8 additions & 0 deletions src/libutil/provenance.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,12 @@ nlohmann::json SubpathProvenance::to_json() const
};
}

Provenance::Register registerSubpathProvenance("subpath", [](nlohmann::json json) {
auto & obj = getObject(json);
std::shared_ptr<const Provenance> next;
if (auto p = optionalValueAt(obj, "next"); p && !p->is_null())
next = Provenance::from_json(*p);
return make_ref<SubpathProvenance>(next, CanonPath(getString(valueAt(obj, "subpath"))));
});

} // namespace nix
1 change: 1 addition & 0 deletions src/nix/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 28 additions & 0 deletions src/nix/provenance-show.md
Original file line number Diff line number Diff line change
@@ -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`.

)""
124 changes: 124 additions & 0 deletions src/nix/provenance.cc
Original file line number Diff line number Diff line change
@@ -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 <memory>
#include <nlohmann/json.hpp>

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> experimentalFeature() override
{
return Xp::Provenance;
}

Category category() override
{
return catUtility;
}
};

static auto rCmdProvenance = registerCommand<CmdProvenance>("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<const Provenance> provenance)
{
while (provenance) {
if (auto copied = std::dynamic_pointer_cast<const CopiedProvenance>(provenance)) {
logger->cout("← copied from " ANSI_BOLD "%s" ANSI_NORMAL, copied->from);
provenance = copied->next;
} else if (auto build = std::dynamic_pointer_cast<const BuildProvenance>(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<const FlakeProvenance>(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<const SubpathProvenance>(next)) {
next = subpath->next;
flakePath = subpath->subpath;
}
if (auto tree = std::dynamic_pointer_cast<const TreeProvenance>(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<const TreeProvenance>(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<const SubpathProvenance>(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<std::string>());
else
logger->cout("← " ANSI_RED "unknown provenance type" ANSI_NORMAL);
break;
}
}
}

void run(ref<Store> 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should probably exit with an error in this case; wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, a path having no provenance is in itself not an error. For a nix provenance verify command it should be, but it's probably fine here.

}
}
};

static auto rCmdProvenanceShow = registerCommand2<CmdProvenanceShow>({"provenance", "show"});
9 changes: 9 additions & 0 deletions tests/functional/flakes/provenance.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ nix copy --from "file://$binaryCache" "$outPath" --no-check-sigs
EOF
) ]]

# Test `nix provenance show`.
[[ $(nix provenance show "$outPath") = $(cat <<EOF
$outPath
← copied from file://$binaryCache
← built from derivation $drvPath (output out)
← instantiated from flake output git+file://$flake1Dir?ref=refs/heads/master&rev=$rev#packages.$system.default
EOF
) ]]

# Check that --impure does not add provenance.
clearStore
nix build --impure --print-out-paths --no-link "$flake1Dir#packages.$system.default"
Expand Down