Skip to content

Gsoc/builder virtual method - #1

Open
ArkhamKnight25 wants to merge 245 commits into
masterfrom
gsoc/builder-virtual-method
Open

Gsoc/builder virtual method#1
ArkhamKnight25 wants to merge 245 commits into
masterfrom
gsoc/builder-virtual-method

Conversation

@ArkhamKnight25

Copy link
Copy Markdown
Owner

Motivation

Context


Add 👍 to pull requests you find important.

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

Mic92 and others added 30 commits April 8, 2026 11:15
Cutting a release currently blocks on the full hydraJobs evaluation
(~900 builds including sanitizers, clang-tidy, static, NixOS VM and
installer tests), even though upload-release only consumes ~25 of them.
On recent maintenance evals the long CI tail and darwin queue depth
pushed the wait into the multi-day range while the actual artifacts were
ready within an hour.

Hydra hard-codes flake jobsets to outputs.hydraJobs, so the subset is
exposed through a legacy jobset expression that re-enters the flake via
builtins.getFlake on the locked GitHub ref. Going through the ref rather
than the checked-out store path preserves rev/lastModified and thus the
version suffix, keeping derivations bit-identical to the flake jobset so
both share builds through the binary cache. A release aggregate job
provides a single gating signal for upload-release.

The release process now creates a release-$VERSION jobset alongside
maintenance-$VERSION and waits on that instead of the full matrix.

Requires adding https://releases.nixos.org/ to allowed-uris on
hydra.nixos.org, since the locked nixpkgs input is a tarball from there
and legacy jobsets run under restrict-eval.
Several things. Derivation is the primary memory hog in the build loop, with
the total memory usage getting into gigabytes just to hold the derivations
in memory. Previously I've optimised most goals to share a single ref<const Derivation>,
but turns out I missed DerivationResolutionGoal.

Also fixes a bug in the move assignment of Goal::Co that didn't destroy the coroutine
frame of the Goal being moved into.

Just this single commit lowers the memory usage of the build loop by 25% in my tests
(from 4.2G -> 3.2G) on a 70k derivation closure.
ChildEvents has no business being in the coroutines promise type - it bloats
it unnecessarily. The whole promise type was like 500 bytes just because it
stored TimedOut inline - now it's behind a unique_ptr as it should have been.
Shaves off like 300MB of memory usage on my stress-testing benchmark.
… lambda, get rid of allocationsA

std::function allocates for the captures, but with C++23 we have a much better way of writing
recursive lambdas - so use it.
Having such helpers in the namespace scope in a translation unit is
a bad time when using unity builds. At some point this was duplicated
between several files and caused name collisions with unity builds. A lambda
is much cleaner and doesn't have the same footgun.

Lambdas can have template parameters too now.
This avoids the fetched path from getting garbage collected if it is
already valid.
When a URL with embedded credentials (scheme://user:pass@host/...) is
passed to builtins.fetchurl or any other downloadFile caller, the
password is printed verbatim in progress, retry-warning and error
messages because they all format request.uri directly:

  warning: unable to download 'http://user:SECRET@host/file': ...; retrying in 256 ms

Add FileTransferRequest::displayUri() which returns uri with the
userinfo component stripped, and use it at every diagnostic call site.
request.uri itself is unchanged so CURLOPT_URL, result.urls and
fetcher-cache keys are unaffected.
libstore: don't print URL userinfo in FileTransfer diagnostics
It was only printing the base name, which isn't how we usually print store paths.
mountInput modifies lockedRef.input to stuff narHash into it.
…d::function

One slight blemish I noticed while touching this code. With C++23 we can simplify
things.
Turns out the readOnlyMode was always dangling in the C API and nobody
noticed... Previous commits just started accessing it, which was caught
by ASan. This is the minimal fix I can think of.
This is necessary for making flake store paths lazier and also slightly more
concise anyway.
This is thankfully not used by anything else anymore. Good riddance, since
all usages of it had bugs in them.
…SourceAccessor

This significantly simplifies path filtering and gets rid of raw file system accesses.
…mlinks

This would be useful for caching parent directory file descriptors in the source accessor.
I don't think the windows code works too well now, so I left the callback there as a FIXME
to be called. It's just a perf improvement in general and shouldn't break if it never gets called.
Avoids drifting from upload-release.pl when new cross targets such as
x86_64-unknown-freebsd are added to fallback-paths. Also drop the
reference to a not-yet-existing Python rewrite of the upload script.

Addresses review comments on NixOS#15640.
This more honestly wraps the underlying FS accessor (we want to use
the unix-specific dirfd-based one).
With the addition of the file descriptor based source accessor (that does caching)
we now create a fresh instance of the accessor and the last path component is not
followed, so the transformation is safe (actually tests fail without this because
some directories get unlinked and result in ENOENT if the file descriptor gets cached).
The API is the same and uses the unix-specific implementation.
We can also drop all makeParentCanonical calls, because the function
follows symlinks in the parents already when opening the dirFd/file.
One slight wrinkle it that we first have to do absPath - this is fine.
Unlike previously, we now follow symlinks in parents (not the last path component),
but this is secure and what versions like 2.18 did.
…ons) and use in recursive traversal

This ensures race-free traversal throughout.
This completely bypasses coroutines and serialisation/deserialisation overhead
and could start using reflinking in the future too.
edolstra and others added 25 commits May 20, 2026 17:48
…for-search

nix search: switch to boost::regex for ~1.74x speedup
…-follows-warning

fix: remove follows after git-hooks bump
build: embed C API symbols in release binaries
…race

Remove non-atomically initialised variable vImportedDrvToDerivation
There is a race between activity and logger teardown, so we
switch from a unique_ptr to a raw pointer and leak the logger so it
persists across all activity teardowns.

Signed-off-by: Lisanna Dettwyler <lisanna.dettwyler@gmail.com>
In pr NixOS#15082 I introduced parser-based binding detection, but
`parseReplBindings` silently swallowed "unexpected end of file" errors
from incomplete input (e.g. unclosed multi-line strings).

This caused the REPL to fall through to expression parsing as opposed
to binding parsing, leading to an "unexpected '='" error instead of
prompting for continuation lines.

Fixes NixOS#15801
…te input detection

Extract isIncompleteInput helper to deduplicate the "unexpected end of
file" string matching that was repeated in parseString and
parseReplBindings.

Simplify parseReplBindings to return nullptr for "not valid binding
syntax" instead of rethrowing a stored exception_ptr that was always
swallowed by the caller. This makes its contract explicit: returns
ExprAttrs* on success, nullptr if not bindings, or throws
IncompleteReplExpr if the input is incomplete.

This in turn simplifies processLine, which no longer needs a
catch-and-rethrow dance to let IncompleteReplExpr propagate past a
ParseError catch.

Add a doc comment to IncompleteReplExpr explaining why the exception
subtype is needed: evaluation can also produce "unexpected end of file"
ParseErrors (e.g. import of a broken file), but those must be reported
as errors, not trigger continuation.
IncompleteReplExpr is a control flow signal, not a parse error. Making
it a ParseError subclass meant that any `catch (ParseError &)` could
accidentally swallow the continuation signal, which is exactly the kind
of bug that required the catch-and-rethrow workaround removed in the
previous commit.
…g-continuation

Fix repl multiline binding continuation
FreeBSD sets EMLINK instead of ELOOP when opening symlinks with `O_NOFOLLOW`
in order to distinguish different error cases.
Change all occurances of ELOOP to handle this difference.
HttpBinaryCacheStore: Don't ignore 401/407 errors
Fix FreeBSD makeFSSourceAccessor on symlinks
This will be used later in the fixed-output derivation hash mismatch
handling path in the case where we fail to lock the correct path, to
invoke this error early.

No functional change intended.
…tching FOD

While trying to lock the final output path of a hash-mismatching
fixed-output derivation, a deadlock can arise if another derivation is
building and has locked the same output path.

Therefore, if locking fails, just throw the hash-mismatch error and
bail. This does mean the output would not exist after, but I think the
situation still makes sense, since this can be explained as another
derivation that produces this output having been cancelled.
A deadlock was fixed for a case where a slow fixed-output derivation and
a fast hash-mismatching fixed output derivation whose correct output
path is the same as the slow one. Add a test case for this, so that it
doesn't regress.
…t-leak

Fix logging segfault by leaking logger
The option has been broken since Nix 2.4. The flag was accepted, but not
used. This change plumbs it through to computeFSClosure.
A concurrent garbage collection can remove an entry from the links
directory between the moment optimisePath_ checks for it and the moment
it creates the hard link. This produced "cannot create hard link: No
such file or directory" and failed the whole build.

Treat the vanished link as a benign race and skip optimising the path,
since a later pass will dedup it. Guard both the lstat (now maybeLstat)
and the create_hard_link call, mirroring the existing too_many_links and
file_exists handling.

Relates to NixOS#7273

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The logger needs to be manually stopped because it is being leaked.

Signed-off-by: Lisanna Dettwyler <lisanna.dettwyler@gmail.com>
…utputs

Fix nix-copy-closure --include-outputs
…oved-links

libstore: skip optimisation when GC removes a link concurrently
derivation-builder: Allow locking final output to fail for hash-mismatching FOD; plus refactor and test case
This is the major first step of NixOS#5025.

Motivation
----------

Right now, there is a bit of conceptual tension between `--store` and
`--builders`:

- With `--store`, it is very convenient to think that the store knows
  how to build. One specifies a store, and gets a different method of
  building (local, some sort of remote) and scheduling (the remote store
  can take an entire derivation graph, multiple jobs) accordingly.

- With `--builders` one has a local scheduler. Stores either act as a
  passive "workbench" for building (the local case) or we give them a
  single job (a single ready-to-build derivation) at a time.

  For historical reasons, this doesn't even use the store interface,
  except on the "other side" of the build hook.

Issue NixOS#5025 is about using the store interface for `--builders`. In this
case, we want to invert the relationship between `Store` and `Worker`.
We have no use in this case for various `Store` methods creating a
`Worker` behind the scenes, because we already have our `Worker`:

- `LocalStore`s don't need any build method at all, we can just have our
  `Worker` directly use the local store, as it does with the default
  `worker.store` today.

- remote stores supporting building (`ssh://` and `ssh-ng://`) are only
  fed a single job at a time, their remote-side scheduling being
  overkill for the task at hand.

But we can't just delete the building methods of `--store` that we don't
need anymore, because that would break `--store` building. We need to
support both cases, where some stores effectively build/schedule and
`Worker` can also own/borrow stores to be a single, unified scheduler.

This change
-----------

The way we satisfy both goals is by:

- Pulling the building methods out of `Store` into a new `Builder` class

- Having some stores also give/implement `Builder`

The separation of `Store` vs `Builder` works for the `--builders`
use-case, and the project of making that leverage `Store` and other C++
interfaces directly without indirecting through build hook or other
ad-hoc implementation swapping methods. Here's how: as opposed to
default `Store::` method implementations creating a `Worker` on the fly,
`Worker` will implement `Builder`, and those methods, now on `Builder`,
will become `Worker`'s own implementation.

Local building

To implement this conceptual switch, the methods that directly delegated
to the worker are now instead ripped off `Store` and put in the new
`Builder` class. (For example, `build/entry-points.cc` now contains all
`Worker` methods (virtual method impls of `Builder`) and not `Store`
method impls.)

`Worker` also now owns `ref<...> srcStore, destStore`, directly out of
issue NixOS#5025. (`Store & store, evalStore` are kept as aliasing references
to reduce churn.)

(`Worker` should be renamed to `LocalBuilder`, since it is the local
build scheduler, and additionally knows how to build in local stores.)

Remote building

What about the `--store` case? The remote stores have a new method to
provide a `Builder` of their choice given an `evalStore`. (This reflects
the fact that `Builder` no longer has `evalStore` parameters on its
methods.) That new method is a new interface `BuildStore` which
`RemoteStore` and `LegacySSHStore` implement. Each one has an unexposed
`Builder` implementation which will just do everything over RPC, like
today.

Putting it all together

Introduce:

- `getLocalBuilder`, a freestanding function to make a `Worker`.
  (Really, the details of `Worker` should be considered private to the
  `build/*.cc` files)

- `getDefaultBuilder`, a freestanding function which will call
  `BuildStore::getBuilder` for `BuildStore`s, and use
  `getLocalBuilder` otherwise.

Future work
-----------

Issue NixOS#1221

The next step of the NixOS#5025 saga is issue NixOS#1221. To solve that issue,
`Worker` will not use the build hook, but instead work via C++. In particular,
it will do this:

- if the builder is a `BuildStore`, use an appropriate method (possibly
  yet to be created) on the remote building store's (remote) `Builder`.

- if the builder is a `LocalStore` `Worker` should *not* create another
  `Worker` (as the `build-remote` program would do today) but instead directly
  manage building in that local store, so we avoid **n** `Worker` instances
  scheduling independently (which is stupid discoordination).

- (Otherwise fail, which matches what happens today, actually, just in fewer
  steps.)

Simplifying the RPC case

I also suspect that longer term, those stores will just implement
`Builder` directly, as `evalStore` doesn't really make sense for RPC
endpoints when the remote side has no idea what the caller is doing with
other stores. We won't need `BuildStore` anymore then.

Other improvements
------------------

Recursive Nix

Speaking of avoiding redundant schedulers: `RestrictedStore`, when it
used to override the store building methods, would spin up a new
`Worker` for each recursive Nix build call. This is again bad --- we
should have a central scheduler that takes in dynamic jobs, same for
recursive Nix and dynamic derivations. Now this is *almost*, but not
quite, fixed. Three changes were made:

- `RestrictedBuilder` was split out from `RestrictedStore` to wrap the
  build methods.
- `processConnection` takes an optional `Builder` parameter, using it
  directly rather than spinning one up with `getDefaultBuilder`.
- `DerivationBuilder` took a callback to process the connection for
  recursive Nix, so the caller could provide the `processConnection`
  call with the ambient worker in order to reuse it.

This would have solved the redundant scheduler problem very nicely!

This unfortunately deadlocked, so instead the caller explicitly creates
a fresh worker (as before, but not hidden beneath a gazillion
abstractions) with a TODO saying the deadlock should be fixed and this
should not be done.

`LegacySSHStore` fix

As a final note, the old `LegacySSHStore` did not override
`buildPathsWithResults`, which meant that when specifying an `ssh://`
store, the local scheduler was being erroneously used for some commands.
Now, `LegacySSHBuilder::buildPathsWithResults` uses a single
`buildPathsRaw` call (which sends the serve protocol `BuildPaths`
command and returns `std::variant<BuildResultSuccessStatus, BuildError>`
with the error message already read from the wire), and then queries
realisations to reconstruct the `BuildResult`s --- code similar to the
old fallback code for `ssh-ng://`.

Use std::shared_ptr for processConnection's builder

This avoids the need to pass a raw pointer.

Signed-off-by: Lisanna Dettwyler <lisanna.dettwyler@gmail.com>

Rename BuildStore to BuildStore

Signed-off-by: Lisanna Dettwyler <lisanna.dettwyler@gmail.com>
Co-authored-by: John Ericson <John.Ericson@Obsidian.Systems>
@ArkhamKnight25
ArkhamKnight25 force-pushed the gsoc/builder-virtual-method branch 3 times, most recently from 94422a5 to 120c9dc Compare June 30, 2026 04:40
Add buildDerivation() and buildPathsWithResults() overloads to the
Builder interface that take a StorePathSet of inputs and copy them into
the builder's store before building, instead of relying on a separate
copyPaths() beforehand.

Shipping the inputs together with the build request lets a builder store
decide where to build based on those inputs (for example, choosing a
machine by total input size) before they are copied. This is not possible
when copying and building are two separate calls.

RemoteBuilder and LegacySSHBuilder open the local store as the source and
copy the inputs into the builder's store; RestrictedBuilder rejects the
operation. The copy honours the builders-use-substitutes setting.

build-remote folds its standalone dependency copy into these overloads:
the trusted/CA path calls buildDerivation() with the input paths, and the
untrusted path ships the inputs together with the derivation's closure via
buildPathsWithResults().
@ArkhamKnight25
ArkhamKnight25 force-pushed the gsoc/builder-virtual-method branch from 120c9dc to aee006b Compare June 30, 2026 05:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.