From 30e33f1ccc5585c04f7c07740e4113efd7d468e6 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Tue, 10 Feb 2026 10:30:11 -0800 Subject: [PATCH 1/4] Record subset of meta in provenance --- src/libexpr/include/nix/expr/meson.build | 1 + src/libexpr/include/nix/expr/provenance.hh | 23 +++++ src/libexpr/meson.build | 1 + src/libexpr/primops.cc | 33 +++++- src/libexpr/provenance.cc | 25 +++++ src/nix/provenance-show.md | 1 + src/nix/provenance.cc | 4 + tests/functional/config.nix.in | 7 +- tests/functional/flakes/provenance.sh | 114 +++++++++++++++------ tests/functional/simple.nix | 17 ++- 10 files changed, 189 insertions(+), 37 deletions(-) create mode 100644 src/libexpr/include/nix/expr/provenance.hh create mode 100644 src/libexpr/provenance.cc diff --git a/src/libexpr/include/nix/expr/meson.build b/src/libexpr/include/nix/expr/meson.build index 9f676b230f1..5c707ed4bff 100644 --- a/src/libexpr/include/nix/expr/meson.build +++ b/src/libexpr/include/nix/expr/meson.build @@ -30,6 +30,7 @@ headers = [ config_pub_h ] + files( 'print-ambiguous.hh', 'print-options.hh', 'print.hh', + 'provenance.hh', 'repl-exit-status.hh', 'search-path.hh', 'static-string-data.hh', diff --git a/src/libexpr/include/nix/expr/provenance.hh b/src/libexpr/include/nix/expr/provenance.hh new file mode 100644 index 00000000000..bbe3ec13c17 --- /dev/null +++ b/src/libexpr/include/nix/expr/provenance.hh @@ -0,0 +1,23 @@ +#pragma once + +#include "nix/util/provenance.hh" + +namespace nix { + +/** + * Provenance indicating that this store path was instantiated by the `derivation` builtin function. Its main purpose is + * to record `meta` fields. + */ +struct MetaProvenance : Provenance +{ + std::shared_ptr next; + ref meta; + + MetaProvenance(std::shared_ptr next, ref meta) + : next(std::move(next)) + , meta(std::move(meta)) {}; + + nlohmann::json to_json() const override; +}; + +} // namespace nix diff --git a/src/libexpr/meson.build b/src/libexpr/meson.build index 0edc2ef1daf..941cb0a8a44 100644 --- a/src/libexpr/meson.build +++ b/src/libexpr/meson.build @@ -181,6 +181,7 @@ sources = files( 'primops.cc', 'print-ambiguous.cc', 'print.cc', + 'provenance.cc', 'search-path.cc', 'symbol-table.cc', 'value-to-json.cc', diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 8d8ad226c57..fed8e272743 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -18,6 +18,7 @@ #include "nix/fetchers/fetch-to-store.hh" #include "nix/util/sort.hh" #include "nix/util/mounted-source-accessor.hh" +#include "nix/expr/provenance.hh" #include #include @@ -1504,6 +1505,8 @@ static void derivationStrictInternal(EvalState & state, std::string_view drvName StringSet outputs; outputs.insert("out"); + auto provenance = state.evalContext.provenance; + for (auto & i : attrs->lexicographicOrder(state.symbols)) { if (i->name == state.s.ignoreNulls) continue; @@ -1571,6 +1574,24 @@ static void derivationStrictInternal(EvalState & state, std::string_view drvName experimentalFeatureSettings.require(Xp::ImpureDerivations); } break; + case EvalState::s.meta.getId(): + experimentalFeatureSettings.require(Xp::Provenance); + + { + auto meta = printValueAsJSON(state, true, *i->value, pos, context); + + for (auto it = meta.begin(); it != meta.end();) { + if (it.key() == "identifiers" || it.key() == "license" || it.key() == "licenses") { + it++; + continue; + } + + it = meta.erase(it); + } + + provenance = std::make_shared(provenance, make_ref(meta)); + } + break; /* The `args' attribute is special: it supplies the command-line arguments to the builder. */ case EvalState::s.args.getId(): @@ -1847,8 +1868,7 @@ static void derivationStrictInternal(EvalState & state, std::string_view drvName } /* Write the resulting term into the Nix store directory. */ - auto drvPath = - writeDerivation(*state.store, *state.asyncPathWriter, drv, state.repair, false, state.evalContext.provenance); + auto drvPath = writeDerivation(*state.store, *state.asyncPathWriter, drv, state.repair, false, provenance); auto drvPathS = state.store->printStorePath(drvPath); printMsg(lvlChatty, "instantiated '%1%' -> '%2%'", drvName, drvPathS); @@ -5420,7 +5440,14 @@ void EvalState::createBaseEnv(const EvalSettings & evalSettings) language feature gets added. It's not necessary to increase it when primops get added, because you can just use `builtins ? primOp' to check. */ - v.mkInt(6); + if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) { + /* Provenance allows for meta to be inside of derivations. + We increment the version to 7 so Nixpkgs will know when + provenance is available. */ + v.mkInt(7); + } else { + v.mkInt(6); + } addConstant( "__langVersion", v, diff --git a/src/libexpr/provenance.cc b/src/libexpr/provenance.cc new file mode 100644 index 00000000000..25eee8191ca --- /dev/null +++ b/src/libexpr/provenance.cc @@ -0,0 +1,25 @@ +#include "nix/expr/provenance.hh" +#include "nix/util/json-utils.hh" + +#include + +namespace nix { + +nlohmann::json MetaProvenance::to_json() const +{ + return nlohmann::json{ + {"type", "meta"}, + {"meta", *meta}, + {"next", next ? next->to_json() : nlohmann::json(nullptr)}, + }; +} + +Provenance::Register registerMetaProvenance("meta", [](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, make_ref(valueAt(obj, "meta"))); +}); + +} // namespace nix diff --git a/src/nix/provenance-show.md b/src/nix/provenance-show.md index 95675430cdf..526fbd54c8e 100644 --- a/src/nix/provenance-show.md +++ b/src/nix/provenance-show.md @@ -22,6 +22,7 @@ The provenance chain shows the history of how the store path came to exist, incl - **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. +- **Meta**: Metadata associated with the derivation. 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 index 205ac76de23..31c7719eb2c 100644 --- a/src/nix/provenance.cc +++ b/src/nix/provenance.cc @@ -1,5 +1,6 @@ #include "nix/cmd/command.hh" #include "nix/store/store-api.hh" +#include "nix/expr/provenance.hh" #include "nix/store/provenance.hh" #include "nix/flake/provenance.hh" #include "nix/fetchers/provenance.hh" @@ -92,6 +93,9 @@ struct CmdProvenanceShow : StorePathsCommand } 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 if (auto meta = std::dynamic_pointer_cast(provenance)) { + logger->cout("← with metadata"); + provenance = meta->next; } else { // Unknown or unhandled provenance type auto json = provenance->to_json(); diff --git a/tests/functional/config.nix.in b/tests/functional/config.nix.in index 00dc007e12f..4dd8529d6ce 100644 --- a/tests/functional/config.nix.in +++ b/tests/functional/config.nix.in @@ -5,6 +5,9 @@ let outputHashMode = "recursive"; outputHashAlgo = "sha256"; } else {}; + + optional = cond: elem: if cond then [ elem ] else []; + optionalAttrs = cond: attrs: if cond then attrs else {}; in rec { @@ -25,6 +28,6 @@ rec { eval "$buildCommand" '')]; PATH = path; - } // caArgs // removeAttrs args ["builder" "meta"]) - // { meta = args.meta or {}; }; + } // caArgs // removeAttrs args (["builder"] ++ optional (builtins.langVersion < 7) "meta")) + // optionalAttrs (builtins.langVersion < 7) { meta = args.meta or {}; }; } diff --git a/tests/functional/flakes/provenance.sh b/tests/functional/flakes/provenance.sh index 20026f41d22..24a67c9c00a 100644 --- a/tests/functional/flakes/provenance.sh +++ b/tests/functional/flakes/provenance.sh @@ -15,36 +15,52 @@ lastModified=$(nix flake metadata --json "$flake1Dir" | jq -r .locked.lastModifi treePath=$(nix flake prefetch --json "$flake1Dir" | jq -r .storePath) builder=$(nix eval --raw "$flake1Dir#packages.$system.default._builder") -# Building a derivation should have tree+subpath+flake+build provenance. -[[ $(nix path-info --json --json-format 1 "$outPath" | jq ".\"$outPath\".provenance") = $(cat < "$flake1Dir/somefile" diff --git a/tests/functional/simple.nix b/tests/functional/simple.nix index 2eeb94fabd8..bd8b234852d 100644 --- a/tests/functional/simple.nix +++ b/tests/functional/simple.nix @@ -6,5 +6,20 @@ mkDerivation { _builder = ./simple.builder.sh; PATH = ""; goodPath = path; - meta.position = "${__curPos.file}:${toString __curPos.line}"; + meta = { + position = "${__curPos.file}:${toString __curPos.line}"; + license = [ + # Since this file is from Nix, use Nix's license. + # Keep in sync with `lib.licenses.lgpl21` from Nixpkgs. + { + deprecated = true; + free = true; + fullName = "GNU Lesser General Public License v2.1"; + redistributable = true; + shortName = "lgpl21"; + spdxId = "LGPL-2.1"; + url = "https://spdx.org/licenses/LGPL-2.1.html"; + } + ]; + }; } From bb832c0449c499087b40f191f9fce49fa30b8e84 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Tue, 10 Feb 2026 12:51:01 -0800 Subject: [PATCH 2/4] Update comment for provenance test --- tests/functional/flakes/provenance.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/flakes/provenance.sh b/tests/functional/flakes/provenance.sh index 24a67c9c00a..edea5fa8ee2 100644 --- a/tests/functional/flakes/provenance.sh +++ b/tests/functional/flakes/provenance.sh @@ -166,7 +166,7 @@ EOF EOF ) ]] -# Check that --impure does not add provenance. +# Check that --impure does not add additional provenance. clearStore nix build --impure --print-out-paths --no-link "$flake1Dir#packages.$system.default" [[ "$(nix path-info --json --json-format 1 "$drvPath" | jq ".\"$drvPath\".provenance")" = "$(cat << EOF From bccd6a4c57a5602128f75df740baf0d2629fd8f9 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Wed, 11 Feb 2026 12:51:21 -0800 Subject: [PATCH 3/4] Various review changes for meta in provenance --- src/libexpr/include/nix/expr/eval.hh | 3 +- src/libexpr/include/nix/expr/provenance.hh | 4 +- src/libexpr/primops.cc | 9 ++-- src/libexpr/provenance.cc | 8 +-- src/nix/provenance.cc | 58 ++++++++++++++++++++-- tests/functional/config.nix.in | 6 ++- tests/functional/flakes/provenance.sh | 10 ++-- 7 files changed, 77 insertions(+), 21 deletions(-) diff --git a/src/libexpr/include/nix/expr/eval.hh b/src/libexpr/include/nix/expr/eval.hh index fd57170d1e8..852ad0f7bcc 100644 --- a/src/libexpr/include/nix/expr/eval.hh +++ b/src/libexpr/include/nix/expr/eval.hh @@ -228,7 +228,7 @@ struct StaticEvalSymbols line, column, functor, toString, right, wrong, structuredAttrs, json, allowedReferences, allowedRequisites, disallowedReferences, disallowedRequisites, maxSize, maxClosureSize, builder, args, contentAddressed, impure, outputHash, outputHashAlgo, outputHashMode, recurseForDerivations, description, self, epsilon, startSet, - operator_, key, path, prefix, outputSpecified; + operator_, key, path, prefix, outputSpecified, __meta; Expr::AstSymbols exprSymbols; @@ -281,6 +281,7 @@ struct StaticEvalSymbols .path = alloc.create("path"), .prefix = alloc.create("prefix"), .outputSpecified = alloc.create("outputSpecified"), + .__meta = alloc.create("__meta"), .exprSymbols = { .sub = alloc.create("__sub"), .lessThan = alloc.create("__lessThan"), diff --git a/src/libexpr/include/nix/expr/provenance.hh b/src/libexpr/include/nix/expr/provenance.hh index bbe3ec13c17..f4cc887a6b2 100644 --- a/src/libexpr/include/nix/expr/provenance.hh +++ b/src/libexpr/include/nix/expr/provenance.hh @@ -8,12 +8,12 @@ namespace nix { * Provenance indicating that this store path was instantiated by the `derivation` builtin function. Its main purpose is * to record `meta` fields. */ -struct MetaProvenance : Provenance +struct DerivationProvenance : Provenance { std::shared_ptr next; ref meta; - MetaProvenance(std::shared_ptr next, ref meta) + DerivationProvenance(std::shared_ptr next, ref meta) : next(std::move(next)) , meta(std::move(meta)) {}; diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index fed8e272743..f2c527d68f2 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1574,10 +1574,8 @@ static void derivationStrictInternal(EvalState & state, std::string_view drvName experimentalFeatureSettings.require(Xp::ImpureDerivations); } break; - case EvalState::s.meta.getId(): - experimentalFeatureSettings.require(Xp::Provenance); - - { + case EvalState::s.__meta.getId(): + if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) { auto meta = printValueAsJSON(state, true, *i->value, pos, context); for (auto it = meta.begin(); it != meta.end();) { @@ -1589,7 +1587,8 @@ static void derivationStrictInternal(EvalState & state, std::string_view drvName it = meta.erase(it); } - provenance = std::make_shared(provenance, make_ref(meta)); + provenance = + std::make_shared(provenance, make_ref(meta)); } break; /* The `args' attribute is special: it supplies the diff --git a/src/libexpr/provenance.cc b/src/libexpr/provenance.cc index 25eee8191ca..8bce4f12076 100644 --- a/src/libexpr/provenance.cc +++ b/src/libexpr/provenance.cc @@ -5,21 +5,21 @@ namespace nix { -nlohmann::json MetaProvenance::to_json() const +nlohmann::json DerivationProvenance::to_json() const { return nlohmann::json{ - {"type", "meta"}, + {"type", "derivation"}, {"meta", *meta}, {"next", next ? next->to_json() : nlohmann::json(nullptr)}, }; } -Provenance::Register registerMetaProvenance("meta", [](nlohmann::json json) { +Provenance::Register registerDerivationProvenance("derivation", [](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, make_ref(valueAt(obj, "meta"))); + return make_ref(next, make_ref(valueAt(obj, "meta"))); }); } // namespace nix diff --git a/src/nix/provenance.cc b/src/nix/provenance.cc index 31c7719eb2c..6d8ddf4cda1 100644 --- a/src/nix/provenance.cc +++ b/src/nix/provenance.cc @@ -5,6 +5,7 @@ #include "nix/flake/provenance.hh" #include "nix/fetchers/provenance.hh" #include "nix/util/provenance.hh" +#include "nix/util/json-utils.hh" #include #include @@ -93,9 +94,60 @@ struct CmdProvenanceShow : StorePathsCommand } 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 if (auto meta = std::dynamic_pointer_cast(provenance)) { - logger->cout("← with metadata"); - provenance = meta->next; + } else if (auto drv = std::dynamic_pointer_cast(provenance)) { + logger->cout("← with derivation metadata"); +#define TAB " " + auto json = getObject(*(drv->meta)); + if (auto identifiers = optionalValueAt(json, "identifiers")) { + auto ident = getObject(*identifiers); + if (auto cpeParts = optionalValueAt(ident, "cpeParts")) { + auto parts = getObject(*cpeParts); + + auto vendor = parts["vendor"]; + auto product = parts["product"]; + auto version = parts["version"]; + auto update = parts["update"]; + + logger->cout( + TAB "" ANSI_BOLD "CPE:" ANSI_NORMAL " cpe:2.3:a:%s:%s:%s:%s:*:*:*:*:*:*", + vendor.is_null() ? "*" : vendor.get(), + product.is_null() ? "*" : product.get(), + version.is_null() ? "*" : version.get(), + update.is_null() ? "*" : update.get()); + } + } + if (auto license = optionalValueAt(json, "license")) { + if (license->is_array()) { + logger->cout(TAB "" ANSI_BOLD "Licenses:" ANSI_NORMAL); + auto licenses = getArray(*license); + for (auto it = licenses.begin(); it != licenses.end(); it++) { + auto license = getObject(*it); + auto shortName = license["shortName"]; + logger->cout(TAB "" TAB "- %s", shortName.get()); + } + } else { + auto obj = getObject(*license); + auto shortName = obj["shortName"]; + logger->cout(TAB "" ANSI_BOLD "License:" ANSI_NORMAL " %s", shortName.get()); + } + } + if (auto licenses = optionalValueAt(json, "licenses")) { + if (licenses->is_array()) { + logger->cout(TAB "" ANSI_BOLD "Licenses:" ANSI_NORMAL); + auto licensesArray = getArray(*licenses); + for (auto it = licensesArray.begin(); it != licensesArray.end(); it++) { + auto license = getObject(*it); + auto shortName = license["shortName"]; + logger->cout(TAB "" TAB "- %s", shortName.get()); + } + } else { + auto license = getObject(*licenses); + auto shortName = license["shortName"]; + logger->cout(TAB "" ANSI_BOLD "License:" ANSI_NORMAL " %s", shortName.get()); + } + } +#undef TAB + provenance = drv->next; } else { // Unknown or unhandled provenance type auto json = provenance->to_json(); diff --git a/tests/functional/config.nix.in b/tests/functional/config.nix.in index 4dd8529d6ce..9f50a8b0454 100644 --- a/tests/functional/config.nix.in +++ b/tests/functional/config.nix.in @@ -28,6 +28,8 @@ rec { eval "$buildCommand" '')]; PATH = path; - } // caArgs // removeAttrs args (["builder"] ++ optional (builtins.langVersion < 7) "meta")) - // optionalAttrs (builtins.langVersion < 7) { meta = args.meta or {}; }; + } // caArgs // optionalAttrs (builtins.langVersion >= 7) { + __meta = args.meta or {}; + } // removeAttrs args ["builder" "meta"]) + // { meta = args.meta or {}; }; } diff --git a/tests/functional/flakes/provenance.sh b/tests/functional/flakes/provenance.sh index edea5fa8ee2..5c98ecd69ac 100644 --- a/tests/functional/flakes/provenance.sh +++ b/tests/functional/flakes/provenance.sh @@ -53,7 +53,7 @@ builder=$(nix eval --raw "$flake1Dir#packages.$system.default._builder") }, "type": "flake" }, - "type": "meta" + "type": "derivation" }, "output": "out", "system": "$system", @@ -145,7 +145,7 @@ nix copy --from "file://$binaryCache" "$outPath" --no-check-sigs }, "type": "flake" }, - "type": "meta" + "type": "derivation" }, "output": "out", "system": "$system", @@ -161,7 +161,9 @@ EOF $outPath ← copied from file://$binaryCache ← built from derivation $drvPath (output out) on test-host for $system -← with metadata +← with derivation metadata + Licenses: + - lgpl21 ← instantiated from flake output git+file://$flake1Dir?ref=refs/heads/master&rev=$rev#packages.$system.default EOF ) ]] @@ -185,7 +187,7 @@ nix build --impure --print-out-paths --no-link "$flake1Dir#packages.$system.defa ] }, "next": null, - "type": "meta" + "type": "derivation" } EOF )" ]] From b15871839fcdbb1295baa4190ebc2acc635e17b1 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Wed, 11 Feb 2026 13:20:11 -0800 Subject: [PATCH 4/4] Selective eval of meta for provenance --- src/libexpr/include/nix/expr/eval.hh | 5 ++++- src/libexpr/primops.cc | 20 +++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/libexpr/include/nix/expr/eval.hh b/src/libexpr/include/nix/expr/eval.hh index 852ad0f7bcc..aade0243268 100644 --- a/src/libexpr/include/nix/expr/eval.hh +++ b/src/libexpr/include/nix/expr/eval.hh @@ -228,7 +228,7 @@ struct StaticEvalSymbols line, column, functor, toString, right, wrong, structuredAttrs, json, allowedReferences, allowedRequisites, disallowedReferences, disallowedRequisites, maxSize, maxClosureSize, builder, args, contentAddressed, impure, outputHash, outputHashAlgo, outputHashMode, recurseForDerivations, description, self, epsilon, startSet, - operator_, key, path, prefix, outputSpecified, __meta; + operator_, key, path, prefix, outputSpecified, __meta, identifiers, license, licenses; Expr::AstSymbols exprSymbols; @@ -282,6 +282,9 @@ struct StaticEvalSymbols .prefix = alloc.create("prefix"), .outputSpecified = alloc.create("outputSpecified"), .__meta = alloc.create("__meta"), + .identifiers = alloc.create("identifier"), + .license = alloc.create("license"), + .licenses = alloc.create("licenses"), .exprSymbols = { .sub = alloc.create("__sub"), .lessThan = alloc.create("__lessThan"), diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index f2c527d68f2..2f99adefd88 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1576,19 +1576,25 @@ static void derivationStrictInternal(EvalState & state, std::string_view drvName break; case EvalState::s.__meta.getId(): if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) { - auto meta = printValueAsJSON(state, true, *i->value, pos, context); + state.forceAttrs(*i->value, pos, ""); + auto meta = i->value->attrs(); + auto obj = nlohmann::json(); - for (auto it = meta.begin(); it != meta.end();) { - if (it.key() == "identifiers" || it.key() == "license" || it.key() == "licenses") { - it++; + for (auto & i : meta->lexicographicOrder(state.symbols)) { + auto key = state.symbols[i->name]; + switch (i->name.getId()) { + case EvalState::s.identifiers.getId(): + case EvalState::s.license.getId(): + case EvalState::s.licenses.getId(): + obj.emplace(key, printValueAsJSON(state, true, *i->value, pos, context)); + break; + default: continue; } - - it = meta.erase(it); } provenance = - std::make_shared(provenance, make_ref(meta)); + std::make_shared(provenance, make_ref(obj)); } break; /* The `args' attribute is special: it supplies the