Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions doc/manual/source/SUMMARY.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
- [`.narinfo` Format](protocols/binary-cache/narinfo.md)
- [Derivation "ATerm" file format](protocols/derivation-aterm.md)
- [Nix32 Encoding](protocols/nix32.md)
- [Derivation Builder Protocol](protocols/derivation-builder/index.md)
- [C API](c-api.md)
- [Glossary](glossary.md)
- [Development](development/index.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# For the derivation builder inside to communicate with Nix
interface org.nix.derivation-builder

type DerivationOutput (
# Input-addressed output
path: ?string,
# Fixed content-addressed output
method: ?string,
hash: ?object,
# Floating content-addressed output
hashAlgo: ?string,
# Impure output
impure: ?bool
)

type DerivationInputs (
srcs: []string,
drvs: [string]object
)

type Derivation (
version: int,
name: string,
outputs: [string]DerivationOutput,
inputs: DerivationInputs,
system: string,
builder: string,
args: []string,
env: [string]string,
# Intentionally freeform, as the point of this is being extensible.
# Implementations should reject what they don't understand.
structuredAttrs: ?object
)

# Add a file to the store.
#
# Parameters:
# - name: file name
# - method: content addressing method ("sha256", etc.)
#
# Returns:
# - path: resulting store path
#
# The added files may only reference the runtime closures of the calling derivation's
# inputs or file system objects that have already been created via the Varlink interface.
#
# Along with the request, the sender must include an SCM_RIGHTS ancillary message with a
# file descriptor containing a NAR of the content.
# This may be either a file or a socket, both are equivalant to the reader.
#
# The file descriptor is not included in the IDL since it is a systemd-specific
# extension and not Varlink proper.
# Opened https://github.com/systemd/systemd/issues/38595 for this question.
method AddToStore(
name: string,
method: string,
) -> (path: string)

# Add a derivation to the store.
# Parameters:
# - derivation: derivation in JSON format
# Returns:
# - path: store path of the derivation
#
# Similarly to `AddToStore`, the generated derivation may only
# reference files that the calling derivation should know about.
method AddDerivation(derivation: Derivation) -> (path: string)

# Submit an output, associating an output with a store object.
#
# Parameters:
# - name: name of the output
# - path: path of the store object (must already exist in store)
#
# The idea is derivations should add and submit their outputs one at a
# time. This allows a few things:
#
# 1. Interesting pipelining. If something downstream just needs e.g. a
# "dev" or "headers" output, it need not block on waiting for the other
# outputs of the upstream derivation.
#
# 2. Content addressing doesn't require Nix-side rewriting. Instead, it
# is the responsibility of the builder to add outputs in reference order,
# and arrange for the store paths that resulted from earlier adds being
# used in later adds. This would be a very hard problem to solve
# in "build system space", and punts it back to userspace, where
# arbitrary strategies can be employed.
method SubmitOutput(name: string, path: string) -> ()

# File Descriptor not sent when required
error NoFileDescriptor ()

# Could not deserialize output NAR
error InvalidNar ()

# Attempted to submit path not in store (or restricted)
error InvalidPath ()

# Attempted to submit output a second time
error DuplicateOutput ()
59 changes: 59 additions & 0 deletions doc/manual/source/protocols/derivation-builder/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Derivation Builder Protocol

This is the specification of the Derivation Builder protocol, which allows derivation builders to communicate with Nix.

The protocol is defined using [Varlink](https://varlink.org/), an Interface Description Language (IDL) for defining service interfaces.

> **Warning**
>
> This protocol is currently experimental and subject to change.

## Background

As described in the [Building](@docroot@/store/building.md) documentation, there are two methods for processing derivation outputs:

1. **Traditional (post-build) processing**: After the builder process exits, Nix scans the output directories left behind, normalizes file permissions, calculates references, and registers the outputs as store objects.
This is the original method and does not require any special communication between the builder and Nix.

2. **Concurrent processing via Varlink IPC**: The builder can communicate with Nix *during* the build to add store objects and submit outputs incrementally.
This protocol enables more advanced use cases like pipelining and avoids the need for Nix-side hash rewriting.

This page describes the second method.

## Varlink Interface Specification

```varlink
{{#include derivation-builder.varlink}}
```

## Overview

The Derivation Builder protocol provides three main methods:

- `AddToStore`: Add a file or directory to the store with content addressing
- `AddDerivation`: Add a derivation to the store (takes a Derivation object in JSON format)
- `SubmitOutput`: Associate an output name with a store object path

The `AddToStore` and `AddDerivation` methods are both similar, creating a [file system object] inside the store given some input.
Derivations can create directories and files with `AddToStore`, but not derivations, i.e. paths that end in `.drv`.
Derivations can create additional derivations with `AddDerivation`.
However, they do not make the files appear in the builder's sandbox.

The `SubmitOutput` method links a file system object in the store created by `AddToStore` or `AddDerivation`
to an output of the calling derivation.
This output name must be declared beforehand in the `outputs` field of the calling derivation.

Not all file system objects need be directly registered as an output.
File system objects that are not registered to an output but included in the [closure] of an object that is will be
kept around by the garbage collector.
Those that are not in a closure may be garbage collected.

This protocol enables interesting capabilities:

1. **Pipelining**: If something downstream just needs e.g. a "dev" or "headers" output, it need not block on waiting for the other outputs of the upstream derivation.

2. **Content addressing without rewriting**: Instead of requiring Nix-side rewriting, it is the responsibility of the builder to add outputs in reference order, and arrange for the store paths that resulted from earlier adds to be used in later adds. This takes what would be a very hard problem to solve in "build system space", and punts it back to userspace, where arbitrary strategies can be employed.

3. **Dynamic derivations with dependencies**: A derivation may call `AddDerivation` a number of times to create small dependency derivations
(e.g. a single C file) before creating a final derivation that combines them (e.g. a linker). Only the final needs to be registered to an output,
and it could then be executed with [dynamic derivations](@docroot@/store/derivation/index.md#dynamic).
78 changes: 64 additions & 14 deletions doc/manual/source/store/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ The life cycle of a build can be broken down into 3 parts:
(Builder processes have no idea what the consumer of their standard output and error does with the pseudo-terminal master, only that they are indeed consumed so buffers do not fill up etc. and writes to each output standard stream will continue to succeed.
In practice, Nix will store the log in `/nix/var/log/nix`)

3. Processing the outputs after the builder has exited.
3. Processing the outputs.

The builder process on exit should have left behind files for each output the derivation is supposed to produce.
The files must be processed to turn them into bona fide store objects.
If the processing succeeds, those store objects are associated with the derivation as (the results of) a successful build.
Traditionally, this happens only after the builder has exited: the builder process should have left behind files for each output the derivation is supposed to produce, and those files are processed to turn them into bona fide store objects.
But there is now also a second approach where the builder sends messages to Nix while it's running, including messages submitting outputs.
This allows outputs to be processed concurrently during the build, allows outputs to depend on other newly created store objects, and also resolves some tricky issues with content-addressing and output-to-output references.
If the processing succeeds, the resulting store objects are associated with the derivation as (the results of) a successful build.

Step (3) is done by Nix externally to the build itself, which is just steps (1) and (2).
In step (3), just inert data is processed, since the builder process has exited or been killed by then.
Step (3) is done by Nix, either externally to the build (in the traditional case, operating on the inert data left behind after the builder has exited or been killed) or concurrently with it (in the IPC case).
Step (1) however is best described not from Nix's perspective, but from the build process's perspective.

> **Explanation**
Expand Down Expand Up @@ -176,7 +176,11 @@ The builder is passed the arguments specified by the derivation attribute `args`

## Processing outputs

If the builder exited successfully, the following steps happen in order to turn the output directories left behind by the builder into proper store objects:
There are two methods for processing outputs.
But first, let us cover the requirements common to both methods.

Regardless of which method is used, each output must be turned into a valid store object.
This involves two steps:

- **Normalize the file permissions**

Expand All @@ -189,15 +193,25 @@ If the builder exited successfully, the following steps happen in order to turn
(The name part and the [store directory path] are ignored when scanning; an input's hash part that is neither followed by a `-` nor proceeded by a `/` still scans as a reference.)
Since these are potential runtime dependencies, Nix will register them as references of the output store object they occur in.

Nix also scans for references from one output to another in the same way, because outputs are allowed to refer to each other.
### Traditional (post-build) processing

With the traditional method, the builder process on exit should have left behind files for each output the derivation is supposed to produce.
The files must be processed to turn them into bona fide store objects.
If the processing succeeds, those store objects are associated with the derivation as (the results of) a successful build.

The outputs' references must form a [directed acyclic graph](@docroot@/glossary.md#gloss-directed-acyclic-graph).
(This is not a special restriction for outputs; it is true for the references of all store objects in general.)
Nix also scans for references from one output to another in the same way, because outputs are allowed to refer to each other.
The outputs' references must form a [directed acyclic graph](@docroot@/glossary.md#gloss-directed-acyclic-graph).
(This is not a special restriction for outputs; it is true for the references of all store objects in general.)

In the case of derivations with output paths that are fixed in advance (i.e. [input-addressing] derivations, or [fixed content-addressing] derivations), the actual final store path to each output is used during the build if possible.
For [floating content-addressing] derivations, however, the final store path is not known in advance by definition.
Scratch store paths must therefore be used instead.
Scanning will use those scratch paths, but then any output-to-be that contains such a scanned scratch path must be rewritten to instead use the final (content-addressed) path of the output in question.
In the case of derivations with output paths that are fixed in advance (i.e. [input-addressing] derivations, or [fixed content-addressing] derivations), the actual final store path to each output is used during the build if possible.
For [floating content-addressing] derivations, however, the final store path is not known in advance by definition.
Scratch store paths must therefore be used instead.
Scanning will use those scratch paths, but then any output-to-be that contains such a scanned scratch path must be rewritten to instead use the final (content-addressed) path of the output in question.

In addition to output-to-output references, rewriting is also needed to support self-references in the content-addressing case.
An output may contain its own store path digest, which is a self-reference.
Hash functions which are secure cannot allow the easy calculation of the quasi-fixed points needed to support self-references "natively", so instead we replace all would-be self-references with a sentinel value, and then rewrite the sentinel value to be the final store path digest.
Superficially, this post-hashing rewriting breaks the content address, but as the self-references are easily identified, the rewriting can be inverted to yield the original hashed data, allowing verifying the content address after all.

At this point, the file system data is in the proper form, and the valid acyclic reference data for each output is also calculated, so the outputs are added to the store as proper store objects.
Additionally, those store objects (at least in the case that they are [content-addressed][content-addressing]) can be associated with the derivation in the [build trace] in the record for a successful build.
Expand All @@ -208,6 +222,42 @@ Additionally, those store objects (at least in the case that they are [content-a
> The builder doesn't know whether Nix does or not, however, as it will have exited before the build directory is cleaned up, and it will not see any old build directory if (after a failed build) it is run again.
> The [`--keep-failed`](@docroot@/command-ref/opt-common.md#opt-keep-failed) option can be specified to keep the build directory in the case of a failing build.

### Concurrent processing via IPC

With this method, the builder communicates with Nix during the build using inter-process communication (IPC).
(The exact varlink-based protocol used is [documented in full in the protocols chapter](@docroot@/protocols/derivation-builder/index.md).)
Instead of leaving files behind for Nix to process after exit, the builder explicitly submits information to create store objects one at a time, and (separately) also submits assignments from output names to store objects.

Scanning for references proceeds as usual for each store object creation request, but the set of potential references to be scanned is greater: it includes both all inputs (as before) and also all previously-added store objects.
This means, if output `bar` is supposed to reference output `foo`, `foo` should be created first, and `bar` second.

All store objects being created are content-addressed (there is no support for input-addressed outputs with the IPC approach).
When a store object is created, its content address store path will be calculated by Nix and then returned in the IPC response message.
The builder then knows what store path to use in subsequent store objects in order for reference scanning to pick them up.

This overall approach has several advantages:

- **No Nix-side rewriting**

For content-addressed outputs, the builder is responsible for adding outputs in reference order, using the store paths from earlier adds in later ones.
This avoids the fragile rewriting that would otherwise be needed to fix up output-to-output references described above.
The builder, unlike Nix itself, is free to leverage domain-specific knowledge to do a better job. For example it can

- uncompress, rewrite, and then recompress man pages, to not miss references hidden by compression.

- make sure to rewrite data that is to be signed, like Apple binaries, before signing that data, so as not to invalidate any signatures by mistake.

- **Pipelining**

Downstream builds that only need some outputs (e.g., a "dev" or "headers" output) can start without waiting for all outputs to be ready.
Nix doesn't yet implement this, but it could and should.

The major *disadvantage* of this approach is that it doesn't yet support self-references.
Unlike acyclic output-to-output references, self-references fundamentally do require rewriting.
The output-to-output case was only a challenge in the traditional case because all the outputs were submitted simultaneously, whereas the self-reference case is fundamentally challenging because of what it means for a hash function to be secure, as described above.
Neither batched (traditional) nor serial (IPC) submission of outputs can avoid this fundamental property of secure hash functions.
We could add support for such rewriting just for self-references, as is done for the traditional post-build processing, but we haven't yet done so as the very point of the IPC approach is to free Nix from any obligation to rewrite black-box data in unsound ways.

[references]: ./store-object.md#references
[store path digest]: ./store-path.md#digest
[store object]: ./store-object.md
Expand Down
Loading
Loading