Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e096c4b
attrsToJSON(): Never serialize the __final attribute
edolstra Jan 12, 2026
607804e
Provenance tracking
edolstra Jan 8, 2026
1d8d430
Include provenance in resBuildResult log messages
edolstra Jan 21, 2026
1a638ee
Include provenance in the daemon protocol
edolstra Jan 21, 2026
8c9d6b2
Add experimental feature to enable provenance
edolstra Jan 21, 2026
c8a845e
Fix test
edolstra Jan 22, 2026
05e14c5
Fix test
edolstra Jan 22, 2026
62e4faf
Implement RemoteStore::addToStore()
edolstra Jan 22, 2026
e7453a4
Cleanup
edolstra Jan 22, 2026
84e8f4b
Move provenance field
edolstra Jan 23, 2026
89bb2f4
DummyStore: Include provenance
edolstra Jan 23, 2026
c426593
Add Provenance destructor
edolstra Jan 23, 2026
b03ddaf
Rename "derivation" provenance to "build"
edolstra Jan 23, 2026
959bc7d
Rename isUsefulProvenance() -> includeInProvenance()
edolstra Jan 23, 2026
06305fb
FetchurlProvenance: Remove sensitive URL data
edolstra Jan 23, 2026
e4ab014
FilteringSourceAccessor: Set subpath provenance correctly
edolstra Jan 25, 2026
a4734c6
Add provenance test
edolstra Jan 25, 2026
094f8dd
Merge remote-tracking branch 'detsys/main' into provenance-detsys
edolstra Feb 4, 2026
bdc94b2
Add EvalContext type for per-thread evaluation context
edolstra Feb 5, 2026
a344766
Add helper functions for propagating eval context
edolstra Feb 5, 2026
2458fc6
Handle the next field consistently
edolstra Feb 5, 2026
7e69413
Use getReference().render()
edolstra Feb 5, 2026
04fc9a5
CopiedProvenance: Handle null value for `next`
edolstra Feb 5, 2026
7f9efc6
Don't set provenance in impure mode
edolstra Feb 6, 2026
ec59646
Merge remote-tracking branch 'detsys/main' into provenance-detsys
edolstra Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions doc/manual/source/protocols/json/schema/store-object-info-v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,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:
Expand Down Expand Up @@ -262,4 +271,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
2 changes: 2 additions & 0 deletions src/libcmd/include/nix/cmd/installable-flake.hh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ struct InstallableFlake : InstallableValue
ref<flake::LockedFlake> getLockedFlake() const;

FlakeRef nixpkgsFlakeRef() const;

std::shared_ptr<const Provenance> makeProvenance(std::string_view attrPath) const;
};

/**
Expand Down
15 changes: 15 additions & 0 deletions src/libcmd/installable-flake.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <regex>
#include <queue>
Expand Down Expand Up @@ -84,6 +85,8 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths()

auto attrPath = attr->getAttrPathStr();

PushProvenance pushedProvenance(*state, makeProvenance(attrPath));

if (!attr->isDerivation()) {

// FIXME: use eval cache?
Expand Down Expand Up @@ -172,6 +175,8 @@ std::vector<ref<eval_cache::AttrCursor>> InstallableFlake::getCursors(EvalState
for (auto & attrPath : attrPaths) {
debug("trying flake output attribute '%s'", attrPath);

PushProvenance pushedProvenance(state, makeProvenance(attrPath));

auto attr = root->findAlongAttrPath(AttrPath::parse(state, attrPath));
if (attr) {
res.push_back(ref(*attr));
Expand Down Expand Up @@ -212,4 +217,14 @@ FlakeRef InstallableFlake::nixpkgsFlakeRef() const
return defaultNixpkgsFlakeRef();
}

std::shared_ptr<const Provenance> InstallableFlake::makeProvenance(std::string_view attrPath) const
{
if (!evalSettings.pureEval)
return nullptr;
auto provenance = getLockedFlake()->flake.provenance;
if (!provenance)
return nullptr;
return std::make_shared<const FlakeProvenance>(provenance, std::string(attrPath));
}

} // namespace nix
2 changes: 2 additions & 0 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ EvalMemory::EvalMemory()
assertGCInitialized();
}

thread_local EvalState::EvalContext EvalState::evalContext;

EvalState::EvalState(
const LookupPath & lookupPathFromArguments,
ref<Store> store,
Expand Down
60 changes: 59 additions & 1 deletion src/libexpr/include/nix/expr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ enum RepairFlag : bool;
struct MemorySourceAccessor;
struct MountedSourceAccessor;
struct AsyncPathWriter;
struct Provenance;
struct Executor;

namespace eval_cache {
class EvalCache;
}
struct Executor;

/**
* Increments a count on construction and decrements on destruction.
Expand Down Expand Up @@ -1128,6 +1129,45 @@ private:
friend class ListBuilder;

public:

/**
* Per-thread evaluation context. This context is propagated to worker threads when a value is evaluated
* asynchronously.
*/
struct EvalContext
{
std::shared_ptr<const Provenance> provenance;
};

thread_local static EvalContext evalContext;

/**
* Create a work item that propagates the current evaluation context.
*/
template<typename T>
auto makeWork(T && t)
{
return [this, t{std::move(t)}, evalContext(evalContext)]() {
this->evalContext = evalContext;
t();
};
}

/**
* Add a work item to the given work vector that propagates the current evaluation context.
*/
template<typename WorkItems, typename T>
void addWork(WorkItems & work, uint8_t priority, T && t)
{
work.emplace_back(makeWork(std::move(t)), priority);
}

template<typename FuturesVector, typename T>
void spawn(FuturesVector & futures, uint8_t priority, T && t)
{
futures.spawn(priority, makeWork(std::move(t)));
}

/**
* Worker threads manager.
*
Expand Down Expand Up @@ -1173,6 +1213,24 @@ SourcePath resolveExprPath(SourcePath path, bool addDefaultNix = true);
*/
bool isAllowedURI(std::string_view uri, const Strings & allowedPaths);

struct PushProvenance
{
EvalState & state;
std::shared_ptr<const Provenance> prev;

PushProvenance(EvalState & state, std::shared_ptr<const Provenance> prov)
: state(state)
{
state.evalContext.provenance.swap(prev);
state.evalContext.provenance.swap(prov);
}

~PushProvenance()
{
state.evalContext.provenance.swap(prev);
}
};

} // namespace nix

#include "nix/expr/eval-inline.hh"
6 changes: 4 additions & 2 deletions src/libexpr/include/nix/expr/parallel-eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ struct Executor

void worker();

std::vector<std::future<void>> spawn(std::vector<std::pair<work_t, uint8_t>> && items);
using WorkItems = std::vector<std::pair<Executor::work_t, uint8_t>>;

std::vector<std::future<void>> spawn(WorkItems && items);

static thread_local bool amWorkerThread;
};
Expand All @@ -77,7 +79,7 @@ struct FutureVector

// FIXME: add a destructor that cancels/waits for all futures.

void spawn(std::vector<std::pair<Executor::work_t, uint8_t>> && work);
void spawn(Executor::WorkItems && work);

void spawn(uint8_t prioPrefix, Executor::work_t && work)
{
Expand Down
9 changes: 5 additions & 4 deletions src/libexpr/parallel-eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ void Executor::worker()
}
}

std::vector<std::future<void>> Executor::spawn(std::vector<std::pair<work_t, uint8_t>> && items)
std::vector<std::future<void>> Executor::spawn(WorkItems && items)
{
if (items.empty())
return {};
Expand Down Expand Up @@ -146,7 +146,7 @@ FutureVector::~FutureVector()
}
}

void FutureVector::spawn(std::vector<std::pair<Executor::work_t, uint8_t>> && work)
void FutureVector::spawn(Executor::WorkItems && work)
{
auto futures = executor.spawn(std::move(work));
auto state(state_.lock());
Expand Down Expand Up @@ -273,10 +273,11 @@ static void prim_parallel(EvalState & state, const PosIdx pos, Value ** args, Va
state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.parallel");

if (state.executor->evalCores > 1) {
std::vector<std::pair<Executor::work_t, uint8_t>> work;
Executor::WorkItems work;
for (auto value : args[0]->listView())
if (!value->isFinished())
work.emplace_back([value(allocRootValue(value)), &state, pos]() { state.forceValue(**value, pos); }, 0);
state.addWork(
work, 0, [value(allocRootValue(value)), &state, pos]() { state.forceValue(**value, pos); });
state.executor->spawn(std::move(work));
}

Expand Down
6 changes: 4 additions & 2 deletions src/libexpr/primops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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.evalContext.provenance);
auto drvPathS = state.store->printStorePath(drvPath);

printMsg(lvlChatty, "instantiated '%1%' -> '%2%'", drvName, drvPathS);
Expand Down Expand Up @@ -2750,7 +2751,8 @@ static void prim_toFile(EvalState & state, const PosIdx pos, Value ** args, Valu
ContentAddressMethod::Raw::Text,
HashAlgorithm::SHA256,
refs,
state.repair);
state.repair,
state.evalContext.provenance);
});

/* Note: we don't need to add `context' to the context of the
Expand Down
7 changes: 4 additions & 3 deletions src/libexpr/value-to-json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ static void parallelForceDeep(EvalState & state, Value & v, PosIdx pos)
{
state.forceValue(v, pos);

std::vector<std::pair<Executor::work_t, uint8_t>> work;
Executor::WorkItems work;

switch (v.type()) {

Expand All @@ -29,8 +29,9 @@ static void parallelForceDeep(EvalState & state, Value & v, PosIdx pos)
if (v.attrs()->get(state.s.outPath))
return;
for (auto & a : *v.attrs())
work.emplace_back(
[value(allocRootValue(a.value)), pos(a.pos), &state]() { parallelForceDeep(state, **value, pos); }, 0);
state.addWork(work, 0, [value(allocRootValue(a.value)), pos(a.pos), &state]() {
parallelForceDeep(state, **value, pos);
});
break;
}

Expand Down
3 changes: 3 additions & 0 deletions src/libfetchers/attrs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint64_t>(&attr.second)) {
json[attr.first] = *v;
} else if (auto v = std::get_if<std::string>(&attr.second)) {
Expand Down
9 changes: 7 additions & 2 deletions src/libfetchers/fetchers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -196,7 +196,6 @@ bool Input::contains(const Input & other) const
return false;
}

// FIXME: remove
std::tuple<StorePath, ref<SourceAccessor>, Input> Input::fetchToStore(const Settings & settings, Store & store) const
{
if (!scheme)
Expand Down Expand Up @@ -339,6 +338,9 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(const Settings
{{"hash", store.queryPathInfo(*storePath)->narHash.to_string(HashFormat::SRI, true)}});
}

if (isLocked(settings))
accessor->provenance = std::make_shared<TreeProvenance>(*this);

// FIXME: ideally we would use the `showPath()` of the
// "real" accessor for this fetcher type.
accessor->setPathDisplay("«" + to_string(true) + "»");
Expand All @@ -363,6 +365,9 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(const Settings
else
accessor->fingerprint = result.getFingerprint(store);

if (result.isLocked(settings))
accessor->provenance = std::make_shared<TreeProvenance>(result);

return {accessor, std::move(result)};
} catch (Error & e) {
if (storePath) {
Expand Down
7 changes: 7 additions & 0 deletions src/libfetchers/filtering-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ std::pair<CanonPath, std::optional<std::string>> FilteringSourceAccessor::getFin
return next->getFingerprint(prefix / path);
}

std::shared_ptr<const Provenance> FilteringSourceAccessor::getProvenance(const CanonPath & path)
{
if (provenance)
return SourceAccessor::getProvenance(path);
return next->getProvenance(prefix / path);
}

void FilteringSourceAccessor::invalidateCache(const CanonPath & path)
{
next->invalidateCache(prefix / path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ struct FilteringSourceAccessor : SourceAccessor

std::pair<CanonPath, std::optional<std::string>> getFingerprint(const CanonPath & path) override;

std::shared_ptr<const Provenance> getProvenance(const CanonPath & path) override;

void invalidateCache(const CanonPath & path) override;

/**
Expand Down
1 change: 1 addition & 0 deletions src/libfetchers/include/nix/fetchers/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ headers = files(
'git-lfs-fetch.hh',
'git-utils.hh',
'input-cache.hh',
'provenance.hh',
'registry.hh',
'tarball.hh',
)
17 changes: 17 additions & 0 deletions src/libfetchers/include/nix/fetchers/provenance.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#include "nix/util/provenance.hh"
#include "nix/fetchers/fetchers.hh"

namespace nix {

struct TreeProvenance : Provenance
{
ref<nlohmann::json> attrs;

TreeProvenance(const fetchers::Input & input);

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

} // namespace nix
1 change: 1 addition & 0 deletions src/libfetchers/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ sources = files(
'input-cache.cc',
'mercurial.cc',
'path.cc',
'provenance.cc',
'registry.cc',
'tarball.cc',
)
Expand Down
27 changes: 27 additions & 0 deletions src/libfetchers/provenance.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#include "nix/fetchers/provenance.hh"
#include "nix/fetchers/attrs.hh"

#include <nlohmann/json.hpp>

namespace nix {

TreeProvenance::TreeProvenance(const fetchers::Input & input)
: attrs(make_ref<nlohmann::json>([&]() {
// 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
Loading