Skip to content

libexpr: let the embedder realise an import from derivation - #16683

Open
DerDennisOP wants to merge 4 commits into
NixOS:masterfrom
DerDennisOP:realise-hook
Open

DerDennisOP wants to merge 4 commits into
NixOS:masterfrom
DerDennisOP:realise-hook

Conversation

@DerDennisOP

Copy link
Copy Markdown
Contributor

Motivation

I maintain a small CI system with a build scheduler of its own. I nned to control how an import from derivation is built: realiseContext() always builds through the store.

Context

EvalSettings::realiseHook replaces that build when set and nix_eval_state_builder_set_realise_hook() installs a C callback as the hook. The embedder only has to make the outputs valid in the evaluation store before returning. Unit test and release note included.

Stacked on the failed-realisation memo PR (#16682).


Add 👍 to pull requests you find important.

The Nix maintainer team uses a GitHub project board to schedule and track reviews.

@github-actions github-actions Bot added documentation c api Nix as a C library with a stable interface labels Oct 25, 2026
@DerDennisOP
DerDennisOP force-pushed the realise-hook branch 3 times, most recently from cac60da to ec50b51 Compare October 25, 2026 19:20
@github-actions github-actions Bot added the store Issues and pull requests concerning the Nix store label Oct 25, 2026
Comment thread src/libexpr/primops.cc Outdated
its build attempt once per process rather than once per importer. */
for (auto & d : drvs)
if (auto failed = getConcurrent(*failedRealisations, d.drvPath->getBaseStorePath()))
std::rethrow_exception(*failed);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very wrong. Exceptions pointers are refcounted and we mutate them. That's why we have throwClone CRTP trick for rethrowing exceptions.

I guess I really should add that clang-tidy rule banning std::rethrow_exception and having a helper which handles that for you...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's usually good to have some codebase rules for things like that. I'll fix the Exeception.

Comment on lines +105 to +112
/**
* Realises the derivations of an import from derivation instead of
* `buildPaths`. On return, the outputs and their closure must be valid in
* the evaluation store. Throws `Error` on failure.
*/
using RealiseHook = std::function<void(const std::vector<DerivedPath::Built> &)>;

RealiseHook realiseHook;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of having hooks that inject into the regular code flow. At the very least the regular mechanism should be expressable with such hooks. But also I think this ties too deeply to the current architecture of the evaluator which blocks on IFD.

@DerDennisOP DerDennisOP Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, maybe you have a better idea how to implement this? Generally speaking I think this is feature really important for CIs out there.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the best shape would be.

I suppose for the implementation that @edolstra cooked up with coroutines this API should be completion-callback driven instead of being blocking-shaped. But our underlying Store interface is currently blocking so not much to be done about it.

So I'd suggest for now to have the C API receive a completion callback that it invokes when the build completes. That will make us a bit more future proof for non-blocking IFD.

But also at the C++ API level it might make sense to instead have an eval-specific Store class that would instead just intercept the build request. I've had some luck doing store adapters in https://github.com/xokdvium/nix/tree/writeback-store (that didn't yet see the light of day, but I want to get back to that).

@DerDennisOP
DerDennisOP force-pushed the realise-hook branch 2 times, most recently from 73a7749 to a0b6276 Compare November 9, 2026 14:59
Dennis Wuitz added 2 commits November 9, 2026 16:07
A cached string's context paths each required one `isValidPath` call
from `getStringWithContext()`. Each path cost a daemon round-trip. The
function switched to `addTempRoot` for each path. Those calls
deduplicate per connection in `RemoteStore`. A single `queryValidPaths`
call can check all paths.
@DerDennisOP
DerDennisOP force-pushed the realise-hook branch 2 times, most recently from a7c7774 to bd17d9a Compare November 9, 2026 15:47
Attributes that import the same failing derivation ask the store to build that derivation again. Many attributes import the same broken derivation. These importers cause one attempt each. The output lock of the store serialized concurrent evaluator attempts.

Build with `buildPathsWithResults`. Save a copy of the build error for each derivation that failed alone. Reasons include failure of the builder, rejection of inputs or outputs, hash mismatch and log limit. Same for a timeout. A second attempt can wait for the full timeout again. Rethrow that copy rather than rebuild. Rebuild transient failures such as network errors from a substituter, dependency failure and loss of the daemon connection. The `:reload` command in the REPL can call `resetFileCache()`. Stored failures disappear after `resetFileCache()`.
An embedder with a build system of its own, such as a CI, has no say in
how an import from derivation is built: realiseContext() always builds
through the store. EvalSettings::realiseHook replaces that build when
set, and nix_eval_state_builder_set_realise_hook() installs a C
callback as the hook. The embedder only has to make the outputs valid
in the evaluation store before it returns.

With a hook set, the failed-realisation memo stays off: the embedder
decides how long a failure holds, since its evaluator can outlive one
evaluation.
@DerDennisOP

Copy link
Copy Markdown
Contributor Author

Agreed. A setting that can bypass the normal path is inappropriate. I propose to remove realiseHook and give EvalState an optional ref<Builder>, with buildStore->getBuilder(store) as default. All builds in the evaluator then use that option. The C API can get Builder from a callback. Replay from #16682 can use an optional Builder wrapper. That design can avoid a forwarding store like writeback-store. The approach with a store adapter can still work if that route is preferable.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c api Nix as a C library with a stable interface documentation store Issues and pull requests concerning the Nix store

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants