Skip to content

Commit 0a5eef6

Browse files
committed
Add subcommand 'nix provenance show'
1 parent c9f71f3 commit 0a5eef6

9 files changed

Lines changed: 200 additions & 0 deletions

File tree

src/libfetchers/include/nix/fetchers/provenance.hh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ struct TreeProvenance : Provenance
1111

1212
TreeProvenance(const fetchers::Input & input);
1313

14+
TreeProvenance(ref<nlohmann::json> attrs)
15+
: attrs(std::move(attrs))
16+
{
17+
}
18+
1419
nlohmann::json to_json() const override;
1520
};
1621

src/libfetchers/provenance.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "nix/fetchers/provenance.hh"
22
#include "nix/fetchers/attrs.hh"
3+
#include "nix/util/json-utils.hh"
34

45
#include <nlohmann/json.hpp>
56

@@ -24,4 +25,10 @@ nlohmann::json TreeProvenance::to_json() const
2425
};
2526
}
2627

28+
Provenance::Register registerTreeProvenance("tree", [](nlohmann::json json) {
29+
auto & obj = getObject(json);
30+
auto & attrsJson = valueAt(obj, "attrs");
31+
return make_ref<TreeProvenance>(make_ref<nlohmann::json>(attrsJson));
32+
});
33+
2734
} // namespace nix

src/libflake/provenance.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "nix/flake/provenance.hh"
2+
#include "nix/util/json-utils.hh"
23

34
#include <nlohmann/json.hpp>
45

@@ -13,4 +14,12 @@ nlohmann::json FlakeProvenance::to_json() const
1314
};
1415
}
1516

17+
Provenance::Register registerFlakeProvenance("flake", [](nlohmann::json json) {
18+
auto & obj = getObject(json);
19+
std::shared_ptr<const Provenance> next;
20+
if (auto p = optionalValueAt(obj, "next"); p && !p->is_null())
21+
next = Provenance::from_json(*p);
22+
return make_ref<FlakeProvenance>(next, getString(valueAt(obj, "flakeOutput")));
23+
});
24+
1625
} // namespace nix

src/libstore/provenance.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ nlohmann::json BuildProvenance::to_json() const
1313
};
1414
}
1515

16+
Provenance::Register registerBuildProvenance("build", [](nlohmann::json json) {
17+
auto & obj = getObject(json);
18+
std::shared_ptr<const Provenance> next;
19+
if (auto p = optionalValueAt(obj, "next"); p && !p->is_null())
20+
next = Provenance::from_json(*p);
21+
return make_ref<BuildProvenance>(
22+
StorePath(getString(valueAt(obj, "drv"))), getString(valueAt(obj, "output")), next);
23+
});
24+
1625
nlohmann::json CopiedProvenance::to_json() const
1726
{
1827
return {

src/libutil/provenance.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,12 @@ nlohmann::json SubpathProvenance::to_json() const
6363
};
6464
}
6565

66+
Provenance::Register registerSubpathProvenance("subpath", [](nlohmann::json json) {
67+
auto & obj = getObject(json);
68+
std::shared_ptr<const Provenance> next;
69+
if (auto p = optionalValueAt(obj, "next"); p && !p->is_null())
70+
next = Provenance::from_json(*p);
71+
return make_ref<SubpathProvenance>(next, CanonPath(getString(valueAt(obj, "subpath"))));
72+
});
73+
6674
} // namespace nix

src/nix/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ nix_sources = [ config_priv_h ] + files(
9494
'path-info.cc',
9595
'prefetch.cc',
9696
'profile.cc',
97+
'provenance.cc',
9798
'ps.cc',
9899
'realisation.cc',
99100
'registry.cc',

src/nix/provenance-show.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
R""(
2+
3+
# Examples
4+
5+
* Show the provenance of a store path:
6+
7+
```console
8+
# nix provenance show /run/current-system
9+
/nix/store/k145bdxhdb89i4fkvgdisdz1yh2wiymm-nixos-system-machine-25.05.20251210.d2b1213
10+
← copied from cache.flakehub.com
11+
← built from derivation /nix/store/w3p3xkminq61hs00kihd34w1dglpj5s9-nixos-system-machine-25.05.20251210.d2b1213.drv (output out)
12+
← instantiated from flake output github:my-org/my-repo/6b03eb949597fe96d536e956a2c14da9901dbd21?dir=machine#nixosConfigurations.machine.config.system.build.toplevel
13+
```
14+
15+
# Description
16+
17+
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.
18+
19+
The provenance chain shows the history of how the store path came to exist, including:
20+
21+
- **Copied**: The path was copied from another Nix store, typically a binary cache.
22+
- **Built**: The path was built from a derivation.
23+
- **Flake evaluation**: The derivation was instantiated during the evaluation of a flake output.
24+
- **Fetched**: The path was obtained by fetching a source tree.
25+
26+
Note: if you want provenance in JSON format, use the `provenance` field returned by `nix path-info --json`.
27+
28+
)""

src/nix/provenance.cc

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#include "nix/cmd/command.hh"
2+
#include "nix/store/store-api.hh"
3+
#include "nix/store/provenance.hh"
4+
#include "nix/flake/provenance.hh"
5+
#include "nix/fetchers/provenance.hh"
6+
#include "nix/util/provenance.hh"
7+
8+
#include <memory>
9+
#include <nlohmann/json.hpp>
10+
11+
using namespace nix;
12+
13+
struct CmdProvenance : NixMultiCommand
14+
{
15+
CmdProvenance()
16+
: NixMultiCommand("provenance", RegisterCommand::getCommandsFor({"provenance"}))
17+
{
18+
}
19+
20+
std::string description() override
21+
{
22+
return "query and check the provenance of store paths";
23+
}
24+
25+
std::optional<ExperimentalFeature> experimentalFeature() override
26+
{
27+
return Xp::Provenance;
28+
}
29+
30+
Category category() override
31+
{
32+
return catUtility;
33+
}
34+
};
35+
36+
static auto rCmdProvenance = registerCommand<CmdProvenance>("provenance");
37+
38+
struct CmdProvenanceShow : StorePathsCommand
39+
{
40+
std::string description() override
41+
{
42+
return "show the provenance chain of store paths";
43+
}
44+
45+
std::string doc() override
46+
{
47+
return
48+
#include "provenance-show.md"
49+
;
50+
}
51+
52+
void displayProvenance(Store & store, const StorePath & path, std::shared_ptr<const Provenance> provenance)
53+
{
54+
while (provenance) {
55+
if (auto copied = std::dynamic_pointer_cast<const CopiedProvenance>(provenance)) {
56+
logger->cout("← copied from " ANSI_BOLD "%s" ANSI_NORMAL, copied->from);
57+
provenance = copied->next;
58+
} else if (auto build = std::dynamic_pointer_cast<const BuildProvenance>(provenance)) {
59+
logger->cout(
60+
"← built from derivation " ANSI_BOLD "%s" ANSI_NORMAL " (output " ANSI_BOLD "%s" ANSI_NORMAL ")",
61+
store.printStorePath(build->drvPath),
62+
build->output);
63+
provenance = build->next;
64+
} else if (auto flake = std::dynamic_pointer_cast<const FlakeProvenance>(provenance)) {
65+
// Collapse subpath/tree provenance into the flake provenance for legibility.
66+
auto next = flake->next;
67+
CanonPath flakePath("/flake.nix");
68+
if (auto subpath = std::dynamic_pointer_cast<const SubpathProvenance>(next)) {
69+
next = subpath->next;
70+
flakePath = subpath->subpath;
71+
}
72+
if (auto tree = std::dynamic_pointer_cast<const TreeProvenance>(next)) {
73+
FlakeRef flakeRef(
74+
fetchers::Input::fromAttrs(fetchSettings, fetchers::jsonToAttrs(*tree->attrs)),
75+
Path(flakePath.parent().value_or(CanonPath::root).rel()));
76+
logger->cout(
77+
"← instantiated from flake output " ANSI_BOLD "%s#%s" ANSI_NORMAL,
78+
flakeRef.to_string(),
79+
flake->flakeOutput);
80+
break;
81+
} else {
82+
logger->cout("← instantiated from flake output " ANSI_BOLD "%s" ANSI_NORMAL, flake->flakeOutput);
83+
provenance = flake->next;
84+
}
85+
} else if (auto tree = std::dynamic_pointer_cast<const TreeProvenance>(provenance)) {
86+
auto input = fetchers::Input::fromAttrs(fetchSettings, fetchers::jsonToAttrs(*tree->attrs));
87+
logger->cout("← from tree " ANSI_BOLD "%s" ANSI_NORMAL, input.to_string());
88+
break;
89+
} else if (auto subpath = std::dynamic_pointer_cast<const SubpathProvenance>(provenance)) {
90+
logger->cout("← from file " ANSI_BOLD "%s" ANSI_NORMAL, subpath->subpath.abs());
91+
provenance = subpath->next;
92+
} else {
93+
// Unknown or unhandled provenance type
94+
auto json = provenance->to_json();
95+
auto typeIt = json.find("type");
96+
if (typeIt != json.end() && typeIt->is_string())
97+
logger->cout("" ANSI_RED "unknown provenance type '%s'" ANSI_NORMAL, typeIt->get<std::string>());
98+
else
99+
logger->cout("" ANSI_RED "unknown provenance type" ANSI_NORMAL);
100+
break;
101+
}
102+
}
103+
}
104+
105+
void run(ref<Store> store, StorePaths && storePaths) override
106+
{
107+
bool first = true;
108+
109+
for (auto & storePath : storePaths) {
110+
auto info = store->queryPathInfo(storePath);
111+
if (!first)
112+
logger->cout("");
113+
first = false;
114+
logger->cout(ANSI_BOLD "%s" ANSI_NORMAL, store->printStorePath(info->path));
115+
116+
if (info->provenance)
117+
displayProvenance(*store, storePath, info->provenance);
118+
else
119+
logger->cout(ANSI_RED " (no provenance information available)" ANSI_NORMAL);
120+
}
121+
}
122+
};
123+
124+
static auto rCmdProvenanceShow = registerCommand2<CmdProvenanceShow>({"provenance", "show"});

tests/functional/flakes/provenance.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ nix copy --from "file://$binaryCache" "$outPath" --no-check-sigs
120120
EOF
121121
) ]]
122122

123+
# Test `nix provenance show`.
124+
[[ $(nix provenance show "$outPath") = $(cat <<EOF
125+
$outPath
126+
← copied from file://$binaryCache
127+
← built from derivation $drvPath (output out)
128+
← instantiated from flake output git+file://$flake1Dir?ref=refs/heads/master&rev=$rev#packages.$system.default
129+
EOF
130+
) ]]
131+
123132
# Check that --impure does not add provenance.
124133
clearStore
125134
nix build --impure --print-out-paths --no-link "$flake1Dir#packages.$system.default"

0 commit comments

Comments
 (0)