diff --git a/src/libexpr/include/nix/expr/eval.hh b/src/libexpr/include/nix/expr/eval.hh index fd57170d1e8..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; + operator_, key, path, prefix, outputSpecified, __meta, identifiers, license, licenses; Expr::AstSymbols exprSymbols; @@ -281,6 +281,10 @@ struct StaticEvalSymbols .path = alloc.create("path"), .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/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..f4cc887a6b2 --- /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 DerivationProvenance : Provenance +{ + std::shared_ptr next; + ref meta; + + DerivationProvenance(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..2f99adefd88 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,29 @@ static void derivationStrictInternal(EvalState & state, std::string_view drvName experimentalFeatureSettings.require(Xp::ImpureDerivations); } break; + case EvalState::s.__meta.getId(): + if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) { + state.forceAttrs(*i->value, pos, ""); + auto meta = i->value->attrs(); + auto obj = nlohmann::json(); + + 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; + } + } + + provenance = + std::make_shared(provenance, make_ref(obj)); + } + break; /* The `args' attribute is special: it supplies the command-line arguments to the builder. */ case EvalState::s.args.getId(): @@ -1847,8 +1873,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 +5445,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..8bce4f12076 --- /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 DerivationProvenance::to_json() const +{ + return nlohmann::json{ + {"type", "derivation"}, + {"meta", *meta}, + {"next", next ? next->to_json() : nlohmann::json(nullptr)}, + }; +} + +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"))); +}); + +} // 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..6d8ddf4cda1 100644 --- a/src/nix/provenance.cc +++ b/src/nix/provenance.cc @@ -1,9 +1,11 @@ #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" #include "nix/util/provenance.hh" +#include "nix/util/json-utils.hh" #include #include @@ -92,6 +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 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 00dc007e12f..9f50a8b0454 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,8 @@ rec { eval "$buildCommand" '')]; PATH = path; - } // caArgs // removeAttrs args ["builder" "meta"]) + } // 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 20026f41d22..5c98ecd69ac 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"; + } + ]; + }; }