From e096c4bce8e8a69e1d620016079298faa8129ca7 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 12 Jan 2026 14:33:15 +0100 Subject: [PATCH 01/17] attrsToJSON(): Never serialize the __final attribute --- src/libfetchers/attrs.cc | 3 +++ src/libflake/lockfile.cc | 4 ---- src/nix/flake.cc | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libfetchers/attrs.cc b/src/libfetchers/attrs.cc index 841808bd16a..648d4854543 100644 --- a/src/libfetchers/attrs.cc +++ b/src/libfetchers/attrs.cc @@ -27,6 +27,9 @@ nlohmann::json attrsToJSON(const Attrs & attrs) { nlohmann::json json; for (auto & attr : attrs) { + /* The __final attribute is purely internal, so never serialize it. */ + if (attr.first == "__final") + continue; if (auto v = std::get_if(&attr.second)) { json[attr.first] = *v; } else if (auto v = std::get_if(&attr.second)) { diff --git a/src/libflake/lockfile.cc b/src/libflake/lockfile.cc index 83a692b9871..b287db5b8e5 100644 --- a/src/libflake/lockfile.cc +++ b/src/libflake/lockfile.cc @@ -230,11 +230,7 @@ std::pair LockFile::toJSON() const if (auto lockedNode = node.dynamic_pointer_cast()) { n["original"] = fetchers::attrsToJSON(lockedNode->originalRef.toAttrs()); n["locked"] = fetchers::attrsToJSON(lockedNode->lockedRef.toAttrs()); - /* For backward compatibility, omit the "__final" - attribute. We never allow non-final inputs in lock files - anyway. */ assert(lockedNode->lockedRef.input.isFinal() || lockedNode->lockedRef.input.isRelative()); - n["locked"].erase("__final"); if (!lockedNode->isFlake) n["flake"] = false; if (lockedNode->buildTime) diff --git a/src/nix/flake.cc b/src/nix/flake.cc index dc7e82d98a0..e03581a25d0 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -1487,7 +1487,6 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON res["hash"] = hash.to_string(HashFormat::SRI, true); res["original"] = fetchers::attrsToJSON(resolvedRef.toAttrs()); res["locked"] = fetchers::attrsToJSON(lockedRef.toAttrs()); - res["locked"].erase("__final"); // internal for now printJSON(res); } else { notice( From 607804e08d78e372eec3ea63f9333bda145c748a Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 8 Jan 2026 17:41:15 +0100 Subject: [PATCH 02/17] Provenance tracking --- .../json/schema/store-object-info-v2.yaml | 20 ++++++ .../include/nix/cmd/installable-flake.hh | 2 + src/libcmd/installable-flake.cc | 13 ++++ src/libexpr/eval.cc | 10 +++ src/libexpr/include/nix/expr/eval.hh | 14 +++++ src/libexpr/primops.cc | 6 +- src/libfetchers/fetchers.cc | 7 ++- src/libfetchers/filtering-source-accessor.cc | 7 +++ .../nix/fetchers/filtering-source-accessor.hh | 2 + .../include/nix/fetchers/meson.build | 1 + .../include/nix/fetchers/provenance.hh | 17 ++++++ src/libfetchers/meson.build | 1 + src/libfetchers/provenance.cc | 27 ++++++++ src/libfetchers/tarball.cc | 22 +++++++ src/libflake/flake.cc | 1 + src/libflake/include/nix/flake/flake.hh | 6 ++ src/libflake/include/nix/flake/meson.build | 1 + src/libflake/include/nix/flake/provenance.hh | 19 ++++++ src/libflake/meson.build | 1 + src/libflake/provenance.cc | 16 +++++ .../data/dummy-store/one-flat-file.json | 1 + .../data/nar-info/json-1/impure.json | 1 + .../data/nar-info/json-2/impure.json | 1 + .../data/path-info/json-1/empty_impure.json | 1 + .../data/path-info/json-1/impure.json | 1 + .../data/path-info/json-2/empty_impure.json | 1 + .../data/path-info/json-2/impure.json | 1 + .../unkeyed-valid-path-info-2.3.json | 2 + .../unkeyed-valid-path-info-2.4.json | 2 + .../unkeyed-valid-path-info-1.15.json | 2 + .../worker-protocol/valid-path-info-1.15.json | 2 + .../worker-protocol/valid-path-info-1.16.json | 3 + src/libstore/async-path-writer.cc | 15 ++++- src/libstore/binary-cache-store.cc | 5 +- .../build/derivation-building-goal.cc | 3 + src/libstore/derivations.cc | 25 ++++++-- src/libstore/dummy-store.cc | 20 +++--- .../include/nix/store/async-path-writer.hh | 7 ++- .../include/nix/store/binary-cache-store.hh | 8 ++- .../nix/store/build/derivation-builder.hh | 5 ++ src/libstore/include/nix/store/derivations.hh | 11 +++- .../include/nix/store/legacy-ssh-store.hh | 16 +++-- src/libstore/include/nix/store/local-store.hh | 3 +- src/libstore/include/nix/store/meson.build | 1 + src/libstore/include/nix/store/path-info.hh | 7 +++ src/libstore/include/nix/store/provenance.hh | 60 ++++++++++++++++++ .../include/nix/store/remote-store.hh | 14 +++-- src/libstore/include/nix/store/store-api.hh | 17 +++++- src/libstore/local-store.cc | 18 ++++-- src/libstore/meson.build | 1 + src/libstore/nar-info.cc | 7 ++- src/libstore/path-info.cc | 7 +++ src/libstore/provenance.cc | 35 +++++++++++ src/libstore/remote-store.cc | 14 ++++- src/libstore/restricted-store.cc | 8 ++- src/libstore/ssh-store.cc | 5 ++ src/libstore/store-api.cc | 38 ++++++++---- src/libstore/unix/build/derivation-builder.cc | 3 + src/libutil/include/nix/util/meson.build | 1 + src/libutil/include/nix/util/provenance.hh | 55 +++++++++++++++++ .../include/nix/util/source-accessor.hh | 8 +++ src/libutil/include/nix/util/source-path.hh | 5 ++ src/libutil/meson.build | 1 + src/libutil/mounted-source-accessor.cc | 6 ++ src/libutil/provenance.cc | 61 +++++++++++++++++++ src/libutil/source-accessor.cc | 7 +++ src/libutil/union-source-accessor.cc | 10 +++ src/nix/flake.cc | 4 +- src/nix/nario.cc | 3 +- 69 files changed, 657 insertions(+), 68 deletions(-) create mode 100644 src/libfetchers/include/nix/fetchers/provenance.hh create mode 100644 src/libfetchers/provenance.cc create mode 100644 src/libflake/include/nix/flake/provenance.hh create mode 100644 src/libflake/provenance.cc create mode 100644 src/libstore/include/nix/store/provenance.hh create mode 100644 src/libstore/provenance.cc create mode 100644 src/libutil/include/nix/util/provenance.hh create mode 100644 src/libutil/provenance.cc diff --git a/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml b/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml index 6ebaa3b2422..97c92fb6bf8 100644 --- a/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml +++ b/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml @@ -122,6 +122,7 @@ $defs: - registrationTime - ultimate - signatures + - provenance properties: version: { $ref: "#/$defs/base/properties/version" } path: { $ref: "#/$defs/base/properties/path" } @@ -179,6 +180,15 @@ $defs: The total size of this store object and every other object in its [closure](@docroot@/glossary.md#gloss-closure). > This field is not stored at all, but computed by traversing the other fields across all the store objects in a closure. + + provenance: + oneOf: + - type: "null" + - type: object # FIXME + title: Provenance + description: | + An arbitrary JSON object containing provenance information about the store object, or `null` if not available. + additionalProperties: false narInfo: @@ -206,6 +216,7 @@ $defs: - compression - downloadHash - downloadSize + - provenance properties: version: { $ref: "#/$defs/base/properties/version" } path: { $ref: "#/$defs/base/properties/path" } @@ -262,4 +273,13 @@ $defs: > This is an impure "`.narinfo`" field that may not be included in certain contexts. > This field is not stored at all, but computed by traversing the other fields across all the store objects in a closure. + + provenance: + oneOf: + - type: "null" + - type: object # FIXME + title: Provenance + description: | + An arbitrary JSON object containing provenance information about the store object, or `null` if not available. + additionalProperties: false diff --git a/src/libcmd/include/nix/cmd/installable-flake.hh b/src/libcmd/include/nix/cmd/installable-flake.hh index 9f449ad48f2..99417dc90ec 100644 --- a/src/libcmd/include/nix/cmd/installable-flake.hh +++ b/src/libcmd/include/nix/cmd/installable-flake.hh @@ -72,6 +72,8 @@ struct InstallableFlake : InstallableValue ref getLockedFlake() const; FlakeRef nixpkgsFlakeRef() const; + + std::shared_ptr makeProvenance(std::string_view attrPath) const; }; /** diff --git a/src/libcmd/installable-flake.cc b/src/libcmd/installable-flake.cc index 70267a65c09..2a1b1444d3f 100644 --- a/src/libcmd/installable-flake.cc +++ b/src/libcmd/installable-flake.cc @@ -17,6 +17,7 @@ #include "nix/util/url.hh" #include "nix/fetchers/registry.hh" #include "nix/store/build-result.hh" +#include "nix/flake/provenance.hh" #include #include @@ -84,6 +85,8 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths() auto attrPath = attr->getAttrPathStr(); + state->setRootProvenance(makeProvenance(attrPath)); + if (!attr->isDerivation()) { // FIXME: use eval cache? @@ -172,6 +175,8 @@ std::vector> InstallableFlake::getCursors(EvalState for (auto & attrPath : attrPaths) { debug("trying flake output attribute '%s'", attrPath); + state.setRootProvenance(makeProvenance(attrPath)); + auto attr = root->findAlongAttrPath(AttrPath::parse(state, attrPath)); if (attr) { res.push_back(ref(*attr)); @@ -212,4 +217,12 @@ FlakeRef InstallableFlake::nixpkgsFlakeRef() const return defaultNixpkgsFlakeRef(); } +std::shared_ptr InstallableFlake::makeProvenance(std::string_view attrPath) const +{ + auto provenance = getLockedFlake()->flake.provenance; + if (!provenance) + return nullptr; + return std::make_shared(provenance, std::string(attrPath)); +} + } // namespace nix diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 46393b79c5e..0939f4fc7fb 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -3369,4 +3369,14 @@ void EvalState::waitForAllPaths() asyncPathWriter->waitForAllPaths(); } +std::shared_ptr EvalState::getRootProvenance() +{ + return rootProvenance; +} + +void EvalState::setRootProvenance(std::shared_ptr provenance) +{ + rootProvenance = provenance; +} + } // namespace nix diff --git a/src/libexpr/include/nix/expr/eval.hh b/src/libexpr/include/nix/expr/eval.hh index 70bc5f6fbff..5c478adcef0 100644 --- a/src/libexpr/include/nix/expr/eval.hh +++ b/src/libexpr/include/nix/expr/eval.hh @@ -52,6 +52,7 @@ enum RepairFlag : bool; struct MemorySourceAccessor; struct MountedSourceAccessor; struct AsyncPathWriter; +struct Provenance; namespace eval_cache { class EvalCache; @@ -1137,6 +1138,19 @@ public: * `EvalState`. */ ref executor; + +private: + + std::shared_ptr rootProvenance; + +public: + + /** + * Set the provenance of derivations instantiated by the evaluator. + */ + void setRootProvenance(std::shared_ptr provenance); + + std::shared_ptr getRootProvenance(); }; struct DebugTraceStacker diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 3254ad0bc12..4f438466529 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1847,7 +1847,8 @@ 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); + auto drvPath = + writeDerivation(*state.store, *state.asyncPathWriter, drv, state.repair, false, state.getRootProvenance()); auto drvPathS = state.store->printStorePath(drvPath); printMsg(lvlChatty, "instantiated '%1%' -> '%2%'", drvName, drvPathS); @@ -2733,7 +2734,8 @@ static void prim_toFile(EvalState & state, const PosIdx pos, Value ** args, Valu ContentAddressMethod::Raw::Text, HashAlgorithm::SHA256, refs, - state.repair); + state.repair, + state.getRootProvenance()); }); /* Note: we don't need to add `context' to the context of the diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index 48c75df4f64..0df991c1d2c 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -4,7 +4,7 @@ #include "nix/fetchers/fetch-to-store.hh" #include "nix/util/json-utils.hh" #include "nix/fetchers/fetch-settings.hh" -#include "nix/fetchers/fetch-to-store.hh" +#include "nix/fetchers/provenance.hh" #include "nix/util/url.hh" #include "nix/util/forwarding-source-accessor.hh" #include "nix/util/archive.hh" @@ -196,7 +196,6 @@ bool Input::contains(const Input & other) const return false; } -// FIXME: remove std::tuple, Input> Input::fetchToStore(const Settings & settings, Store & store) const { if (!scheme) @@ -341,6 +340,8 @@ std::pair, Input> Input::getAccessorUnchecked(const Settings {{"hash", store.queryPathInfo(*storePath)->narHash.to_string(HashFormat::SRI, true)}}); } + accessor->provenance = std::make_shared(*this); + // FIXME: ideally we would use the `showPath()` of the // "real" accessor for this fetcher type. accessor->setPathDisplay("«" + to_string(true) + "»"); @@ -365,6 +366,8 @@ std::pair, Input> Input::getAccessorUnchecked(const Settings else accessor->fingerprint = result.getFingerprint(store); + accessor->provenance = std::make_shared(result); + return {accessor, std::move(result)}; } catch (Error & e) { if (storePath) { diff --git a/src/libfetchers/filtering-source-accessor.cc b/src/libfetchers/filtering-source-accessor.cc index 6b0748860c8..17aed89dff3 100644 --- a/src/libfetchers/filtering-source-accessor.cc +++ b/src/libfetchers/filtering-source-accessor.cc @@ -68,6 +68,13 @@ std::pair> FilteringSourceAccessor::getFin return next->getFingerprint(prefix / path); } +std::shared_ptr FilteringSourceAccessor::getProvenance(const CanonPath & path) +{ + if (provenance) + return provenance; + return next->getProvenance(prefix / path); +} + void FilteringSourceAccessor::invalidateCache(const CanonPath & path) { next->invalidateCache(prefix / path); diff --git a/src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh b/src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh index 63df495907a..b53c8db5bd7 100644 --- a/src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh +++ b/src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh @@ -52,6 +52,8 @@ struct FilteringSourceAccessor : SourceAccessor std::pair> getFingerprint(const CanonPath & path) override; + std::shared_ptr getProvenance(const CanonPath & path) override; + void invalidateCache(const CanonPath & path) override; /** diff --git a/src/libfetchers/include/nix/fetchers/meson.build b/src/libfetchers/include/nix/fetchers/meson.build index a313b1e0bc0..f3bb80942a2 100644 --- a/src/libfetchers/include/nix/fetchers/meson.build +++ b/src/libfetchers/include/nix/fetchers/meson.build @@ -10,6 +10,7 @@ headers = files( 'git-lfs-fetch.hh', 'git-utils.hh', 'input-cache.hh', + 'provenance.hh', 'registry.hh', 'tarball.hh', ) diff --git a/src/libfetchers/include/nix/fetchers/provenance.hh b/src/libfetchers/include/nix/fetchers/provenance.hh new file mode 100644 index 00000000000..bb7847112ac --- /dev/null +++ b/src/libfetchers/include/nix/fetchers/provenance.hh @@ -0,0 +1,17 @@ +#pragma once + +#include "nix/util/provenance.hh" +#include "nix/fetchers/fetchers.hh" + +namespace nix { + +struct TreeProvenance : Provenance +{ + ref attrs; + + TreeProvenance(const fetchers::Input & input); + + nlohmann::json to_json() const override; +}; + +} // namespace nix diff --git a/src/libfetchers/meson.build b/src/libfetchers/meson.build index cd04615e52c..0f99bc10f19 100644 --- a/src/libfetchers/meson.build +++ b/src/libfetchers/meson.build @@ -49,6 +49,7 @@ sources = files( 'input-cache.cc', 'mercurial.cc', 'path.cc', + 'provenance.cc', 'registry.cc', 'tarball.cc', ) diff --git a/src/libfetchers/provenance.cc b/src/libfetchers/provenance.cc new file mode 100644 index 00000000000..bfbb9133fe8 --- /dev/null +++ b/src/libfetchers/provenance.cc @@ -0,0 +1,27 @@ +#include "nix/fetchers/provenance.hh" +#include "nix/fetchers/attrs.hh" + +#include + +namespace nix { + +TreeProvenance::TreeProvenance(const fetchers::Input & input) + : attrs(make_ref([&]() { + // Remove the narHash attribute from the provenance info, as it's redundant (it's already recorded in the store + // path info). + auto attrs2 = input.attrs; + attrs2.erase("narHash"); + return fetchers::attrsToJSON(attrs2); + }())) +{ +} + +nlohmann::json TreeProvenance::to_json() const +{ + return nlohmann::json{ + {"type", "tree"}, + {"attrs", *attrs}, + }; +} + +} // namespace nix diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index 3b9e756fec8..d5b62e74f19 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -9,9 +9,30 @@ #include "nix/store/store-api.hh" #include "nix/fetchers/git-utils.hh" #include "nix/fetchers/fetch-settings.hh" +#include "nix/util/provenance.hh" + +#include namespace nix::fetchers { +struct FetchurlProvenance : Provenance +{ + std::string url; + + FetchurlProvenance(const std::string & url) + : url(url) + { + } + + nlohmann::json to_json() const override + { + return nlohmann::json{ + {"type", "fetchurl"}, + {"url", url}, + }; + } +}; + DownloadFileResult downloadFile( Store & store, const Settings & settings, @@ -83,6 +104,7 @@ DownloadFileResult downloadFile( }, hashString(HashAlgorithm::SHA256, sink.s)); info.narSize = sink.s.size(); + info.provenance = std::make_shared(url); auto source = StringSource{sink.s}; store.addToStore(info, source, NoRepair, NoCheckSigs); storePath = std::move(info.path); diff --git a/src/libflake/flake.cc b/src/libflake/flake.cc index 66d18f09f1a..90913594865 100644 --- a/src/libflake/flake.cc +++ b/src/libflake/flake.cc @@ -269,6 +269,7 @@ static Flake readFlake( .resolvedRef = resolvedRef, .lockedRef = lockedRef, .path = flakePath, + .provenance = flakePath.getProvenance(), }; if (auto description = vInfo.attrs()->get(state.s.description)) { diff --git a/src/libflake/include/nix/flake/flake.hh b/src/libflake/include/nix/flake/flake.hh index 0ca6094175e..49a7968db4b 100644 --- a/src/libflake/include/nix/flake/flake.hh +++ b/src/libflake/include/nix/flake/flake.hh @@ -10,6 +10,7 @@ namespace nix { class EvalState; +struct Provenance; namespace flake { @@ -94,6 +95,11 @@ struct Flake */ SourcePath path; + /** + * Cached provenance of `flake.nix` (equivalent to `path.getProvenance()`). + */ + std::shared_ptr provenance; + /** * Pretend that `lockedRef` is dirty. */ diff --git a/src/libflake/include/nix/flake/meson.build b/src/libflake/include/nix/flake/meson.build index fc580164eae..fbe54f41208 100644 --- a/src/libflake/include/nix/flake/meson.build +++ b/src/libflake/include/nix/flake/meson.build @@ -6,6 +6,7 @@ headers = files( 'flake.hh', 'flakeref.hh', 'lockfile.hh', + 'provenance.hh', 'settings.hh', 'url-name.hh', ) diff --git a/src/libflake/include/nix/flake/provenance.hh b/src/libflake/include/nix/flake/provenance.hh new file mode 100644 index 00000000000..a4debc6b0c6 --- /dev/null +++ b/src/libflake/include/nix/flake/provenance.hh @@ -0,0 +1,19 @@ +#pragma once + +#include "nix/util/provenance.hh" + +namespace nix { + +struct FlakeProvenance : Provenance +{ + std::shared_ptr next; + std::string flakeOutput; + + FlakeProvenance(std::shared_ptr next, std::string flakeOutput) + : next(std::move(next)) + , flakeOutput(std::move(flakeOutput)) {}; + + nlohmann::json to_json() const override; +}; + +} // namespace nix diff --git a/src/libflake/meson.build b/src/libflake/meson.build index 58916ecd9ab..010b534abf8 100644 --- a/src/libflake/meson.build +++ b/src/libflake/meson.build @@ -45,6 +45,7 @@ sources = files( 'flake.cc', 'flakeref.cc', 'lockfile.cc', + 'provenance.cc', 'settings.cc', 'url-name.cc', ) diff --git a/src/libflake/provenance.cc b/src/libflake/provenance.cc new file mode 100644 index 00000000000..cd3441a7a90 --- /dev/null +++ b/src/libflake/provenance.cc @@ -0,0 +1,16 @@ +#include "nix/flake/provenance.hh" + +#include + +namespace nix { + +nlohmann::json FlakeProvenance::to_json() const +{ + return nlohmann::json{ + {"type", "flake"}, + {"next", next ? next->to_json() : nlohmann::json(nullptr)}, + {"flakeOutput", flakeOutput}, + }; +} + +} // namespace nix diff --git a/src/libstore-tests/data/dummy-store/one-flat-file.json b/src/libstore-tests/data/dummy-store/one-flat-file.json index 804bbf07da6..3dc6db9e92e 100644 --- a/src/libstore-tests/data/dummy-store/one-flat-file.json +++ b/src/libstore-tests/data/dummy-store/one-flat-file.json @@ -18,6 +18,7 @@ "deriver": null, "narHash": "sha256-f1eduuSIYC1BofXA1tycF79Ai2NSMJQtUErx5DxLYSU=", "narSize": 120, + "provenance": null, "references": [], "registrationTime": null, "signatures": [], diff --git a/src/libstore-tests/data/nar-info/json-1/impure.json b/src/libstore-tests/data/nar-info/json-1/impure.json index c6fafe13daa..b353fafa288 100644 --- a/src/libstore-tests/data/nar-info/json-1/impure.json +++ b/src/libstore-tests/data/nar-info/json-1/impure.json @@ -6,6 +6,7 @@ "downloadSize": 4029176, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, + "provenance": null, "references": [ "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "/nix/store/n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/nar-info/json-2/impure.json b/src/libstore-tests/data/nar-info/json-2/impure.json index b7b9f511827..9fbbc83586e 100644 --- a/src/libstore-tests/data/nar-info/json-2/impure.json +++ b/src/libstore-tests/data/nar-info/json-2/impure.json @@ -9,6 +9,7 @@ "downloadSize": 4029176, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, + "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/path-info/json-1/empty_impure.json b/src/libstore-tests/data/path-info/json-1/empty_impure.json index eb262899a60..47693711b46 100644 --- a/src/libstore-tests/data/path-info/json-1/empty_impure.json +++ b/src/libstore-tests/data/path-info/json-1/empty_impure.json @@ -3,6 +3,7 @@ "deriver": null, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 0, + "provenance": null, "references": [], "registrationTime": null, "signatures": [], diff --git a/src/libstore-tests/data/path-info/json-1/impure.json b/src/libstore-tests/data/path-info/json-1/impure.json index 04d1dedc2af..c4ec92c8352 100644 --- a/src/libstore-tests/data/path-info/json-1/impure.json +++ b/src/libstore-tests/data/path-info/json-1/impure.json @@ -3,6 +3,7 @@ "deriver": "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, + "provenance": null, "references": [ "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "/nix/store/n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/path-info/json-2/empty_impure.json b/src/libstore-tests/data/path-info/json-2/empty_impure.json index d22fbf7ec52..fe5bab9855a 100644 --- a/src/libstore-tests/data/path-info/json-2/empty_impure.json +++ b/src/libstore-tests/data/path-info/json-2/empty_impure.json @@ -3,6 +3,7 @@ "deriver": null, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 0, + "provenance": null, "references": [], "registrationTime": null, "signatures": [], diff --git a/src/libstore-tests/data/path-info/json-2/impure.json b/src/libstore-tests/data/path-info/json-2/impure.json index bed67610b1b..c4b7932dedc 100644 --- a/src/libstore-tests/data/path-info/json-2/impure.json +++ b/src/libstore-tests/data/path-info/json-2/impure.json @@ -6,6 +6,7 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, + "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.3.json b/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.3.json index 0f593f4248d..b2380a64648 100644 --- a/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.3.json +++ b/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.3.json @@ -4,6 +4,7 @@ "deriver": null, "narHash": "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "narSize": 34878, + "provenance": null, "references": [], "registrationTime": null, "signatures": [], @@ -16,6 +17,7 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "narSize": 34878, + "provenance": null, "references": [ "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo.drv" ], diff --git a/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.4.json b/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.4.json index 801f2040002..3793ad9de0f 100644 --- a/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.4.json +++ b/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.4.json @@ -4,6 +4,7 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, + "provenance": null, "references": [ "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo.drv" ], @@ -21,6 +22,7 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, + "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/worker-protocol/unkeyed-valid-path-info-1.15.json b/src/libstore-tests/data/worker-protocol/unkeyed-valid-path-info-1.15.json index 9cc53c6804e..dfb48b5ef36 100644 --- a/src/libstore-tests/data/worker-protocol/unkeyed-valid-path-info-1.15.json +++ b/src/libstore-tests/data/worker-protocol/unkeyed-valid-path-info-1.15.json @@ -4,6 +4,7 @@ "deriver": null, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, + "provenance": null, "references": [], "registrationTime": 23423, "signatures": [], @@ -16,6 +17,7 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, + "provenance": null, "references": [ "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo.drv" ], diff --git a/src/libstore-tests/data/worker-protocol/valid-path-info-1.15.json b/src/libstore-tests/data/worker-protocol/valid-path-info-1.15.json index 427c286ddfb..cded582772f 100644 --- a/src/libstore-tests/data/worker-protocol/valid-path-info-1.15.json +++ b/src/libstore-tests/data/worker-protocol/valid-path-info-1.15.json @@ -5,6 +5,7 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + "provenance": null, "references": [], "registrationTime": 23423, "signatures": [], @@ -18,6 +19,7 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo" diff --git a/src/libstore-tests/data/worker-protocol/valid-path-info-1.16.json b/src/libstore-tests/data/worker-protocol/valid-path-info-1.16.json index f980d842174..034e2a64e5b 100644 --- a/src/libstore-tests/data/worker-protocol/valid-path-info-1.16.json +++ b/src/libstore-tests/data/worker-protocol/valid-path-info-1.16.json @@ -5,6 +5,7 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + "provenance": null, "references": [], "registrationTime": 23423, "signatures": [], @@ -18,6 +19,7 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo" @@ -40,6 +42,7 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo", + "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore/async-path-writer.cc b/src/libstore/async-path-writer.cc index 3271e7926a8..925c52ea371 100644 --- a/src/libstore/async-path-writer.cc +++ b/src/libstore/async-path-writer.cc @@ -1,5 +1,6 @@ #include "nix/store/async-path-writer.hh" #include "nix/util/archive.hh" +#include "nix/util/provenance.hh" #include #include @@ -18,6 +19,7 @@ struct AsyncPathWriterImpl : AsyncPathWriter Hash hash; StorePathSet references; RepairFlag repair; + std::shared_ptr provenance; std::promise promise; }; @@ -69,8 +71,13 @@ struct AsyncPathWriterImpl : AsyncPathWriter workerThread.join(); } - StorePath - addPath(std::string contents, std::string name, StorePathSet references, RepairFlag repair, bool readOnly) override + StorePath addPath( + std::string contents, + std::string name, + StorePathSet references, + RepairFlag repair, + bool readOnly, + std::shared_ptr provenance) override { auto hash = hashString(HashAlgorithm::SHA256, contents); @@ -93,6 +100,7 @@ struct AsyncPathWriterImpl : AsyncPathWriter .hash = hash, .references = std::move(references), .repair = repair, + .provenance = provenance, .promise = std::move(promise), }); wakeupCV.notify_all(); @@ -159,7 +167,8 @@ struct AsyncPathWriterImpl : AsyncPathWriter ContentAddressMethod::Raw::Text, HashAlgorithm::SHA256, item.references, - item.repair); + item.repair, + item.provenance); assert(storePath == item.storePath); } } diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc index 848669ae84f..e189e604e2c 100644 --- a/src/libstore/binary-cache-store.cc +++ b/src/libstore/binary-cache-store.cc @@ -315,7 +315,8 @@ StorePath BinaryCacheStore::addToStoreFromDump( ContentAddressMethod hashMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair) + RepairFlag repair, + std::shared_ptr provenance) { std::optional caHash; std::string nar; @@ -378,6 +379,7 @@ StorePath BinaryCacheStore::addToStoreFromDump( }), nar.hash); info.narSize = nar.numBytesDigested; + info.provenance = provenance; return info; }) ->path; @@ -503,6 +505,7 @@ StorePath BinaryCacheStore::addToStore( }), nar.hash); info.narSize = nar.numBytesDigested; + info.provenance = path.getProvenance(); return info; }) ->path; diff --git a/src/libstore/build/derivation-building-goal.cc b/src/libstore/build/derivation-building-goal.cc index 10ba0e78b77..e6cbc734994 100644 --- a/src/libstore/build/derivation-building-goal.cc +++ b/src/libstore/build/derivation-building-goal.cc @@ -617,8 +617,11 @@ Goal::Co DerivationBuildingGoal::tryToBuild() co_return doneFailure(std::move(e)); } + auto info = worker.evalStore.maybeQueryPathInfo(drvPath); + DerivationBuilderParams params{ .drvPath = drvPath, + .drvProvenance = info ? info->provenance : nullptr, .buildResult = buildResult, .drv = *drv, .drvOptions = drvOptions, diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index 1a5d683c865..8dc185e4554 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -127,16 +127,22 @@ static auto infoForDerivation(Store & store, const Derivation & drv) }; } -StorePath writeDerivation(Store & store, const Derivation & drv, RepairFlag repair, bool readOnly) +StorePath writeDerivation( + Store & store, + const Derivation & drv, + RepairFlag repair, + bool readOnly, + std::shared_ptr provenance) { if (readOnly || settings.readOnlyMode) { auto [_x, _y, _z, path] = infoForDerivation(store, drv); return path; } else - return store.writeDerivation(drv, repair); + return store.writeDerivation(drv, repair, provenance); } -StorePath Store::writeDerivation(const Derivation & drv, RepairFlag repair) +StorePath +Store::writeDerivation(const Derivation & drv, RepairFlag repair, std::shared_ptr provenance) { auto [suffix, contents, references, path] = infoForDerivation(*this, drv); @@ -151,14 +157,20 @@ StorePath Store::writeDerivation(const Derivation & drv, RepairFlag repair) ContentAddressMethod::Raw::Text, HashAlgorithm::SHA256, references, - repair); + repair, + provenance); assert(path2 == path); return path; } StorePath writeDerivation( - Store & store, AsyncPathWriter & asyncPathWriter, const Derivation & drv, RepairFlag repair, bool readOnly) + Store & store, + AsyncPathWriter & asyncPathWriter, + const Derivation & drv, + RepairFlag repair, + bool readOnly, + std::shared_ptr provenance) { auto references = drv.inputSrcs; for (auto & i : drv.inputDrvs.map) @@ -168,7 +180,8 @@ StorePath writeDerivation( std::string(drv.name) + drvExtension, references, repair, - readOnly || settings.readOnlyMode); + readOnly || settings.readOnlyMode, + provenance); } namespace { diff --git a/src/libstore/dummy-store.cc b/src/libstore/dummy-store.cc index 375ed7b2d24..425fab6800e 100644 --- a/src/libstore/dummy-store.cc +++ b/src/libstore/dummy-store.cc @@ -216,7 +216,9 @@ struct DummyStoreImpl : DummyStore if (info.path.isDerivation()) { warn("back compat supporting `addToStore` for inserting derivations in dummy store"); writeDerivation( - parseDerivation(*this, accessor->readFile(CanonPath::root), Derivation::nameFromPath(info.path))); + parseDerivation(*this, accessor->readFile(CanonPath::root), Derivation::nameFromPath(info.path)), + repair, + info.provenance); return; } @@ -233,11 +235,12 @@ struct DummyStoreImpl : DummyStore StorePath addToStoreFromDump( Source & source, std::string_view name, - FileSerialisationMethod dumpMethod = FileSerialisationMethod::NixArchive, - ContentAddressMethod hashMethod = FileIngestionMethod::NixArchive, - HashAlgorithm hashAlgo = HashAlgorithm::SHA256, - const StorePathSet & references = StorePathSet(), - RepairFlag repair = NoRepair) override + FileSerialisationMethod dumpMethod, + ContentAddressMethod hashMethod, + HashAlgorithm hashAlgo, + const StorePathSet & references, + RepairFlag repair, + std::shared_ptr provenance) override { if (isDerivation(name)) throw Error("Do not insert derivation into dummy store with `addToStoreFromDump`"); @@ -300,9 +303,10 @@ struct DummyStoreImpl : DummyStore return path; } - StorePath writeDerivation(const Derivation & drv, RepairFlag repair = NoRepair) override + StorePath + writeDerivation(const Derivation & drv, RepairFlag repair, std::shared_ptr provenance) override { - auto drvPath = ::nix::writeDerivation(*this, drv, repair, /*readonly=*/true); + auto drvPath = ::nix::writeDerivation(*this, drv, repair, /*readonly=*/true, provenance); if (!derivations.contains(drvPath) || repair) { if (config->readOnly) diff --git a/src/libstore/include/nix/store/async-path-writer.hh b/src/libstore/include/nix/store/async-path-writer.hh index 80997dc6ac2..d64418479bc 100644 --- a/src/libstore/include/nix/store/async-path-writer.hh +++ b/src/libstore/include/nix/store/async-path-writer.hh @@ -7,7 +7,12 @@ namespace nix { struct AsyncPathWriter { virtual StorePath addPath( - std::string contents, std::string name, StorePathSet references, RepairFlag repair, bool readOnly = false) = 0; + std::string contents, + std::string name, + StorePathSet references, + RepairFlag repair, + bool readOnly = false, + std::shared_ptr provenance = {}) = 0; virtual void waitForPath(const StorePath & path) = 0; diff --git a/src/libstore/include/nix/store/binary-cache-store.hh b/src/libstore/include/nix/store/binary-cache-store.hh index e7b3d07ebb6..eb1304875f5 100644 --- a/src/libstore/include/nix/store/binary-cache-store.hh +++ b/src/libstore/include/nix/store/binary-cache-store.hh @@ -100,6 +100,11 @@ protected: public: + bool isUsefulProvenance() override + { + return true; + } + virtual bool fileExists(const std::string & path) = 0; virtual void upsertFile( @@ -183,7 +188,8 @@ public: ContentAddressMethod hashMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair) override; + RepairFlag repair, + std::shared_ptr provenance) override; StorePath addToStore( std::string_view name, diff --git a/src/libstore/include/nix/store/build/derivation-builder.hh b/src/libstore/include/nix/store/build/derivation-builder.hh index c51424d0ea6..6d3749d8f5f 100644 --- a/src/libstore/include/nix/store/build/derivation-builder.hh +++ b/src/libstore/include/nix/store/build/derivation-builder.hh @@ -57,6 +57,11 @@ struct DerivationBuilderParams /** The path of the derivation. */ const StorePath & drvPath; + /** + * The provenance of the derivation, if known + */ + const std::shared_ptr drvProvenance; + BuildResult & buildResult; /** diff --git a/src/libstore/include/nix/store/derivations.hh b/src/libstore/include/nix/store/derivations.hh index e4c3e29e877..3b07072d99d 100644 --- a/src/libstore/include/nix/store/derivations.hh +++ b/src/libstore/include/nix/store/derivations.hh @@ -18,6 +18,7 @@ namespace nix { struct StoreDirConfig; struct AsyncPathWriter; +struct Provenance; /* Abstract syntax of derivations. */ @@ -456,7 +457,12 @@ class Store; /** * Write a derivation to the Nix store, and return its path. */ -StorePath writeDerivation(Store & store, const Derivation & drv, RepairFlag repair = NoRepair, bool readOnly = false); +StorePath writeDerivation( + Store & store, + const Derivation & drv, + RepairFlag repair = NoRepair, + bool readOnly = false, + std::shared_ptr provenance = nullptr); /** * Asynchronously write a derivation to the Nix store, and return its path. @@ -466,7 +472,8 @@ StorePath writeDerivation( AsyncPathWriter & asyncPathWriter, const Derivation & drv, RepairFlag repair = NoRepair, - bool readOnly = false); + bool readOnly = false, + std::shared_ptr provenance = nullptr); /** * Read a derivation from a file. diff --git a/src/libstore/include/nix/store/legacy-ssh-store.hh b/src/libstore/include/nix/store/legacy-ssh-store.hh index 994918f90f0..0d54d9cb8ac 100644 --- a/src/libstore/include/nix/store/legacy-ssh-store.hh +++ b/src/libstore/include/nix/store/legacy-ssh-store.hh @@ -73,6 +73,11 @@ struct LegacySSHStore : public virtual Store ref openConnection(); + bool isUsefulProvenance() override + { + return true; + } + void queryPathInfoUncached( const StorePath & path, Callback> callback) noexcept override; @@ -112,11 +117,12 @@ struct LegacySSHStore : public virtual Store StorePath addToStoreFromDump( Source & dump, std::string_view name, - FileSerialisationMethod dumpMethod = FileSerialisationMethod::NixArchive, - ContentAddressMethod hashMethod = FileIngestionMethod::NixArchive, - HashAlgorithm hashAlgo = HashAlgorithm::SHA256, - const StorePathSet & references = StorePathSet(), - RepairFlag repair = NoRepair) override + FileSerialisationMethod dumpMethod, + ContentAddressMethod hashMethod, + HashAlgorithm hashAlgo, + const StorePathSet & references, + RepairFlag repair, + std::shared_ptr provenance) override { unsupported("addToStore"); } diff --git a/src/libstore/include/nix/store/local-store.hh b/src/libstore/include/nix/store/local-store.hh index fd457c2d3be..40aa7c699e7 100644 --- a/src/libstore/include/nix/store/local-store.hh +++ b/src/libstore/include/nix/store/local-store.hh @@ -250,7 +250,8 @@ public: ContentAddressMethod hashMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair) override; + RepairFlag repair, + std::shared_ptr provenance) override; void addTempRoot(const StorePath & path) override; diff --git a/src/libstore/include/nix/store/meson.build b/src/libstore/include/nix/store/meson.build index 91bce9ba9b9..f99cf39040c 100644 --- a/src/libstore/include/nix/store/meson.build +++ b/src/libstore/include/nix/store/meson.build @@ -69,6 +69,7 @@ headers = [ config_pub_h ] + files( 'pathlocks.hh', 'posix-fs-canonicalise.hh', 'profiles.hh', + 'provenance.hh', 'realisation.hh', 'references.hh', 'remote-fs-accessor.hh', diff --git a/src/libstore/include/nix/store/path-info.hh b/src/libstore/include/nix/store/path-info.hh index dbcd933f426..374b90a26ae 100644 --- a/src/libstore/include/nix/store/path-info.hh +++ b/src/libstore/include/nix/store/path-info.hh @@ -13,6 +13,7 @@ namespace nix { class Store; struct StoreDirConfig; +struct Provenance; /** * JSON format version for path info output. @@ -123,6 +124,12 @@ struct UnkeyedValidPathInfo */ std::optional ca; + /** + * The provenance of this store path, i.e. a link back to the Nix + * expression used to create it. + */ + std::shared_ptr provenance; + UnkeyedValidPathInfo(const UnkeyedValidPathInfo & other) = default; UnkeyedValidPathInfo(const StoreDirConfig & store, Hash narHash); diff --git a/src/libstore/include/nix/store/provenance.hh b/src/libstore/include/nix/store/provenance.hh new file mode 100644 index 00000000000..371ee4f6426 --- /dev/null +++ b/src/libstore/include/nix/store/provenance.hh @@ -0,0 +1,60 @@ +#pragma once + +#include "nix/util/provenance.hh" +#include "nix/store/path.hh" +#include "nix/store/outputs-spec.hh" + +namespace nix { + +struct DerivationProvenance : Provenance +{ + /** + * The derivation that built this path. + */ + StorePath drvPath; + + /** + * The output of the derivation that corresponds to this path. + */ + OutputName output; + + /** + * The provenance of the derivation, if known. + */ + std::shared_ptr next; + + // FIXME: do we need anything extra for CA derivations? + + DerivationProvenance(const StorePath & drvPath, const OutputName & output, std::shared_ptr next) + : drvPath(drvPath) + , output(output) + , next(std::move(next)) + { + } + + nlohmann::json to_json() const override; +}; + +struct CopiedProvenance : Provenance +{ + /** + * Store URL (typically a binary cache) from which this store + * path was copied. + */ + std::string from; + + /** + * Provenance of the store path in the upstream store, if any. + */ + std::shared_ptr next; + + CopiedProvenance(std::string_view from, std::shared_ptr next) + : from(from) + , next(std::move(next)) + { + } + + nlohmann::json to_json() const override; +}; + +} // namespace nix diff --git a/src/libstore/include/nix/store/remote-store.hh b/src/libstore/include/nix/store/remote-store.hh index 1244eeec001..3644c55f206 100644 --- a/src/libstore/include/nix/store/remote-store.hh +++ b/src/libstore/include/nix/store/remote-store.hh @@ -82,7 +82,8 @@ struct RemoteStore : public virtual Store, ContentAddressMethod caMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair); + RepairFlag repair, + std::shared_ptr provenance); /** * Add a content-addressable store path. `dump` will be drained. @@ -90,11 +91,12 @@ struct RemoteStore : public virtual Store, StorePath addToStoreFromDump( Source & dump, std::string_view name, - FileSerialisationMethod dumpMethod = FileSerialisationMethod::NixArchive, - ContentAddressMethod hashMethod = FileIngestionMethod::NixArchive, - HashAlgorithm hashAlgo = HashAlgorithm::SHA256, - const StorePathSet & references = StorePathSet(), - RepairFlag repair = NoRepair) override; + FileSerialisationMethod dumpMethod, + ContentAddressMethod hashMethod, + HashAlgorithm hashAlgo, + const StorePathSet & references, + RepairFlag repair, + std::shared_ptr provenance) override; void addToStore(const ValidPathInfo & info, Source & nar, RepairFlag repair, CheckSigsFlag checkSigs) override; diff --git a/src/libstore/include/nix/store/store-api.hh b/src/libstore/include/nix/store/store-api.hh index e74bee09550..e4d38faf3ec 100644 --- a/src/libstore/include/nix/store/store-api.hh +++ b/src/libstore/include/nix/store/store-api.hh @@ -43,6 +43,8 @@ struct SourceAccessor; class NarInfoDiskCache; class Store; +struct Provenance; + typedef std::map OutputPathMap; enum CheckSigsFlag : bool { NoCheckSigs = false, CheckSigs = true }; @@ -597,7 +599,8 @@ public: ContentAddressMethod hashMethod = ContentAddressMethod::Raw::NixArchive, HashAlgorithm hashAlgo = HashAlgorithm::SHA256, const StorePathSet & references = StorePathSet(), - RepairFlag repair = NoRepair) = 0; + RepairFlag repair = NoRepair, + std::shared_ptr provenance = nullptr) = 0; /** * Add a mapping indicating that `deriver!outputName` maps to the output path @@ -790,7 +793,8 @@ public: /** * Write a derivation to the Nix store, and return its path. */ - virtual StorePath writeDerivation(const Derivation & drv, RepairFlag repair = NoRepair); + virtual StorePath writeDerivation( + const Derivation & drv, RepairFlag repair = NoRepair, std::shared_ptr provenance = nullptr); /** * Read a derivation (which must already be valid). @@ -915,6 +919,15 @@ public: return {}; } + /** + * Whether, when copying *from* this store, a "copied" provenance + * record should be added. + */ + virtual bool isUsefulProvenance() + { + return false; + } + protected: Stats stats; diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 3c9ae14f03f..554790bb771 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -20,6 +20,7 @@ #include "nix/util/users.hh" #include "nix/store/store-open.hh" #include "nix/store/store-registration.hh" +#include "nix/util/provenance.hh" #include #include @@ -336,13 +337,13 @@ LocalStore::LocalStore(ref config) /* Prepare SQL statements. */ state->stmts->RegisterValidPath.create( state->db, - "insert into ValidPaths (path, hash, registrationTime, deriver, narSize, ultimate, sigs, ca) values (?, ?, ?, ?, ?, ?, ?, ?);"); + "insert into ValidPaths (path, hash, registrationTime, deriver, narSize, ultimate, sigs, ca, provenance) values (?, ?, ?, ?, ?, ?, ?, ?, ?);"); state->stmts->UpdatePathInfo.create( state->db, "update ValidPaths set narSize = ?, hash = ?, ultimate = ?, sigs = ?, ca = ? where path = ?;"); state->stmts->AddReference.create(state->db, "insert or replace into Refs (referrer, reference) values (?, ?);"); state->stmts->QueryPathInfo.create( state->db, - "select id, hash, registrationTime, deriver, narSize, ultimate, sigs, ca from ValidPaths where path = ?;"); + "select id, hash, registrationTime, deriver, narSize, ultimate, sigs, ca, provenance from ValidPaths where path = ?;"); state->stmts->QueryReferences.create( state->db, "select path from Refs join ValidPaths on reference = id where referrer = ?;"); state->stmts->QueryReferrers.create( @@ -597,6 +598,8 @@ void LocalStore::upgradeDBSchema(State & state) "20220326-ca-derivations", #include "ca-specific-schema.sql.gen.hh" ); + + doUpgrade("20241024-provenance", "alter table ValidPaths add column provenance text"); } /* To improve purity, users may want to make the Nix store a read-only @@ -697,7 +700,8 @@ uint64_t LocalStore::addValidPath(State & state, const ValidPathInfo & info, boo info.registrationTime == 0 ? time(0) : info.registrationTime)( info.deriver ? printStorePath(*info.deriver) : "", (bool) info.deriver)(info.narSize, info.narSize != 0)(info.ultimate ? 1 : 0, info.ultimate)( - concatStringsSep(" ", info.sigs), !info.sigs.empty())(renderContentAddress(info.ca), (bool) info.ca) + concatStringsSep(" ", info.sigs), !info.sigs.empty())(renderContentAddress(info.ca), (bool) info.ca)( + info.provenance ? info.provenance->to_json_str() : "", (bool) info.provenance) .exec(); uint64_t id = state.db.getLastInsertedRowId(); @@ -788,6 +792,10 @@ std::shared_ptr LocalStore::queryPathInfoInternal(State & s while (useQueryReferences.next()) info->references.insert(parseStorePath(useQueryReferences.getStr(0))); + auto prov = (const char *) sqlite3_column_text(state.stmts->QueryPathInfo, 8); + if (prov) + info->provenance = Provenance::from_json_str(prov); + return info; } @@ -1169,7 +1177,8 @@ StorePath LocalStore::addToStoreFromDump( ContentAddressMethod hashMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair) + RepairFlag repair, + std::shared_ptr provenance) { /* For computing the store path. */ auto hashSink = std::make_unique(hashAlgo); @@ -1313,6 +1322,7 @@ StorePath LocalStore::addToStoreFromDump( auto info = ValidPathInfo::makeFromCA(*this, name, std::move(desc), narHash.hash); info.narSize = narHash.numBytesDigested; + info.provenance = provenance; registerValidPath(info); } diff --git a/src/libstore/meson.build b/src/libstore/meson.build index 6556adc27d8..3beb0fa6cf8 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -331,6 +331,7 @@ sources = files( 'pathlocks.cc', 'posix-fs-canonicalise.cc', 'profiles.cc', + 'provenance.cc', 'realisation.cc', 'references.cc', 'remote-fs-accessor.cc', diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index 27ed5a76143..0128fc7321b 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -3,6 +3,7 @@ #include "nix/store/store-api.hh" #include "nix/util/strings.hh" #include "nix/util/json-utils.hh" +#include "nix/util/provenance.hh" namespace nix { @@ -84,7 +85,8 @@ NarInfo::NarInfo(const StoreDirConfig & store, const std::string & s, const std: throw corrupt("extra CA"); // FIXME: allow blank ca or require skipping field? ca = ContentAddress::parseOpt(value); - } + } else if (name == "Provenance") + provenance = Provenance::from_json_str(value); pos = eol + 1; line += 1; @@ -129,6 +131,9 @@ std::string NarInfo::to_string(const StoreDirConfig & store) const if (ca) res += "CA: " + renderContentAddress(*ca) + "\n"; + if (provenance) + res += "Provenance: " + provenance->to_json_str() + "\n"; + return res; } diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index ebab52cec81..6f8d81a6fb1 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -5,6 +5,7 @@ #include "nix/util/json-utils.hh" #include "nix/util/comparator.hh" #include "nix/util/strings.hh" +#include "nix/util/provenance.hh" namespace nix { @@ -214,6 +215,8 @@ UnkeyedValidPathInfo::toJSON(const StoreDirConfig * store, bool includeImpureInf auto & sigsObj = jsonObject["signatures"] = json::array(); for (auto & sig : sigs) sigsObj.push_back(sig); + + jsonObject["provenance"] = provenance ? provenance->to_json() : nullptr; } return jsonObject; @@ -289,6 +292,10 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON(const StoreDirConfig * store if (auto * rawSignatures = optionalValueAt(json, "signatures")) res.sigs = getStringSet(*rawSignatures); + auto prov = json.find("provenance"); + if (prov != json.end() && !prov->second.is_null()) + res.provenance = Provenance::from_json(prov->second); + return res; } diff --git a/src/libstore/provenance.cc b/src/libstore/provenance.cc new file mode 100644 index 00000000000..867944dcaed --- /dev/null +++ b/src/libstore/provenance.cc @@ -0,0 +1,35 @@ +#include "nix/store/provenance.hh" +#include "nix/util/json-utils.hh" + +namespace nix { + +nlohmann::json DerivationProvenance::to_json() const +{ + return nlohmann::json{ + {"type", "derivation"}, + {"drv", drvPath.to_string()}, + {"output", output}, + {"next", next ? next->to_json() : nlohmann::json(nullptr)}, + }; +} + +nlohmann::json CopiedProvenance::to_json() const +{ + nlohmann::json j{ + {"type", "copied"}, + {"from", from}, + }; + if (next) + j["next"] = next->to_json(); + return j; +} + +Provenance::Register registerCopiedProvenance("copied", [](nlohmann::json json) { + auto & obj = getObject(json); + std::shared_ptr next; + if (auto prov = optionalValueAt(obj, "next")) + next = Provenance::from_json(*prov); + return make_ref(getString(valueAt(obj, "from")), next); +}); + +} // namespace nix \ No newline at end of file diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 91ff48a76c1..e3491d94c36 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -311,8 +311,12 @@ ref RemoteStore::addCAToStore( ContentAddressMethod caMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair) + RepairFlag repair, + std::shared_ptr provenance) { + if (provenance) + throw UnimplementedError("RemoteStore::addToStore() with provenance"); + std::optional conn_(getConnection()); auto & conn = *conn_; @@ -398,7 +402,8 @@ StorePath RemoteStore::addToStoreFromDump( ContentAddressMethod hashMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair) + RepairFlag repair, + std::shared_ptr provenance) { FileSerialisationMethod fsm; switch (hashMethod.getFileIngestionMethod()) { @@ -417,13 +422,16 @@ StorePath RemoteStore::addToStoreFromDump( } if (fsm != dumpMethod) unsupported("RemoteStore::addToStoreFromDump doesn't support this `dumpMethod` `hashMethod` combination"); - auto storePath = addCAToStore(dump, name, hashMethod, hashAlgo, references, repair)->path; + auto storePath = addCAToStore(dump, name, hashMethod, hashAlgo, references, repair, provenance)->path; invalidatePathInfoCacheFor(storePath); return storePath; } void RemoteStore::addToStore(const ValidPathInfo & info, Source & source, RepairFlag repair, CheckSigsFlag checkSigs) { + if (info.provenance) + throw UnimplementedError("RemoteStore::addToStore() with provenance"); + auto conn(getConnection()); conn->to << WorkerProto::Op::AddToStoreNar; diff --git a/src/libstore/restricted-store.cc b/src/libstore/restricted-store.cc index ef8aaa3801d..e4602f69c06 100644 --- a/src/libstore/restricted-store.cc +++ b/src/libstore/restricted-store.cc @@ -98,7 +98,8 @@ struct RestrictedStore : public virtual IndirectRootStore, public virtual GcStor ContentAddressMethod hashMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair) override; + RepairFlag repair, + std::shared_ptr provenance) override; void narFromPath(const StorePath & path, Sink & sink) override; @@ -215,9 +216,10 @@ StorePath RestrictedStore::addToStoreFromDump( ContentAddressMethod hashMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair) + RepairFlag repair, + std::shared_ptr provenance) { - auto path = next->addToStoreFromDump(dump, name, dumpMethod, hashMethod, hashAlgo, references, repair); + auto path = next->addToStoreFromDump(dump, name, dumpMethod, hashMethod, hashAlgo, references, repair, provenance); goal.addDependency(path); return path; } diff --git a/src/libstore/ssh-store.cc b/src/libstore/ssh-store.cc index dc70b4ba8de..08b9b1ee2ee 100644 --- a/src/libstore/ssh-store.cc +++ b/src/libstore/ssh-store.cc @@ -54,6 +54,11 @@ struct alignas(8) /* Work around ASAN failures on i686-linux. */ { } + bool isUsefulProvenance() override + { + return true; + } + // FIXME extend daemon protocol, move implementation to RemoteStore std::optional getBuildLogExact(const StorePath & path) override { diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 5abaee7355e..862178aaa38 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -18,14 +18,13 @@ // `addMultipleToStore`. #include "nix/store/worker-protocol.hh" #include "nix/util/signals.hh" +#include "nix/store/provenance.hh" #include #include #include "nix/util/strings.hh" -using json = nlohmann::json; - namespace nix { Path StoreConfigBase::getDefaultNixStoreDir() @@ -109,7 +108,8 @@ StorePath Store::addToStore( std::optional storePath; auto sink = sourceToSink([&](Source & source) { LengthSource lengthSource(source); - storePath = addToStoreFromDump(lengthSource, name, fsm, method, hashAlgo, references, repair); + storePath = + addToStoreFromDump(lengthSource, name, fsm, method, hashAlgo, references, repair, path.getProvenance()); if (settings.warnLargePathThreshold && lengthSource.total >= settings.warnLargePathThreshold) { static bool failOnLargePath = getEnv("_NIX_TEST_FAIL_ON_LARGE_PATH").value_or("") == "1"; if (failOnLargePath) @@ -295,6 +295,7 @@ ValidPathInfo Store::addToStoreSlow( }), narHash); info.narSize = narSize; + info.provenance = srcPath.getProvenance(); if (!isValidPath(info.path)) { auto source = sinkToSource([&](Sink & scratchpadSink) { srcPath.dumpPath(scratchpadSink); }); @@ -871,6 +872,19 @@ makeCopyPathMessage(const StoreConfig & srcCfg, const StoreConfig & dstCfg, std: "copying path '%s' from '%s' to '%s'", storePath, srcCfg.getHumanReadableURI(), dstCfg.getHumanReadableURI()); } +/** + * Wrap upstream provenance in a "copied" provenance record to record + * where the path was copied from. But uninformative origins like + * LocalStore are omitted. + */ +static std::shared_ptr +addCopiedProvenance(std::shared_ptr provenance, Store & srcStore) +{ + if (!srcStore.isUsefulProvenance()) + return provenance; + return std::make_shared(srcStore.config.getHumanReadableURI(), provenance); +} + void copyStorePath( Store & srcStore, Store & dstStore, const StorePath & storePath, RepairFlag repair, CheckSigsFlag checkSigs) { @@ -890,25 +904,22 @@ void copyStorePath( {storePathS, srcCfg.getHumanReadableURI(), dstCfg.getHumanReadableURI()}); PushActivity pact(act.id); - auto info = srcStore.queryPathInfo(storePath); + auto srcInfo = srcStore.queryPathInfo(storePath); + auto info = make_ref(*srcInfo); uint64_t total = 0; // recompute store path on the chance dstStore does it differently if (info->ca && info->references.empty()) { - auto info2 = make_ref(*info); - info2->path = + info->path = dstStore.makeFixedOutputPathFromCA(info->path.name(), info->contentAddressWithReferences().value()); if (dstStore.storeDir == srcStore.storeDir) - assert(info->path == info2->path); - info = info2; + assert(info->path == srcInfo->path); } - if (info->ultimate) { - auto info2 = make_ref(*info); - info2->ultimate = false; - info = info2; - } + info->ultimate = false; + + info->provenance = addCopiedProvenance(info->provenance, srcStore); auto source = sinkToSource( [&](Sink & sink) { @@ -1035,6 +1046,7 @@ std::map copyPaths( ValidPathInfo infoForDst = *info; infoForDst.path = storePathForDst; + infoForDst.provenance = addCopiedProvenance(info->provenance, srcStore); auto source = sinkToSource([&, narSize = info->narSize](Sink & sink) { // We can reasonably assume that the copy will happen whenever we diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc index d097333d43f..b1a93a684c6 100644 --- a/src/libstore/unix/build/derivation-builder.cc +++ b/src/libstore/unix/build/derivation-builder.cc @@ -20,6 +20,7 @@ #include "nix/store/globals.hh" #include "nix/store/build/derivation-env-desugar.hh" #include "nix/util/terminal.hh" +#include "nix/store/provenance.hh" #include @@ -1863,6 +1864,8 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() newInfo.deriver = drvPath; newInfo.ultimate = true; + if (drvProvenance) + newInfo.provenance = std::make_shared(drvPath, outputName, drvProvenance); store.signPathInfo(newInfo); finish(newInfo.path); diff --git a/src/libutil/include/nix/util/meson.build b/src/libutil/include/nix/util/meson.build index b2d2dc8d316..5d26a25c034 100644 --- a/src/libutil/include/nix/util/meson.build +++ b/src/libutil/include/nix/util/meson.build @@ -59,6 +59,7 @@ headers = files( 'position.hh', 'posix-source-accessor.hh', 'processes.hh', + 'provenance.hh', 'ref.hh', 'regex-combinators.hh', 'repair-flag.hh', diff --git a/src/libutil/include/nix/util/provenance.hh b/src/libutil/include/nix/util/provenance.hh new file mode 100644 index 00000000000..65ac23bb677 --- /dev/null +++ b/src/libutil/include/nix/util/provenance.hh @@ -0,0 +1,55 @@ +#pragma once + +#include "nix/util/ref.hh" +#include "nix/util/canon-path.hh" + +#include + +#include + +namespace nix { + +struct Provenance +{ + static ref from_json_str(std::string_view); + + static ref from_json(const nlohmann::json & json); + + std::string to_json_str() const; + + virtual nlohmann::json to_json() const = 0; + +protected: + + using ProvenanceFactory = std::function(nlohmann::json)>; + + using RegisteredTypes = std::map; + + static RegisteredTypes & registeredTypes(); + +public: + + struct Register + { + Register(const std::string & type, ProvenanceFactory && factory) + { + registeredTypes().insert_or_assign(type, std::move(factory)); + } + }; +}; + +struct SubpathProvenance : public Provenance +{ + std::shared_ptr next; + CanonPath subpath; + + SubpathProvenance(std::shared_ptr next, const CanonPath & subpath) + : next(std::move(next)) + , subpath(subpath) + { + } + + nlohmann::json to_json() const override; +}; + +} // namespace nix diff --git a/src/libutil/include/nix/util/source-accessor.hh b/src/libutil/include/nix/util/source-accessor.hh index 1357cf79a28..b1948438f2e 100644 --- a/src/libutil/include/nix/util/source-accessor.hh +++ b/src/libutil/include/nix/util/source-accessor.hh @@ -9,6 +9,7 @@ namespace nix { struct Sink; +struct Provenance; /** * Note there is a decent chance this type soon goes away because the problem is solved another way. @@ -210,6 +211,13 @@ struct SourceAccessor : std::enable_shared_from_this return std::nullopt; } + std::shared_ptr provenance; + + /** + * Return the provenance of the specified path, or `nullptr` if not available. + */ + virtual std::shared_ptr getProvenance(const CanonPath & path); + /** * Invalidate any cached value the accessor may have for the specified path. */ diff --git a/src/libutil/include/nix/util/source-path.hh b/src/libutil/include/nix/util/source-path.hh index 4597de1ff46..2810d8c56d5 100644 --- a/src/libutil/include/nix/util/source-path.hh +++ b/src/libutil/include/nix/util/source-path.hh @@ -114,6 +114,11 @@ struct SourcePath return {accessor, accessor->resolveSymlinks(path, mode)}; } + std::shared_ptr getProvenance() const + { + return accessor->getProvenance(path); + } + void invalidateCache() const { accessor->invalidateCache(path); diff --git a/src/libutil/meson.build b/src/libutil/meson.build index 0b4a0841f90..d4b818b4285 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -152,6 +152,7 @@ sources = [ config_priv_h ] + files( 'pos-table.cc', 'position.cc', 'posix-source-accessor.cc', + 'provenance.cc', 'serialise.cc', 'signature/local-keys.cc', 'signature/signer.cc', diff --git a/src/libutil/mounted-source-accessor.cc b/src/libutil/mounted-source-accessor.cc index 13b77d2d1e1..22e5acf70b4 100644 --- a/src/libutil/mounted-source-accessor.cc +++ b/src/libutil/mounted-source-accessor.cc @@ -100,6 +100,12 @@ struct MountedSourceAccessorImpl : MountedSourceAccessor return accessor->getFingerprint(subpath); } + std::shared_ptr getProvenance(const CanonPath & path) override + { + auto [accessor, subpath] = resolve(path); + return accessor->getProvenance(subpath); + } + void invalidateCache(const CanonPath & path) override { auto [accessor, subpath] = resolve(path); diff --git a/src/libutil/provenance.cc b/src/libutil/provenance.cc new file mode 100644 index 00000000000..92097fff25e --- /dev/null +++ b/src/libutil/provenance.cc @@ -0,0 +1,61 @@ +#include "nix/util/provenance.hh" +#include "nix/util/json-utils.hh" + +namespace nix { + +struct UnknownProvenance : Provenance +{ + nlohmann::json payload; + + UnknownProvenance(nlohmann::json payload) + : payload(std::move(payload)) + { + } + + nlohmann::json to_json() const override + { + return payload; + } +}; + +Provenance::RegisteredTypes & Provenance::registeredTypes() +{ + static Provenance::RegisteredTypes types; + return types; +} + +ref Provenance::from_json_str(std::string_view s) +{ + return from_json(nlohmann::json::parse(s)); +} + +ref Provenance::from_json(const nlohmann::json & json) +{ + auto & obj = getObject(json); + + auto type = getString(valueAt(obj, "type")); + + auto it = registeredTypes().find(type); + if (it == registeredTypes().end()) + return make_ref(obj); + + return it->second(obj); +} + +std::string Provenance::to_json_str() const +{ + return to_json().dump(); +} + +nlohmann::json SubpathProvenance::to_json() const +{ + nlohmann::json j{ + {"type", "subpath"}, + {"subpath", subpath.abs()}, + }; + if (next) + j["next"] = next->to_json(); + return j; +} + +} // namespace nix diff --git a/src/libutil/source-accessor.cc b/src/libutil/source-accessor.cc index 3c2d658290c..76f3edc17a0 100644 --- a/src/libutil/source-accessor.cc +++ b/src/libutil/source-accessor.cc @@ -1,5 +1,7 @@ #include + #include "nix/util/source-accessor.hh" +#include "nix/util/provenance.hh" namespace nix { @@ -126,4 +128,9 @@ CanonPath SourceAccessor::resolveSymlinks(const CanonPath & path, SymlinkResolut return res; } +std::shared_ptr SourceAccessor::getProvenance(const CanonPath & path) +{ + return provenance && !path.isRoot() ? std::make_shared(provenance, path) : provenance; +} + } // namespace nix diff --git a/src/libutil/union-source-accessor.cc b/src/libutil/union-source-accessor.cc index ea5f77f64f2..7be670400b0 100644 --- a/src/libutil/union-source-accessor.cc +++ b/src/libutil/union-source-accessor.cc @@ -89,6 +89,16 @@ struct UnionSourceAccessor : SourceAccessor return {path, std::nullopt}; } + std::shared_ptr getProvenance(const CanonPath & path) override + { + for (auto & accessor : accessors) { + auto prov = accessor->getProvenance(path); + if (prov) + return prov; + } + return nullptr; + } + void invalidateCache(const CanonPath & path) override { for (auto & accessor : accessors) diff --git a/src/nix/flake.cc b/src/nix/flake.cc index e03581a25d0..f4e8052718b 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -1476,9 +1476,7 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON { auto originalRef = getFlakeRef(); auto resolvedRef = originalRef.resolve(fetchSettings, *store); - auto [accessor, lockedRef] = resolvedRef.lazyFetch(getEvalState()->fetchSettings, *store); - auto storePath = - fetchToStore(getEvalState()->fetchSettings, *store, accessor, FetchMode::Copy, lockedRef.input.getName()); + auto [storePath, accessor, lockedRef] = resolvedRef.input.fetchToStore(fetchSettings, *store); auto hash = store->queryPathInfo(storePath)->narHash; if (json) { diff --git a/src/nix/nario.cc b/src/nix/nario.cc index df9ae16340b..452c8c9ffaa 100644 --- a/src/nix/nario.cc +++ b/src/nix/nario.cc @@ -295,7 +295,8 @@ struct CmdNarioList : Command, MixJSON, MixLongListing ContentAddressMethod hashMethod, HashAlgorithm hashAlgo, const StorePathSet & references, - RepairFlag repair) override + RepairFlag repair, + std::shared_ptr provenance) override { unsupported("addToStoreFromDump"); } From 1d8d4307f334c7132435fdd93296b0c0e1762042 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 21 Jan 2026 12:31:19 +0100 Subject: [PATCH 03/17] Include provenance in resBuildResult log messages Example of a substitution event: { "action": "result", "id": 0, "payload": { "builtOutputs": {}, "path": "3bb116cnl86svn2lgc41a3i4a9qblgsf-libtool-2.4.7", "provenance": { "from": "https://cache.nixos.org", "type": "copied" }, "startTime": 0, "status": "Substituted", "stopTime": 0, "success": true, "timesBuilt": 0 }, "type": 110 } Example of a derivation event: { "action": "result", "id": 3381333262860569, "payload": { "builtOutputs": { "out": { "dependentRealisations": {}, "id": "sha256:deb37b0f322203d852a27010200f08e2dd739cb02b51d77999bd7f3162cdfe39!out", "outPath": "6b9w3gdjnbdvi50c0h0b9xg91hq6aryl-patchelf-0.18.0", "signatures": [] } }, "cpuSystem": 2118013, "cpuUser": 11471586, "path": { "drvPath": "fm8zrgh4dazysyz3imcva658h0iv34k0-patchelf-0.18.0.drv", "outputs": [ "*" ] }, "provenance": { "flakeOutput": "packages.x86_64-linux.default", "next": { "attrs": { "dirtyRev": "bb2f1eb3c1e4dc9c4523642a3e39d55806fc9a81-dirty", "dirtyShortRev": "bb2f1eb-dirty", "lastModified": 1768573749, "type": "git", "url": "file:///home/eelco/Dev/patchelf" }, "type": "tree" }, "type": "flake" }, "startTime": 1768993105, "status": "Built", "stopTime": 1768993120, "success": true, "timesBuilt": 1 }, "type": 110 } --- src/libstore/build-result.cc | 12 +++++++++++ .../build/derivation-building-goal.cc | 17 +++++++++------ src/libstore/build/substitution-goal.cc | 21 ++++++++++++------- .../include/nix/store/build-result.hh | 13 ++++++++++++ .../store/build/derivation-building-goal.hh | 5 ++++- .../nix/store/build/substitution-goal.hh | 2 +- src/libstore/include/nix/store/store-api.hh | 5 +++-- src/libstore/store-api.cc | 8 ++++--- 8 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/libstore/build-result.cc b/src/libstore/build-result.cc index 4967b64423d..ad54819ee34 100644 --- a/src/libstore/build-result.cc +++ b/src/libstore/build-result.cc @@ -1,5 +1,7 @@ #include "nix/store/build-result.hh" #include "nix/util/json-utils.hh" +#include "nix/util/provenance.hh" + #include namespace nix { @@ -105,12 +107,14 @@ void adl_serializer::to_json(json & res, const BuildResult & br) res["success"] = true; res["status"] = BuildResult::Success::statusToString(success.status); res["builtOutputs"] = success.builtOutputs; + res["provenance"] = success.provenance ? success.provenance->to_json() : nlohmann::json(nullptr); }, [&](const BuildResult::Failure & failure) { res["success"] = false; res["status"] = BuildResult::Failure::statusToString(failure.status); res["errorMsg"] = failure.errorMsg; res["isNonDeterministic"] = failure.isNonDeterministic; + res["provenance"] = failure.provenance ? failure.provenance->to_json() : nlohmann::json(nullptr); }, }, br.inner); @@ -138,16 +142,24 @@ BuildResult adl_serializer::from_json(const json & _json) bool success = getBoolean(valueAt(json, "success")); std::string statusStr = getString(valueAt(json, "status")); + auto provenanceFromJson = [](const nlohmann::json * j) -> std::shared_ptr { + if (j && !j->is_null()) + return Provenance::from_json(*j); + return nullptr; + }; + if (success) { BuildResult::Success s; s.status = successStatusFromString(statusStr); s.builtOutputs = valueAt(json, "builtOutputs"); + s.provenance = provenanceFromJson(optionalValueAt(json, "provenance")); br.inner = std::move(s); } else { BuildResult::Failure f; f.status = failureStatusFromString(statusStr); f.errorMsg = getString(valueAt(json, "errorMsg")); f.isNonDeterministic = getBoolean(valueAt(json, "isNonDeterministic")); + f.provenance = provenanceFromJson(optionalValueAt(json, "provenance")); br.inner = std::move(f); } diff --git a/src/libstore/build/derivation-building-goal.cc b/src/libstore/build/derivation-building-goal.cc index e6cbc734994..c7a902811b3 100644 --- a/src/libstore/build/derivation-building-goal.cc +++ b/src/libstore/build/derivation-building-goal.cc @@ -440,6 +440,11 @@ Goal::Co DerivationBuildingGoal::tryToBuild() actLock.reset(); + /* Get the provenance of the derivation, if available. */ + std::shared_ptr provenance; + if (auto info = worker.evalStore.maybeQueryPathInfo(drvPath)) + provenance = info->provenance; + if (useHook) { buildResult.startTime = time(0); // inexact started(); @@ -522,7 +527,7 @@ Goal::Co DerivationBuildingGoal::tryToBuild() outputLocks.setDeletion(true); outputLocks.unlock(); - co_return doneSuccess(BuildResult::Success::Built, std::move(builtOutputs)); + co_return doneSuccess(BuildResult::Success::Built, std::move(builtOutputs), provenance); } co_await yield(); @@ -617,11 +622,9 @@ Goal::Co DerivationBuildingGoal::tryToBuild() co_return doneFailure(std::move(e)); } - auto info = worker.evalStore.maybeQueryPathInfo(drvPath); - DerivationBuilderParams params{ .drvPath = drvPath, - .drvProvenance = info ? info->provenance : nullptr, + .drvProvenance = provenance, .buildResult = buildResult, .drv = *drv, .drvOptions = drvOptions, @@ -728,7 +731,7 @@ Goal::Co DerivationBuildingGoal::tryToBuild() (unlinked) lock files. */ outputLocks.setDeletion(true); outputLocks.unlock(); - co_return doneSuccess(BuildResult::Success::Built, std::move(builtOutputs)); + co_return doneSuccess(BuildResult::Success::Built, std::move(builtOutputs), provenance); } #endif } @@ -1178,11 +1181,13 @@ DerivationBuildingGoal::checkPathValidity(std::map & return {allValid, validOutputs}; } -Goal::Done DerivationBuildingGoal::doneSuccess(BuildResult::Success::Status status, SingleDrvOutputs builtOutputs) +Goal::Done DerivationBuildingGoal::doneSuccess( + BuildResult::Success::Status status, SingleDrvOutputs builtOutputs, std::shared_ptr provenance) { buildResult.inner = BuildResult::Success{ .status = status, .builtOutputs = std::move(builtOutputs), + .provenance = provenance, }; logger->result( diff --git a/src/libstore/build/substitution-goal.cc b/src/libstore/build/substitution-goal.cc index b2e321c7238..3e29a9884c4 100644 --- a/src/libstore/build/substitution-goal.cc +++ b/src/libstore/build/substitution-goal.cc @@ -29,10 +29,12 @@ PathSubstitutionGoal::~PathSubstitutionGoal() cleanup(); } -Goal::Done PathSubstitutionGoal::doneSuccess(BuildResult::Success::Status status) +Goal::Done +PathSubstitutionGoal::doneSuccess(BuildResult::Success::Status status, std::shared_ptr provenance) { buildResult.inner = BuildResult::Success{ .status = status, + .provenance = provenance, }; logger->result( @@ -67,7 +69,7 @@ Goal::Co PathSubstitutionGoal::init() /* If the path already exists we're done. */ if (!repair && worker.store.isValidPath(storePath)) { - co_return doneSuccess(BuildResult::Success::AlreadyValid); + co_return doneSuccess(BuildResult::Success::AlreadyValid, nullptr); } if (settings.readOnlyMode) @@ -237,7 +239,7 @@ Goal::Co PathSubstitutionGoal::tryToRun( outPipe.createAsyncPipe(worker.ioport.get()); #endif - auto promise = std::promise(); + auto promise = std::promise>(); thr = std::thread([this, &promise, &subPath, &sub]() { try { @@ -252,9 +254,8 @@ Goal::Co PathSubstitutionGoal::tryToRun( Logger::Fields{worker.store.printStorePath(storePath), sub->config.getHumanReadableURI()}); PushActivity pact(act.id); - copyStorePath(*sub, worker.store, subPath, repair, sub->config.isTrusted ? NoCheckSigs : CheckSigs); - - promise.set_value(); + promise.set_value( + copyStorePath(*sub, worker.store, subPath, repair, sub->config.isTrusted ? NoCheckSigs : CheckSigs)); } catch (...) { promise.set_exception(std::current_exception()); } @@ -279,8 +280,12 @@ Goal::Co PathSubstitutionGoal::tryToRun( thr.join(); worker.childTerminated(this); + std::shared_ptr provenance; + try { - promise.get_future().get(); + auto info = promise.get_future().get(); + if (info) + provenance = info->provenance; } catch (std::exception & e) { /* Cause the parent build to fail unless --fallback is given, or the substitute has disappeared. The latter case behaves @@ -321,7 +326,7 @@ Goal::Co PathSubstitutionGoal::tryToRun( worker.updateProgress(); - co_return doneSuccess(BuildResult::Success::Substituted); + co_return doneSuccess(BuildResult::Success::Substituted, provenance); } void PathSubstitutionGoal::handleEOF(Descriptor fd) diff --git a/src/libstore/include/nix/store/build-result.hh b/src/libstore/include/nix/store/build-result.hh index bbf4de6310a..e6cbd1f73cc 100644 --- a/src/libstore/include/nix/store/build-result.hh +++ b/src/libstore/include/nix/store/build-result.hh @@ -11,6 +11,8 @@ namespace nix { +struct Provenance; + struct BuildResult { struct Success @@ -38,6 +40,12 @@ struct BuildResult */ SingleDrvOutputs builtOutputs; + /** + * The provenance of the derivation, if any. Note that this is the provenance of the current build, not + * necessarily of previously existing outputs. + */ + std::shared_ptr provenance; + bool operator==(const BuildResult::Success &) const noexcept; std::strong_ordering operator<=>(const BuildResult::Success &) const noexcept; @@ -97,6 +105,11 @@ struct BuildResult */ bool isNonDeterministic = false; + /** + * The provenance of the derivation, if any. + */ + std::shared_ptr provenance; + bool operator==(const BuildResult::Failure &) const noexcept; std::strong_ordering operator<=>(const BuildResult::Failure &) const noexcept; diff --git a/src/libstore/include/nix/store/build/derivation-building-goal.hh b/src/libstore/include/nix/store/build/derivation-building-goal.hh index be95c796b05..12a38a55686 100644 --- a/src/libstore/include/nix/store/build/derivation-building-goal.hh +++ b/src/libstore/include/nix/store/build/derivation-building-goal.hh @@ -155,7 +155,10 @@ private: */ void killChild(); - Done doneSuccess(BuildResult::Success::Status status, SingleDrvOutputs builtOutputs); + Done doneSuccess( + BuildResult::Success::Status status, + SingleDrvOutputs builtOutputs, + std::shared_ptr provenance = nullptr); Done doneFailure(BuildError ex); diff --git a/src/libstore/include/nix/store/build/substitution-goal.hh b/src/libstore/include/nix/store/build/substitution-goal.hh index 5f33b9aa5d7..1b7d956a1a2 100644 --- a/src/libstore/include/nix/store/build/substitution-goal.hh +++ b/src/libstore/include/nix/store/build/substitution-goal.hh @@ -41,7 +41,7 @@ struct PathSubstitutionGoal : public Goal */ std::optional ca; - Done doneSuccess(BuildResult::Success::Status status); + Done doneSuccess(BuildResult::Success::Status status, std::shared_ptr provenance); Done doneFailure(ExitCode result, BuildResult::Failure::Status status, std::string errorMsg); diff --git a/src/libstore/include/nix/store/store-api.hh b/src/libstore/include/nix/store/store-api.hh index e4d38faf3ec..dd74bf2136a 100644 --- a/src/libstore/include/nix/store/store-api.hh +++ b/src/libstore/include/nix/store/store-api.hh @@ -946,9 +946,10 @@ protected: }; /** - * Copy a path from one store to another. + * Copy a path from one store to another. Return the path info of the newly added store path, or nullptr if the path was + * already valid. */ -void copyStorePath( +std::shared_ptr copyStorePath( Store & srcStore, Store & dstStore, const StorePath & storePath, diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 862178aaa38..454d4e03e03 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -885,13 +885,13 @@ addCopiedProvenance(std::shared_ptr provenance, Store & srcSto return std::make_shared(srcStore.config.getHumanReadableURI(), provenance); } -void copyStorePath( +std::shared_ptr copyStorePath( Store & srcStore, Store & dstStore, const StorePath & storePath, RepairFlag repair, CheckSigsFlag checkSigs) { /* Bail out early (before starting a download from srcStore) if dstStore already has this path. */ if (!repair && dstStore.isValidPath(storePath)) - return; + return nullptr; const auto & srcCfg = srcStore.config; const auto & dstCfg = dstStore.config; @@ -905,7 +905,7 @@ void copyStorePath( PushActivity pact(act.id); auto srcInfo = srcStore.queryPathInfo(storePath); - auto info = make_ref(*srcInfo); + auto info = std::make_shared(*srcInfo); uint64_t total = 0; @@ -938,6 +938,8 @@ void copyStorePath( }); dstStore.addToStore(*info, *source, repair, checkSigs); + + return info; } std::map copyPaths( From 1a638ee20ac079cb4394cdc91982d81dc2ed8458 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 21 Jan 2026 16:31:02 +0100 Subject: [PATCH 04/17] Include provenance in the daemon protocol --- src/libstore/daemon.cc | 11 +++++++++-- .../include/nix/store/worker-protocol-connection.hh | 2 ++ src/libstore/include/nix/store/worker-protocol.hh | 3 +++ src/libstore/remote-store.cc | 6 +++--- src/libstore/worker-protocol-connection.cc | 3 ++- src/libstore/worker-protocol.cc | 8 ++++++++ 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index f71f66db5d8..abfd620c4ed 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -18,6 +18,7 @@ #include "nix/util/logging.hh" #include "nix/store/globals.hh" #include "nix/store/active-builds.hh" +#include "nix/util/provenance.hh" #ifndef _WIN32 // TODO need graceful async exit support on Windows? # include "nix/util/monitor-fd.hh" @@ -433,6 +434,12 @@ static void performOp( bool repairBool; conn.from >> repairBool; auto repair = RepairFlag{repairBool}; + std::shared_ptr provenance; + if (conn.features.contains(WorkerProto::featureProvenance)) { + auto s = readString(conn.from); + if (!s.empty()) + provenance = Provenance::from_json_str(s); + } logger->startWork(); auto pathInfo = [&]() { @@ -458,8 +465,8 @@ static void performOp( assert(false); } // TODO these two steps are essentially RemoteStore::addCAToStore. Move it up to Store. - auto path = - store->addToStoreFromDump(source, name, dumpMethod, contentAddressMethod, hashAlgo, refs, repair); + auto path = store->addToStoreFromDump( + source, name, dumpMethod, contentAddressMethod, hashAlgo, refs, repair, provenance); return store->queryPathInfo(path); }(); logger->stopWork(); diff --git a/src/libstore/include/nix/store/worker-protocol-connection.hh b/src/libstore/include/nix/store/worker-protocol-connection.hh index 31436395fe7..591e2cf09b5 100644 --- a/src/libstore/include/nix/store/worker-protocol-connection.hh +++ b/src/libstore/include/nix/store/worker-protocol-connection.hh @@ -41,6 +41,7 @@ struct WorkerProto::BasicConnection return WorkerProto::ReadConn{ .from = from, .version = protoVersion, + .provenance = features.contains(WorkerProto::featureProvenance), }; } @@ -57,6 +58,7 @@ struct WorkerProto::BasicConnection return WorkerProto::WriteConn{ .to = to, .version = protoVersion, + .provenance = features.contains(WorkerProto::featureProvenance), }; } }; diff --git a/src/libstore/include/nix/store/worker-protocol.hh b/src/libstore/include/nix/store/worker-protocol.hh index 36d918a3dc8..8ae2d261a9e 100644 --- a/src/libstore/include/nix/store/worker-protocol.hh +++ b/src/libstore/include/nix/store/worker-protocol.hh @@ -68,6 +68,7 @@ struct WorkerProto Source & from; Version version; bool shortStorePaths = false; + bool provenance = false; }; /** @@ -79,6 +80,7 @@ struct WorkerProto Sink & to; Version version; bool shortStorePaths = false; + bool provenance = false; }; /** @@ -140,6 +142,7 @@ struct WorkerProto using FeatureSet = std::set>; static constexpr std::string_view featureQueryActiveBuilds{"queryActiveBuilds"}; + static constexpr std::string_view featureProvenance{"provenance"}; static const FeatureSet allFeatures; }; diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index e3491d94c36..5df87ea9118 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -18,6 +18,7 @@ #include "nix/util/callback.hh" #include "nix/store/filetransfer.hh" #include "nix/util/signals.hh" +#include "nix/util/provenance.hh" #include @@ -314,9 +315,6 @@ ref RemoteStore::addCAToStore( RepairFlag repair, std::shared_ptr provenance) { - if (provenance) - throw UnimplementedError("RemoteStore::addToStore() with provenance"); - std::optional conn_(getConnection()); auto & conn = *conn_; @@ -325,6 +323,8 @@ ref RemoteStore::addCAToStore( conn->to << WorkerProto::Op::AddToStore << name << caMethod.renderWithAlgo(hashAlgo); WorkerProto::write(*this, *conn, references); conn->to << repair; + if (conn->features.contains(WorkerProto::featureProvenance)) + conn->to << (provenance ? provenance->to_json_str() : ""); // The dump source may invoke the store, so we need to make some room. connections->incCapacity(); diff --git a/src/libstore/worker-protocol-connection.cc b/src/libstore/worker-protocol-connection.cc index 24d1ea82395..7f41b0c47e7 100644 --- a/src/libstore/worker-protocol-connection.cc +++ b/src/libstore/worker-protocol-connection.cc @@ -5,7 +5,8 @@ namespace nix { -const WorkerProto::FeatureSet WorkerProto::allFeatures{{std::string(WorkerProto::featureQueryActiveBuilds)}}; +const WorkerProto::FeatureSet WorkerProto::allFeatures{ + {std::string(WorkerProto::featureQueryActiveBuilds), std::string(WorkerProto::featureProvenance)}}; WorkerProto::BasicClientConnection::~BasicClientConnection() { diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index 2788222c0d7..d9bcd7dfdac 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -7,6 +7,7 @@ #include "nix/store/worker-protocol-impl.hh" #include "nix/util/archive.hh" #include "nix/store/path-info.hh" +#include "nix/util/provenance.hh" #include #include @@ -303,6 +304,11 @@ UnkeyedValidPathInfo WorkerProto::Serialise::read(const St info.sigs = readStrings(conn.from); info.ca = ContentAddress::parseOpt(readString(conn.from)); } + if (conn.provenance) { + auto s = readString(conn.from); + if (!s.empty()) + info.provenance = Provenance::from_json_str(s); + } return info; } @@ -316,6 +322,8 @@ void WorkerProto::Serialise::write( if (GET_PROTOCOL_MINOR(conn.version) >= 16) { conn.to << pathInfo.ultimate << pathInfo.sigs << renderContentAddress(pathInfo.ca); } + if (conn.provenance) + conn.to << (pathInfo.provenance ? pathInfo.provenance->to_json_str() : ""); } WorkerProto::ClientHandshakeInfo From 8c9d6b2db97aaeb51d6b252524d82acaef695fc4 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 21 Jan 2026 16:57:36 +0100 Subject: [PATCH 05/17] Add experimental feature to enable provenance --- src/libstore/daemon.cc | 6 +++- src/libstore/local-store.cc | 34 +++++++++++-------- src/libstore/nar-info.cc | 2 +- src/libstore/path-info.cc | 11 +++--- src/libstore/remote-store.cc | 5 ++- src/libutil/experimental-features.cc | 10 +++++- .../include/nix/util/experimental-features.hh | 1 + 7 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index abfd620c4ed..e872d68d1c4 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -1047,8 +1047,12 @@ void processConnection(ref store, FdSource && from, FdSink && to, Trusted #endif /* Exchange the greeting. */ + auto myFeatures = WorkerProto::allFeatures; + if (!experimentalFeatureSettings.isEnabled(Xp::Provenance)) + myFeatures.erase(std::string(WorkerProto::featureProvenance)); + auto [protoVersion, features] = - WorkerProto::BasicServerConnection::handshake(to, from, PROTOCOL_VERSION, WorkerProto::allFeatures); + WorkerProto::BasicServerConnection::handshake(to, from, PROTOCOL_VERSION, myFeatures); if (protoVersion < MINIMUM_PROTOCOL_VERSION) throw Error("the Nix client version is too old"); diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 554790bb771..047fb30f84a 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -337,13 +337,16 @@ LocalStore::LocalStore(ref config) /* Prepare SQL statements. */ state->stmts->RegisterValidPath.create( state->db, - "insert into ValidPaths (path, hash, registrationTime, deriver, narSize, ultimate, sigs, ca, provenance) values (?, ?, ?, ?, ?, ?, ?, ?, ?);"); + fmt("insert into ValidPaths (path, hash, registrationTime, deriver, narSize, ultimate, sigs, ca%s) values (?, ?, ?, ?, ?, ?, ?, ?%s);", + experimentalFeatureSettings.isEnabled(Xp::Provenance) ? ", provenance" : "", + experimentalFeatureSettings.isEnabled(Xp::Provenance) ? ", ?" : "")); state->stmts->UpdatePathInfo.create( state->db, "update ValidPaths set narSize = ?, hash = ?, ultimate = ?, sigs = ?, ca = ? where path = ?;"); state->stmts->AddReference.create(state->db, "insert or replace into Refs (referrer, reference) values (?, ?);"); state->stmts->QueryPathInfo.create( state->db, - "select id, hash, registrationTime, deriver, narSize, ultimate, sigs, ca, provenance from ValidPaths where path = ?;"); + fmt("select id, hash, registrationTime, deriver, narSize, ultimate, sigs, ca%s from ValidPaths where path = ?;", + experimentalFeatureSettings.isEnabled(Xp::Provenance) ? ", provenance" : "")); state->stmts->QueryReferences.create( state->db, "select path from Refs join ValidPaths on reference = id where referrer = ?;"); state->stmts->QueryReferrers.create( @@ -599,7 +602,8 @@ void LocalStore::upgradeDBSchema(State & state) #include "ca-specific-schema.sql.gen.hh" ); - doUpgrade("20241024-provenance", "alter table ValidPaths add column provenance text"); + if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) + doUpgrade("20241024-provenance", "alter table ValidPaths add column provenance text"); } /* To improve purity, users may want to make the Nix store a read-only @@ -695,14 +699,14 @@ uint64_t LocalStore::addValidPath(State & state, const ValidPathInfo & info, boo "cannot add path '%s' to the Nix store because it claims to be content-addressed but isn't", printStorePath(info.path)); - state.stmts->RegisterValidPath - .use()(printStorePath(info.path))(info.narHash.to_string(HashFormat::Base16, true))( - info.registrationTime == 0 ? time(0) : info.registrationTime)( - info.deriver ? printStorePath(*info.deriver) : "", - (bool) info.deriver)(info.narSize, info.narSize != 0)(info.ultimate ? 1 : 0, info.ultimate)( - concatStringsSep(" ", info.sigs), !info.sigs.empty())(renderContentAddress(info.ca), (bool) info.ca)( - info.provenance ? info.provenance->to_json_str() : "", (bool) info.provenance) - .exec(); + auto query = state.stmts->RegisterValidPath.use()(printStorePath(info.path))( + info.narHash.to_string(HashFormat::Base16, true))(info.registrationTime == 0 ? time(0) : info.registrationTime)( + info.deriver ? printStorePath(*info.deriver) : "", + (bool) info.deriver)(info.narSize, info.narSize != 0)(info.ultimate ? 1 : 0, info.ultimate)( + concatStringsSep(" ", info.sigs), !info.sigs.empty())(renderContentAddress(info.ca), (bool) info.ca); + if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) + query(info.provenance ? info.provenance->to_json_str() : "", (bool) info.provenance); + query.exec(); uint64_t id = state.db.getLastInsertedRowId(); /* If this is a derivation, then store the derivation outputs in @@ -792,9 +796,11 @@ std::shared_ptr LocalStore::queryPathInfoInternal(State & s while (useQueryReferences.next()) info->references.insert(parseStorePath(useQueryReferences.getStr(0))); - auto prov = (const char *) sqlite3_column_text(state.stmts->QueryPathInfo, 8); - if (prov) - info->provenance = Provenance::from_json_str(prov); + if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) { + auto prov = (const char *) sqlite3_column_text(state.stmts->QueryPathInfo, 8); + if (prov) + info->provenance = Provenance::from_json_str(prov); + } return info; } diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index 0128fc7321b..6c2095b7bae 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -85,7 +85,7 @@ NarInfo::NarInfo(const StoreDirConfig & store, const std::string & s, const std: throw corrupt("extra CA"); // FIXME: allow blank ca or require skipping field? ca = ContentAddress::parseOpt(value); - } else if (name == "Provenance") + } else if (name == "Provenance" && experimentalFeatureSettings.isEnabled(Xp::Provenance)) provenance = Provenance::from_json_str(value); pos = eol + 1; diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index 6f8d81a6fb1..c5a9eba16c8 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -216,7 +216,8 @@ UnkeyedValidPathInfo::toJSON(const StoreDirConfig * store, bool includeImpureInf for (auto & sig : sigs) sigsObj.push_back(sig); - jsonObject["provenance"] = provenance ? provenance->to_json() : nullptr; + if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) + jsonObject["provenance"] = provenance ? provenance->to_json() : nullptr; } return jsonObject; @@ -292,9 +293,11 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON(const StoreDirConfig * store if (auto * rawSignatures = optionalValueAt(json, "signatures")) res.sigs = getStringSet(*rawSignatures); - auto prov = json.find("provenance"); - if (prov != json.end() && !prov->second.is_null()) - res.provenance = Provenance::from_json(prov->second); + if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) { + auto prov = json.find("provenance"); + if (prov != json.end() && !prov->second.is_null()) + res.provenance = Provenance::from_json(prov->second); + } return res; } diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 5df87ea9118..7c8d64d6fcc 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -72,8 +72,11 @@ void RemoteStore::initConnection(Connection & conn) StringSink saved; TeeSource tee(conn.from, saved); try { + auto myFeatures = WorkerProto::allFeatures; + if (!experimentalFeatureSettings.isEnabled(Xp::Provenance)) + myFeatures.erase(std::string(WorkerProto::featureProvenance)); auto [protoVersion, features] = - WorkerProto::BasicClientConnection::handshake(conn.to, tee, PROTOCOL_VERSION, WorkerProto::allFeatures); + WorkerProto::BasicClientConnection::handshake(conn.to, tee, PROTOCOL_VERSION, myFeatures); if (protoVersion < MINIMUM_PROTOCOL_VERSION) throw Error("the Nix daemon version is too old"); conn.protoVersion = protoVersion; diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index b606b3b0ac4..ad692ff3d24 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -25,7 +25,7 @@ struct ExperimentalFeatureDetails * feature, we either have no issue at all if few features are not added * at the end of the list, or a proper merge conflict if they are. */ -constexpr size_t numXpFeatures = 1 + static_cast(Xp::WasmDerivations); +constexpr size_t numXpFeatures = 1 + static_cast(Xp::Provenance); constexpr std::array xpFeatureDetails = {{ { @@ -339,6 +339,14 @@ constexpr std::array xpFeatureDetails )", .trackingUrl = "", }, + { + .tag = Xp::Provenance, + .name = "provenance", + .description = R"( + Enable keeping track of the provenance of store paths. + )", + .trackingUrl = "", + }, }}; static_assert( diff --git a/src/libutil/include/nix/util/experimental-features.hh b/src/libutil/include/nix/util/experimental-features.hh index 848fcab7a83..f8955ec8ca9 100644 --- a/src/libutil/include/nix/util/experimental-features.hh +++ b/src/libutil/include/nix/util/experimental-features.hh @@ -41,6 +41,7 @@ enum struct ExperimentalFeature { ParallelEval, WasmBuiltin, WasmDerivations, + Provenance, }; extern std::set stabilizedFeatures; From c8a845e3bdfdf0cf0beb8877057e466b78d48545 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 22 Jan 2026 15:09:14 +0100 Subject: [PATCH 06/17] Fix test --- src/libstore-tests/data/dummy-store/one-flat-file.json | 1 - src/libstore-tests/data/nar-info/json-1/impure.json | 1 - src/libstore-tests/data/nar-info/json-2/impure.json | 1 - src/libstore-tests/data/path-info/json-1/empty_impure.json | 1 - src/libstore-tests/data/path-info/json-1/impure.json | 1 - src/libstore-tests/data/path-info/json-2/empty_impure.json | 1 - src/libstore-tests/data/path-info/json-2/impure.json | 1 - .../data/serve-protocol/unkeyed-valid-path-info-2.3.json | 2 -- .../data/serve-protocol/unkeyed-valid-path-info-2.4.json | 2 -- .../data/worker-protocol/unkeyed-valid-path-info-1.15.json | 2 -- .../data/worker-protocol/valid-path-info-1.15.json | 2 -- .../data/worker-protocol/valid-path-info-1.16.json | 3 --- src/libstore/build-result.cc | 6 ++++-- src/libstore/nar-info.cc | 2 +- 14 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/libstore-tests/data/dummy-store/one-flat-file.json b/src/libstore-tests/data/dummy-store/one-flat-file.json index 3dc6db9e92e..804bbf07da6 100644 --- a/src/libstore-tests/data/dummy-store/one-flat-file.json +++ b/src/libstore-tests/data/dummy-store/one-flat-file.json @@ -18,7 +18,6 @@ "deriver": null, "narHash": "sha256-f1eduuSIYC1BofXA1tycF79Ai2NSMJQtUErx5DxLYSU=", "narSize": 120, - "provenance": null, "references": [], "registrationTime": null, "signatures": [], diff --git a/src/libstore-tests/data/nar-info/json-1/impure.json b/src/libstore-tests/data/nar-info/json-1/impure.json index b353fafa288..c6fafe13daa 100644 --- a/src/libstore-tests/data/nar-info/json-1/impure.json +++ b/src/libstore-tests/data/nar-info/json-1/impure.json @@ -6,7 +6,6 @@ "downloadSize": 4029176, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, - "provenance": null, "references": [ "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "/nix/store/n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/nar-info/json-2/impure.json b/src/libstore-tests/data/nar-info/json-2/impure.json index 9fbbc83586e..b7b9f511827 100644 --- a/src/libstore-tests/data/nar-info/json-2/impure.json +++ b/src/libstore-tests/data/nar-info/json-2/impure.json @@ -9,7 +9,6 @@ "downloadSize": 4029176, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, - "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/path-info/json-1/empty_impure.json b/src/libstore-tests/data/path-info/json-1/empty_impure.json index 47693711b46..eb262899a60 100644 --- a/src/libstore-tests/data/path-info/json-1/empty_impure.json +++ b/src/libstore-tests/data/path-info/json-1/empty_impure.json @@ -3,7 +3,6 @@ "deriver": null, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 0, - "provenance": null, "references": [], "registrationTime": null, "signatures": [], diff --git a/src/libstore-tests/data/path-info/json-1/impure.json b/src/libstore-tests/data/path-info/json-1/impure.json index c4ec92c8352..04d1dedc2af 100644 --- a/src/libstore-tests/data/path-info/json-1/impure.json +++ b/src/libstore-tests/data/path-info/json-1/impure.json @@ -3,7 +3,6 @@ "deriver": "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, - "provenance": null, "references": [ "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "/nix/store/n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/path-info/json-2/empty_impure.json b/src/libstore-tests/data/path-info/json-2/empty_impure.json index fe5bab9855a..d22fbf7ec52 100644 --- a/src/libstore-tests/data/path-info/json-2/empty_impure.json +++ b/src/libstore-tests/data/path-info/json-2/empty_impure.json @@ -3,7 +3,6 @@ "deriver": null, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 0, - "provenance": null, "references": [], "registrationTime": null, "signatures": [], diff --git a/src/libstore-tests/data/path-info/json-2/impure.json b/src/libstore-tests/data/path-info/json-2/impure.json index c4b7932dedc..bed67610b1b 100644 --- a/src/libstore-tests/data/path-info/json-2/impure.json +++ b/src/libstore-tests/data/path-info/json-2/impure.json @@ -6,7 +6,6 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, - "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.3.json b/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.3.json index b2380a64648..0f593f4248d 100644 --- a/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.3.json +++ b/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.3.json @@ -4,7 +4,6 @@ "deriver": null, "narHash": "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "narSize": 34878, - "provenance": null, "references": [], "registrationTime": null, "signatures": [], @@ -17,7 +16,6 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "narSize": 34878, - "provenance": null, "references": [ "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo.drv" ], diff --git a/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.4.json b/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.4.json index 3793ad9de0f..801f2040002 100644 --- a/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.4.json +++ b/src/libstore-tests/data/serve-protocol/unkeyed-valid-path-info-2.4.json @@ -4,7 +4,6 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, - "provenance": null, "references": [ "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo.drv" ], @@ -22,7 +21,6 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, - "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore-tests/data/worker-protocol/unkeyed-valid-path-info-1.15.json b/src/libstore-tests/data/worker-protocol/unkeyed-valid-path-info-1.15.json index dfb48b5ef36..9cc53c6804e 100644 --- a/src/libstore-tests/data/worker-protocol/unkeyed-valid-path-info-1.15.json +++ b/src/libstore-tests/data/worker-protocol/unkeyed-valid-path-info-1.15.json @@ -4,7 +4,6 @@ "deriver": null, "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, - "provenance": null, "references": [], "registrationTime": 23423, "signatures": [], @@ -17,7 +16,6 @@ "deriver": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, - "provenance": null, "references": [ "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo.drv" ], diff --git a/src/libstore-tests/data/worker-protocol/valid-path-info-1.15.json b/src/libstore-tests/data/worker-protocol/valid-path-info-1.15.json index cded582772f..427c286ddfb 100644 --- a/src/libstore-tests/data/worker-protocol/valid-path-info-1.15.json +++ b/src/libstore-tests/data/worker-protocol/valid-path-info-1.15.json @@ -5,7 +5,6 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", - "provenance": null, "references": [], "registrationTime": 23423, "signatures": [], @@ -19,7 +18,6 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", - "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo" diff --git a/src/libstore-tests/data/worker-protocol/valid-path-info-1.16.json b/src/libstore-tests/data/worker-protocol/valid-path-info-1.16.json index 034e2a64e5b..f980d842174 100644 --- a/src/libstore-tests/data/worker-protocol/valid-path-info-1.16.json +++ b/src/libstore-tests/data/worker-protocol/valid-path-info-1.16.json @@ -5,7 +5,6 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", - "provenance": null, "references": [], "registrationTime": 23423, "signatures": [], @@ -19,7 +18,6 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", - "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo" @@ -42,7 +40,6 @@ "narHash": "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc=", "narSize": 34878, "path": "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo", - "provenance": null, "references": [ "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo" diff --git a/src/libstore/build-result.cc b/src/libstore/build-result.cc index ad54819ee34..19080e0f19a 100644 --- a/src/libstore/build-result.cc +++ b/src/libstore/build-result.cc @@ -107,14 +107,16 @@ void adl_serializer::to_json(json & res, const BuildResult & br) res["success"] = true; res["status"] = BuildResult::Success::statusToString(success.status); res["builtOutputs"] = success.builtOutputs; - res["provenance"] = success.provenance ? success.provenance->to_json() : nlohmann::json(nullptr); + if (success.provenance) + res["provenance"] = success.provenance->to_json(); }, [&](const BuildResult::Failure & failure) { res["success"] = false; res["status"] = BuildResult::Failure::statusToString(failure.status); res["errorMsg"] = failure.errorMsg; res["isNonDeterministic"] = failure.isNonDeterministic; - res["provenance"] = failure.provenance ? failure.provenance->to_json() : nlohmann::json(nullptr); + if (failure.provenance) + res["provenance"] = failure.provenance->to_json(); }, }, br.inner); diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index 6c2095b7bae..ef72987699d 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -131,7 +131,7 @@ std::string NarInfo::to_string(const StoreDirConfig & store) const if (ca) res += "CA: " + renderContentAddress(*ca) + "\n"; - if (provenance) + if (provenance && experimentalFeatureSettings.isEnabled(Xp::Provenance)) res += "Provenance: " + provenance->to_json_str() + "\n"; return res; From 05e14c5bd0c37196a8388352bd48efa7ee9e27ba Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 22 Jan 2026 15:39:18 +0100 Subject: [PATCH 07/17] Fix test --- .../source/protocols/json/schema/store-object-info-v2.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml b/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml index 97c92fb6bf8..582b5e9eb47 100644 --- a/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml +++ b/doc/manual/source/protocols/json/schema/store-object-info-v2.yaml @@ -122,7 +122,6 @@ $defs: - registrationTime - ultimate - signatures - - provenance properties: version: { $ref: "#/$defs/base/properties/version" } path: { $ref: "#/$defs/base/properties/path" } @@ -216,7 +215,6 @@ $defs: - compression - downloadHash - downloadSize - - provenance properties: version: { $ref: "#/$defs/base/properties/version" } path: { $ref: "#/$defs/base/properties/path" } From 62e4fafde2f31ff5c6544228cf4089a1a829a7eb Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 22 Jan 2026 19:26:05 +0100 Subject: [PATCH 08/17] Implement RemoteStore::addToStore() --- src/libstore/daemon.cc | 5 +++++ src/libstore/remote-store.cc | 9 ++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index e872d68d1c4..36790671ec5 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -920,6 +920,11 @@ static void performOp( conn.from >> info.registrationTime >> info.narSize >> info.ultimate; info.sigs = readStrings(conn.from); info.ca = ContentAddress::parseOpt(readString(conn.from)); + if (conn.features.contains(WorkerProto::featureProvenance)) { + auto s = readString(conn.from); + if (!s.empty()) + info.provenance = Provenance::from_json_str(s); + } conn.from >> repair >> dontCheckSigs; if (!trusted && dontCheckSigs) dontCheckSigs = false; diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 7c8d64d6fcc..0274df18cbb 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -432,9 +432,6 @@ StorePath RemoteStore::addToStoreFromDump( void RemoteStore::addToStore(const ValidPathInfo & info, Source & source, RepairFlag repair, CheckSigsFlag checkSigs) { - if (info.provenance) - throw UnimplementedError("RemoteStore::addToStore() with provenance"); - auto conn(getConnection()); conn->to << WorkerProto::Op::AddToStoreNar; @@ -442,8 +439,10 @@ void RemoteStore::addToStore(const ValidPathInfo & info, Source & source, Repair WorkerProto::write(*this, *conn, info.deriver); conn->to << info.narHash.to_string(HashFormat::Base16, false); WorkerProto::write(*this, *conn, info.references); - conn->to << info.registrationTime << info.narSize << info.ultimate << info.sigs << renderContentAddress(info.ca) - << repair << !checkSigs; + conn->to << info.registrationTime << info.narSize << info.ultimate << info.sigs << renderContentAddress(info.ca); + if (conn->features.contains(WorkerProto::featureProvenance)) + conn->to << (info.provenance ? info.provenance->to_json_str() : ""); + conn->to << repair << !checkSigs; if (GET_PROTOCOL_MINOR(conn->protoVersion) >= 23) { conn.withFramedSink([&](Sink & sink) { copyNAR(source, sink); }); From e7453a4e9c219f115bdaab410cedb42cf52d5e08 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 22 Jan 2026 19:34:28 +0100 Subject: [PATCH 09/17] Cleanup --- src/libstore/daemon.cc | 17 ++++++----------- src/libstore/worker-protocol.cc | 7 ++----- src/libutil/include/nix/util/provenance.hh | 2 ++ src/libutil/provenance.cc | 7 +++++++ 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 36790671ec5..cd0e6fc6c56 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -434,12 +434,9 @@ static void performOp( bool repairBool; conn.from >> repairBool; auto repair = RepairFlag{repairBool}; - std::shared_ptr provenance; - if (conn.features.contains(WorkerProto::featureProvenance)) { - auto s = readString(conn.from); - if (!s.empty()) - provenance = Provenance::from_json_str(s); - } + auto provenance = conn.features.contains(WorkerProto::featureProvenance) + ? Provenance::from_json_str_optional(readString(conn.from)) + : nullptr; logger->startWork(); auto pathInfo = [&]() { @@ -920,11 +917,9 @@ static void performOp( conn.from >> info.registrationTime >> info.narSize >> info.ultimate; info.sigs = readStrings(conn.from); info.ca = ContentAddress::parseOpt(readString(conn.from)); - if (conn.features.contains(WorkerProto::featureProvenance)) { - auto s = readString(conn.from); - if (!s.empty()) - info.provenance = Provenance::from_json_str(s); - } + info.provenance = conn.features.contains(WorkerProto::featureProvenance) + ? Provenance::from_json_str_optional(readString(conn.from)) + : nullptr; conn.from >> repair >> dontCheckSigs; if (!trusted && dontCheckSigs) dontCheckSigs = false; diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index d9bcd7dfdac..6dc1f837102 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -304,11 +304,8 @@ UnkeyedValidPathInfo WorkerProto::Serialise::read(const St info.sigs = readStrings(conn.from); info.ca = ContentAddress::parseOpt(readString(conn.from)); } - if (conn.provenance) { - auto s = readString(conn.from); - if (!s.empty()) - info.provenance = Provenance::from_json_str(s); - } + if (conn.provenance) + info.provenance = Provenance::from_json_str_optional(readString(conn.from)); return info; } diff --git a/src/libutil/include/nix/util/provenance.hh b/src/libutil/include/nix/util/provenance.hh index 65ac23bb677..7085c250d46 100644 --- a/src/libutil/include/nix/util/provenance.hh +++ b/src/libutil/include/nix/util/provenance.hh @@ -13,6 +13,8 @@ struct Provenance { static ref from_json_str(std::string_view); + static std::shared_ptr from_json_str_optional(std::string_view); + static ref from_json(const nlohmann::json & json); std::string to_json_str() const; diff --git a/src/libutil/provenance.cc b/src/libutil/provenance.cc index 92097fff25e..1035c1d39fd 100644 --- a/src/libutil/provenance.cc +++ b/src/libutil/provenance.cc @@ -29,6 +29,13 @@ ref Provenance::from_json_str(std::string_view s) return from_json(nlohmann::json::parse(s)); } +std::shared_ptr Provenance::from_json_str_optional(std::string_view s) +{ + if (s.empty()) + return nullptr; + return Provenance::from_json_str(s); +} + ref Provenance::from_json(const nlohmann::json & json) { auto & obj = getObject(json); From 84e8f4badee8049a985aa59f89904abe198a616e Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 23 Jan 2026 15:16:32 +0100 Subject: [PATCH 10/17] Move provenance field --- src/libexpr/include/nix/expr/eval.hh | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/libexpr/include/nix/expr/eval.hh b/src/libexpr/include/nix/expr/eval.hh index 5c478adcef0..c7c54113c3f 100644 --- a/src/libexpr/include/nix/expr/eval.hh +++ b/src/libexpr/include/nix/expr/eval.hh @@ -1128,19 +1128,6 @@ private: friend struct Value; friend class ListBuilder; -public: - /** - * Worker threads manager. - * - * Note: keep this last to ensure that it's destroyed first, so we - * don't have any background work items (e.g. from - * `builtins.parallel`) referring to a partially destroyed - * `EvalState`. - */ - ref executor; - -private: - std::shared_ptr rootProvenance; public: @@ -1151,6 +1138,16 @@ public: void setRootProvenance(std::shared_ptr provenance); std::shared_ptr getRootProvenance(); + + /** + * Worker threads manager. + * + * Note: keep this last to ensure that it's destroyed first, so we + * don't have any background work items (e.g. from + * `builtins.parallel`) referring to a partially destroyed + * `EvalState`. + */ + ref executor; }; struct DebugTraceStacker From 89bb2f4244e946f7c0cda03f9bfc4b1ec3652954 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 23 Jan 2026 15:25:23 +0100 Subject: [PATCH 11/17] DummyStore: Include provenance --- src/libstore/dummy-store.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libstore/dummy-store.cc b/src/libstore/dummy-store.cc index 425fab6800e..32e95646a74 100644 --- a/src/libstore/dummy-store.cc +++ b/src/libstore/dummy-store.cc @@ -288,6 +288,7 @@ struct DummyStoreImpl : DummyStore std::move(narHash.first)); info.narSize = narHash.second.value(); + info.provenance = provenance; auto path = info.path; auto accessor = make_ref(std::move(*temp)); From c4265937e97249aae1ee3f175aa2b82a0e68b7e7 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 23 Jan 2026 15:30:15 +0100 Subject: [PATCH 12/17] Add Provenance destructor --- src/libstore/provenance.cc | 2 +- src/libutil/include/nix/util/provenance.hh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libstore/provenance.cc b/src/libstore/provenance.cc index 867944dcaed..7f614e8626b 100644 --- a/src/libstore/provenance.cc +++ b/src/libstore/provenance.cc @@ -32,4 +32,4 @@ Provenance::Register registerCopiedProvenance("copied", [](nlohmann::json json) return make_ref(getString(valueAt(obj, "from")), next); }); -} // namespace nix \ No newline at end of file +} // namespace nix diff --git a/src/libutil/include/nix/util/provenance.hh b/src/libutil/include/nix/util/provenance.hh index 7085c250d46..da8005b3181 100644 --- a/src/libutil/include/nix/util/provenance.hh +++ b/src/libutil/include/nix/util/provenance.hh @@ -11,6 +11,8 @@ namespace nix { struct Provenance { + virtual ~Provenance() = default; + static ref from_json_str(std::string_view); static std::shared_ptr from_json_str_optional(std::string_view); From b03ddaf7a417932c1956c425d5fe2be5a773baf1 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 23 Jan 2026 15:35:59 +0100 Subject: [PATCH 13/17] Rename "derivation" provenance to "build" Makes it clearer that this store path is not a derivation itself, but the result of building a derivation. --- src/libstore/include/nix/store/provenance.hh | 4 ++-- src/libstore/provenance.cc | 4 ++-- src/libstore/unix/build/derivation-builder.cc | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libstore/include/nix/store/provenance.hh b/src/libstore/include/nix/store/provenance.hh index 371ee4f6426..ee7771c0a33 100644 --- a/src/libstore/include/nix/store/provenance.hh +++ b/src/libstore/include/nix/store/provenance.hh @@ -6,7 +6,7 @@ namespace nix { -struct DerivationProvenance : Provenance +struct BuildProvenance : Provenance { /** * The derivation that built this path. @@ -25,7 +25,7 @@ struct DerivationProvenance : Provenance // FIXME: do we need anything extra for CA derivations? - DerivationProvenance(const StorePath & drvPath, const OutputName & output, std::shared_ptr next) + BuildProvenance(const StorePath & drvPath, const OutputName & output, std::shared_ptr next) : drvPath(drvPath) , output(output) , next(std::move(next)) diff --git a/src/libstore/provenance.cc b/src/libstore/provenance.cc index 7f614e8626b..bb67844a01f 100644 --- a/src/libstore/provenance.cc +++ b/src/libstore/provenance.cc @@ -3,10 +3,10 @@ namespace nix { -nlohmann::json DerivationProvenance::to_json() const +nlohmann::json BuildProvenance::to_json() const { return nlohmann::json{ - {"type", "derivation"}, + {"type", "build"}, {"drv", drvPath.to_string()}, {"output", output}, {"next", next ? next->to_json() : nlohmann::json(nullptr)}, diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc index b1a93a684c6..9cc5aff8164 100644 --- a/src/libstore/unix/build/derivation-builder.cc +++ b/src/libstore/unix/build/derivation-builder.cc @@ -1865,7 +1865,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() newInfo.deriver = drvPath; newInfo.ultimate = true; if (drvProvenance) - newInfo.provenance = std::make_shared(drvPath, outputName, drvProvenance); + newInfo.provenance = std::make_shared(drvPath, outputName, drvProvenance); store.signPathInfo(newInfo); finish(newInfo.path); @@ -1876,8 +1876,8 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() This is also good so that if a fixed-output produces the wrong path, we still store the result (just don't consider - the derivation sucessful, so if someone fixes the problem by - just changing the wanted hash, the redownload (or whateer + the derivation successful, so if someone fixes the problem by + just changing the wanted hash, the redownload (or whatever possibly quite slow thing it was) doesn't have to be done again. */ if (newInfo.ca) From 959bc7daf05286a15d83456acd365aa9a02319ee Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 23 Jan 2026 16:47:17 +0100 Subject: [PATCH 14/17] Rename isUsefulProvenance() -> includeInProvenance() --- src/libstore/include/nix/store/binary-cache-store.hh | 2 +- src/libstore/include/nix/store/legacy-ssh-store.hh | 2 +- src/libstore/include/nix/store/store-api.hh | 2 +- src/libstore/ssh-store.cc | 2 +- src/libstore/store-api.cc | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libstore/include/nix/store/binary-cache-store.hh b/src/libstore/include/nix/store/binary-cache-store.hh index eb1304875f5..4cb7b23f2d3 100644 --- a/src/libstore/include/nix/store/binary-cache-store.hh +++ b/src/libstore/include/nix/store/binary-cache-store.hh @@ -100,7 +100,7 @@ protected: public: - bool isUsefulProvenance() override + bool includeInProvenance() override { return true; } diff --git a/src/libstore/include/nix/store/legacy-ssh-store.hh b/src/libstore/include/nix/store/legacy-ssh-store.hh index 0d54d9cb8ac..6fd07779604 100644 --- a/src/libstore/include/nix/store/legacy-ssh-store.hh +++ b/src/libstore/include/nix/store/legacy-ssh-store.hh @@ -73,7 +73,7 @@ struct LegacySSHStore : public virtual Store ref openConnection(); - bool isUsefulProvenance() override + bool includeInProvenance() override { return true; } diff --git a/src/libstore/include/nix/store/store-api.hh b/src/libstore/include/nix/store/store-api.hh index dd74bf2136a..f7beb07c30c 100644 --- a/src/libstore/include/nix/store/store-api.hh +++ b/src/libstore/include/nix/store/store-api.hh @@ -923,7 +923,7 @@ public: * Whether, when copying *from* this store, a "copied" provenance * record should be added. */ - virtual bool isUsefulProvenance() + virtual bool includeInProvenance() { return false; } diff --git a/src/libstore/ssh-store.cc b/src/libstore/ssh-store.cc index 08b9b1ee2ee..1d69ca640f2 100644 --- a/src/libstore/ssh-store.cc +++ b/src/libstore/ssh-store.cc @@ -54,7 +54,7 @@ struct alignas(8) /* Work around ASAN failures on i686-linux. */ { } - bool isUsefulProvenance() override + bool includeInProvenance() override { return true; } diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 454d4e03e03..32ee071a95d 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -880,7 +880,7 @@ makeCopyPathMessage(const StoreConfig & srcCfg, const StoreConfig & dstCfg, std: static std::shared_ptr addCopiedProvenance(std::shared_ptr provenance, Store & srcStore) { - if (!srcStore.isUsefulProvenance()) + if (!srcStore.includeInProvenance()) return provenance; return std::make_shared(srcStore.config.getHumanReadableURI(), provenance); } From 06305fb69a1721bc58bc57d211489d0ed3374c99 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 23 Jan 2026 16:56:09 +0100 Subject: [PATCH 15/17] FetchurlProvenance: Remove sensitive URL data --- src/libfetchers/tarball.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index d5b62e74f19..9ad80bb06d0 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -104,7 +104,13 @@ DownloadFileResult downloadFile( }, hashString(HashAlgorithm::SHA256, sink.s)); info.narSize = sink.s.size(); - info.provenance = std::make_shared(url); + if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) { + auto sanitizedUrl = request.uri.parsed(); + if (sanitizedUrl.authority) + sanitizedUrl.authority->password.reset(); + sanitizedUrl.query.clear(); + info.provenance = std::make_shared(sanitizedUrl.to_string()); + } auto source = StringSource{sink.s}; store.addToStore(info, source, NoRepair, NoCheckSigs); storePath = std::move(info.path); From e4ab014cffd7d20f3b05337e43947a9ae93c8602 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Sun, 25 Jan 2026 22:27:18 +0100 Subject: [PATCH 16/17] FilteringSourceAccessor: Set subpath provenance correctly --- src/libfetchers/filtering-source-accessor.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libfetchers/filtering-source-accessor.cc b/src/libfetchers/filtering-source-accessor.cc index 17aed89dff3..68eb2156693 100644 --- a/src/libfetchers/filtering-source-accessor.cc +++ b/src/libfetchers/filtering-source-accessor.cc @@ -71,7 +71,7 @@ std::pair> FilteringSourceAccessor::getFin std::shared_ptr FilteringSourceAccessor::getProvenance(const CanonPath & path) { if (provenance) - return provenance; + return SourceAccessor::getProvenance(path); return next->getProvenance(prefix / path); } From a4734c60e5247d70aba2f094f9dddbc179ff8e60 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Sun, 25 Jan 2026 22:28:50 +0100 Subject: [PATCH 17/17] Add provenance test --- tests/functional/common/init.sh | 4 +- tests/functional/flakes/meson.build | 1 + tests/functional/flakes/provenance.sh | 121 ++++++++++++++++++++++++++ tests/functional/simple.nix | 1 + 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 tests/functional/flakes/provenance.sh diff --git a/tests/functional/common/init.sh b/tests/functional/common/init.sh index 7f28a09d753..98ae42cd4ce 100755 --- a/tests/functional/common/init.sh +++ b/tests/functional/common/init.sh @@ -12,7 +12,7 @@ if isTestOnNixOS; then ! test -e "$test_nix_conf" cat > "$test_nix_conf" < "$NIX_CONF_DIR"/nix.conf <