diff --git a/doc/manual/source/SUMMARY.md.in b/doc/manual/source/SUMMARY.md.in index a12e84becc35..5fe6bd173726 100644 --- a/doc/manual/source/SUMMARY.md.in +++ b/doc/manual/source/SUMMARY.md.in @@ -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) diff --git a/doc/manual/source/protocols/derivation-builder/derivation-builder.varlink b/doc/manual/source/protocols/derivation-builder/derivation-builder.varlink new file mode 100644 index 000000000000..cfc2dfceb5f4 --- /dev/null +++ b/doc/manual/source/protocols/derivation-builder/derivation-builder.varlink @@ -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 () diff --git a/doc/manual/source/protocols/derivation-builder/index.md b/doc/manual/source/protocols/derivation-builder/index.md new file mode 100644 index 000000000000..6297b622838b --- /dev/null +++ b/doc/manual/source/protocols/derivation-builder/index.md @@ -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). diff --git a/doc/manual/source/store/building.md b/doc/manual/source/store/building.md index 087413406487..b546aea1e3f7 100644 --- a/doc/manual/source/store/building.md +++ b/doc/manual/source/store/building.md @@ -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** @@ -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** @@ -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. @@ -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 diff --git a/src/libstore/build/derivation-builder-varlink.cc b/src/libstore/build/derivation-builder-varlink.cc new file mode 100644 index 000000000000..9bc3dad31539 --- /dev/null +++ b/src/libstore/build/derivation-builder-varlink.cc @@ -0,0 +1,343 @@ +#include "nix/store/build/derivation-builder.hh" +#include "nix/store/build/derivation-builder-varlink.hh" +#include "nix/store/restricted-store.hh" +#include "nix/store/store-api.hh" +#include "nix/store/derivations.hh" +#include "nix/store/path.hh" +#include "nix/util/serialise.hh" +#include "nix/util/error.hh" +#include "nix/util/experimental-features.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/util/unix-domain-socket.hh" + +#include +#include + +#include +#include + +namespace nix { + +using namespace derivation_builder_varlink; + +/** + * Process Varlink protocol messages for the derivation builder interface. + * This implements the org.nix.derivation-builder Varlink interface defined in + * doc/manual/source/protocols/derivation-builder/derivation-builder.varlink + */ +void processVarlinkConnection( + Store & store, const StorePath & drvPath, ref> _submittedOutputs, FdSource & from, FdSink & to) +{ + using json = nlohmann::json; + + auto sendData = [&](const json & data) { + auto responseStr = data.dump(); + responseStr += '\0'; + writeFull(to.fd, responseStr); + }; + + auto sendResponse = [&](const Response & response) { + json j; + nlohmann::adl_serializer::to_json(j, response); + sendData(j); + }; + + auto sendError = [&](const std::string & errorName) { + sendData({ + {"error", errorName}, + {"parameters", json::object()}, + }); + }; + + std::deque buffer; + std::deque fds; + + while (true) { + while (std::find(buffer.cbegin(), buffer.cend(), (std::byte) 0) == buffer.cend()) { + std::array messageBuffer; + unix::ReceivedMessage response; + try { + response = unix::receiveMessageWithFds(from.fd, messageBuffer); + } catch (EndOfFile &) { + return; + } + std::span receivedData(messageBuffer.data(), response.bytesReceived); + + buffer.insert(buffer.end(), receivedData.begin(), receivedData.end()); + + for (auto & item : response.fds) { + fds.insert(fds.end(), std::move(item)); + } + } + + std::vector line; + while (true) { + auto ch = buffer.front(); + buffer.pop_front(); + if (ch == (std::byte) 0) + break; + line.push_back(ch); + } + + if (line.empty()) + continue; + + json requestJson; + try { + requestJson = json::parse(line); + } catch (json::parse_error & e) { + throw Error("Invalid JSON in Varlink request: %s", e.what()); + } + + // Parse the request using the typed Request structure + Request request = nlohmann::adl_serializer::from_json(requestJson); + + // Handle the request based on its type + std::visit( + overloaded{ + [&](const Request::AddToStore & req) { + // Receive file descriptor from client via SCM_RIGHTS + // The client sends the file descriptor containing the NAR archive. + if (fds.size() < 1) { + warn( + "Derivation '%s' didn't send file descriptor when adding to store", + store.printStorePath(drvPath)); + sendError("org.nix.derivation-builder.NoFileDescriptor"); + return; + } + AutoCloseFD narFd = std::move(fds.front()); + fds.pop_front(); + + try { + // Read from the received file descriptor + FdSource narSource(narFd.get()); + // TODO: lock paths + auto path = store.addToStoreFromDump( + narSource, + req.name, + FileSerialisationMethod::NixArchive, + req.method, + HashAlgorithm::SHA256, + {}); + sendResponse(Response{Response::AddToStore{.path = path}}); + } catch (SerialisationError & e) { + warn("Derivation '%s' sent an invalid NAR: %s", store.printStorePath(drvPath), e.info().msg); + sendError("org.nix.derivation-builder.InvalidNar"); + } + }, + [&](const Request::AddDerivation & req) { + // Write the derivation to the store + // TODO: lock paths + auto path = store.writeDerivation(req.derivation); + + sendResponse(Response{Response::AddDerivation{.path = path}}); + }, + [&](const Request::SubmitOutput & req) { + // Register this as a build output + // Note: The actual output registration happens in registerOutputs() + // This method is primarily for the builder to signal completion of an output + // The store path is already tracked by the RestrictedStore + // Authorization is handled automatically by the RestrictedStore wrapper + + try { + ValidPathInfo pathInfo(*store.queryPathInfo(req.path)); + + if (!pathInfo.isContentAddressed(store)) { + warn( + "Derivation '%s' tried to submit non-CA path '%s' for output '%s', skipping", + store.printStorePath(drvPath), + store.printStorePath(req.path), + req.name); + sendError("org.nix.derivation-builder.InvalidPath"); + return; + } + } catch (const InvalidPath & ex) { + warn( + "Derivation '%s' tried to submit invalid path '%s' for output '%s', skipping", + store.printStorePath(drvPath), + store.printStorePath(req.path), + req.name); + sendError("org.nix.derivation-builder.InvalidPath"); + return; + } + + { + auto submittedOutputs(_submittedOutputs->lock()); + if (submittedOutputs->contains(req.name)) { + warn( + "Derivation '%s' submitted duplicate output '%s', ignoring", + store.printStorePath(drvPath), + req.name); + sendError("org.nix.derivation-builder.DuplicateOutput"); + return; + } + + submittedOutputs->insert_or_assign(req.name, req.path); + } + + sendResponse(Response{Response::SubmitOutput{}}); + }}, + request.raw); + } +} + +} // namespace nix + +// JSON serialization implementations +namespace nlohmann { + +using namespace nix; +using namespace nix::derivation_builder_varlink; +using json = nlohmann::json; + +Request::AddToStore adl_serializer::from_json(const json & j) +{ + return Request::AddToStore{ + .name = j.at("name").get(), + .method = ContentAddressMethod::parse(j.at("method").get()), + }; +} + +void adl_serializer::to_json(json & j, const Request::AddToStore & req) +{ + j = json{ + {"name", req.name}, + {"method", req.method.render()}, + }; +} + +Response::AddToStore adl_serializer::from_json(const json & j) +{ + return Response::AddToStore{ + .path = adl_serializer::from_json(j.at("path")), + }; +} + +void adl_serializer::to_json(json & j, const Response::AddToStore & resp) +{ + j = json{ + {"path", resp.path.to_string()}, + }; +} + +Request::AddDerivation adl_serializer::from_json(const json & j) +{ + return Request::AddDerivation{ + .derivation = adl_serializer::from_json(j.at("derivation"), experimentalFeatureSettings), + }; +} + +void adl_serializer::to_json(json & j, const Request::AddDerivation & req) +{ + j = json{}; + adl_serializer::to_json(j["derivation"], req.derivation); +} + +Response::AddDerivation adl_serializer::from_json(const json & j) +{ + return Response::AddDerivation{ + .path = adl_serializer::from_json(j.at("path")), + }; +} + +void adl_serializer::to_json(json & j, const Response::AddDerivation & resp) +{ + j = json{ + {"path", resp.path.to_string()}, + }; +} + +Request::SubmitOutput adl_serializer::from_json(const json & j) +{ + return Request::SubmitOutput{ + .name = j.at("name").get(), + .path = adl_serializer::from_json(j.at("path")), + }; +} + +void adl_serializer::to_json(json & j, const Request::SubmitOutput & req) +{ + j = json{ + {"name", req.name}, + {"path", req.path.to_string()}, + }; +} + +Response::SubmitOutput adl_serializer::from_json(const json & j) +{ + return Response::SubmitOutput{}; +} + +void adl_serializer::to_json(json & j, const Response::SubmitOutput & resp) +{ + j = json::object(); +} + +Request adl_serializer::from_json(const json & j) +{ + std::string method = j.at("method").get(); + json params = j.value("parameters", json::object()); + + if (method == "org.nix.derivation-builder.AddToStore") { + return Request{adl_serializer::from_json(params)}; + } else if (method == "org.nix.derivation-builder.AddDerivation") { + return Request{adl_serializer::from_json(params)}; + } else if (method == "org.nix.derivation-builder.SubmitOutput") { + return Request{adl_serializer::from_json(params)}; + } else { + throw Error("Unknown Varlink method: %s", method); + } +} + +void adl_serializer::to_json(json & j, const Request & req) +{ + std::visit( + overloaded{ + [&](const Request::AddToStore & r) { + j["method"] = "org.nix.derivation-builder.AddToStore"; + adl_serializer::to_json(j["parameters"], r); + }, + [&](const Request::AddDerivation & r) { + j["method"] = "org.nix.derivation-builder.AddDerivation"; + adl_serializer::to_json(j["parameters"], r); + }, + [&](const Request::SubmitOutput & r) { + j["method"] = "org.nix.derivation-builder.SubmitOutput"; + adl_serializer::to_json(j["parameters"], r); + }, + }, + req.raw); +} + +Response adl_serializer::from_json(const json & j) +{ + json params = j.value("parameters", json::object()); + + // Response type is determined by which fields are present + if (params.contains("path") && !params.contains("name")) { + // Could be Response::AddToStore or Response::AddDerivation + // We can't distinguish them from JSON alone, so we'll need context + // For now, just return one type + return Response{adl_serializer::from_json(params)}; + } else { + return Response{Response::SubmitOutput{}}; + } +} + +void adl_serializer::to_json(json & j, const Response & resp) +{ + j = json::object(); + std::visit( + overloaded{ + [&](const Response::AddToStore & r) { adl_serializer::to_json(j["parameters"], r); }, + [&](const Response::AddDerivation & r) { + adl_serializer::to_json(j["parameters"], r); + }, + [&](const Response::SubmitOutput & r) { + adl_serializer::to_json(j["parameters"], r); + }, + }, + resp.raw); +} + +} // namespace nlohmann diff --git a/src/libstore/include/nix/store/build/derivation-builder-varlink.hh b/src/libstore/include/nix/store/build/derivation-builder-varlink.hh new file mode 100644 index 000000000000..75f8eb050772 --- /dev/null +++ b/src/libstore/include/nix/store/build/derivation-builder-varlink.hh @@ -0,0 +1,149 @@ +#pragma once +///@file + +#include "nix/store/path.hh" +#include "nix/store/content-address.hh" +#include "nix/store/derivations.hh" + +#include "nix/store/store-api.hh" +#include "nix/util/json-impls.hh" + +#include + +#include +#include + +namespace nix { + +/** + * Messages for the Derivation Builder Varlink protocol. + * + * This protocol is defined in: + * doc/manual/source/protocols/derivation-builder/derivation-builder.varlink + */ +namespace derivation_builder_varlink { + +/** + * A Varlink protocol request message. + * + * Uses the same pattern as DerivationOutput with a Raw variant type. + */ +struct Request +{ + /** + * Request to add a file to the store with content addressing. + * + * The actual file data is sent out-of-band via a file descriptor + * passed using SCM_RIGHTS. + */ + struct AddToStore + { + std::string name; + ContentAddressMethod method; + + bool operator==(const AddToStore &) const = default; + }; + + /** + * Request to add a derivation to the store. + */ + struct AddDerivation + { + Derivation derivation; + + bool operator==(const AddDerivation &) const = default; + }; + + /** + * Request to register a build output. + * + * This signals that a particular output has been completed and + * associates it with a store path. + */ + struct SubmitOutput + { + std::string name; + StorePath path; + + bool operator==(const SubmitOutput &) const = default; + }; + + typedef std::variant Raw; + + Raw raw; + + bool operator==(const Request &) const = default; + + MAKE_WRAPPER_CONSTRUCTOR(Request); + + /** + * Force choosing a variant + */ + Request() = delete; +}; + +/** + * A Varlink protocol response message. + */ +struct Response +{ + /** + * Response from AddToStore containing the resulting store path. + */ + struct AddToStore + { + StorePath path; + + bool operator==(const AddToStore &) const = default; + }; + + /** + * Response from AddDerivation containing the derivation's store path. + */ + struct AddDerivation + { + StorePath path; + + bool operator==(const AddDerivation &) const = default; + }; + + /** + * Response from SubmitOutput (currently empty). + */ + struct SubmitOutput + { + bool operator==(const SubmitOutput &) const = default; + }; + + typedef std::variant Raw; + + Raw raw; + + bool operator==(const Response &) const = default; + + MAKE_WRAPPER_CONSTRUCTOR(Response); + + /** + * Force choosing a variant + */ + Response() = delete; +}; + +} // namespace derivation_builder_varlink + +/** + * Process Varlink protocol messages for the derivation builder interface. + */ +void processVarlinkConnection( + Store & store, const StorePath & drvPath, ref> submittedOutputs, FdSource & from, FdSink & to); + +} // namespace nix + +JSON_IMPL(nix::derivation_builder_varlink::Request::AddToStore) +JSON_IMPL(nix::derivation_builder_varlink::Request::AddDerivation) +JSON_IMPL(nix::derivation_builder_varlink::Request::SubmitOutput) +JSON_IMPL(nix::derivation_builder_varlink::Response::AddToStore) +JSON_IMPL(nix::derivation_builder_varlink::Response::AddDerivation) +JSON_IMPL(nix::derivation_builder_varlink::Response::SubmitOutput) +JSON_IMPL(nix::derivation_builder_varlink::Request) +JSON_IMPL(nix::derivation_builder_varlink::Response) diff --git a/src/libstore/meson.build b/src/libstore/meson.build index c266f05d4dd1..f49b15dde5bd 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -284,6 +284,7 @@ sources = files( 'binary-cache-store.cc', 'build-result.cc', 'build/build-log.cc', + 'build/derivation-builder-varlink.cc', 'build/derivation-builder.cc', 'build/derivation-building-goal.cc', 'build/derivation-check.cc', diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index fee7c8061efa..95982112ff62 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -388,6 +388,9 @@ StringSet Store::Config::getDefaultSystemFeatures() if (experimentalFeatureSettings.isEnabled(Xp::RecursiveNix)) res.insert("recursive-nix"); + if (experimentalFeatureSettings.isEnabled(Xp::BuilderRpc)) + res.insert("builder-rpc-v1"); + return res; } diff --git a/src/libstore/unix/build/derivation-builder-impl.hh b/src/libstore/unix/build/derivation-builder-impl.hh index cdfe9a901791..97321f68dd23 100644 --- a/src/libstore/unix/build/derivation-builder-impl.hh +++ b/src/libstore/unix/build/derivation-builder-impl.hh @@ -46,6 +46,7 @@ public: , store{store} , miscMethods{miscMethods} , derivationType{drv.type()} + , submittedOutputs(make_ref>()) { } @@ -62,7 +63,12 @@ public: ignoreExceptionInDestructor(); } try { - stopDaemon(); + stopWorkerProtoDaemon(); + } catch (...) { + ignoreExceptionInDestructor(); + } + try { + stopVarlinkDaemon(); } catch (...) { ignoreExceptionInDestructor(); } @@ -129,6 +135,15 @@ protected: */ OutputPathMap scratchOutputs; + /** + * Whether or not derivation is using outputs submitted via varlink + */ + bool usingSubmitted; + /** + * Output paths from the `SubmitOutput` varlink command + */ + ref> submittedOutputs; + const static std::filesystem::path homeDir; /** @@ -152,6 +167,21 @@ protected: */ std::list daemonWorkerThreads; + /** + * The Varlink builder RPC daemon socket. + */ + AutoCloseFD varlinkSocket; + + /** + * The Varlink daemon main thread. + */ + std::thread varlinkThread; + + /** + * The Varlink daemon worker threads. + */ + std::vector varlinkWorkerThreads; + const StorePathSet & originalPaths() override { return inputPaths; @@ -288,15 +318,26 @@ protected: private: /** - * Start an in-process nix daemon thread for recursive-nix. + * Start an in-process worker protocol daemon thread for recursive-nix. + */ + void startWorkerProtoDaemon(); + + /** + * Stop the worker protocol daemon thread. + * @see startWorkerProtoDaemon */ - void startDaemon(); + void stopWorkerProtoDaemon(); /** - * Stop the in-process nix daemon thread. - * @see startDaemon + * Start an in-process Varlink daemon thread for builder-rpc-v1. */ - void stopDaemon(); + void startVarlinkDaemon(); + + /** + * Stop the Varlink daemon thread. + * @see startVarlinkDaemon + */ + void stopVarlinkDaemon(); protected: @@ -366,6 +407,12 @@ private: */ SingleDrvOutputs registerOutputs(); + /** + * Check that the derivation outputs submitted by varlink exist + * and attach them to the derivation + */ + SingleDrvOutputs checkSubmittedOutputs(); + protected: /** diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc index 3b02dfc4f172..7cb1d23ccf32 100644 --- a/src/libstore/unix/build/derivation-builder.cc +++ b/src/libstore/unix/build/derivation-builder.cc @@ -18,6 +18,7 @@ #include "nix/store/user-lock.hh" #include "nix/store/globals.hh" #include "nix/store/build/derivation-env-desugar.hh" +#include "nix/store/build/derivation-builder-varlink.hh" #include "nix/util/terminal.hh" #include "nix/store/filetransfer.hh" @@ -211,8 +212,9 @@ SingleDrvOutputs DerivationBuilderImpl::unprepareBuild() root. */ killSandbox(true); - /* Terminate the recursive Nix daemon. */ - stopDaemon(); + /* Terminate the recursive Nix daemons. */ + stopWorkerProtoDaemon(); + stopVarlinkDaemon(); if (buildResult.cpuUser && buildResult.cpuSystem) { debug( @@ -239,9 +241,14 @@ SingleDrvOutputs DerivationBuilderImpl::unprepareBuild() }; } - /* Compute the FS closure of the outputs and register them as - being valid. */ - auto builtOutputs = registerOutputs(); + SingleDrvOutputs builtOutputs; + if (this->usingSubmitted) { + builtOutputs = checkSubmittedOutputs(); + } else { + /* Compute the FS closure of the outputs and register them as + being valid. */ + builtOutputs = registerOutputs(); + } cleanupBuild(true); @@ -469,8 +476,12 @@ std::optional DerivationBuilderImpl::startBuild() /* Fire up a Nix daemon to process recursive Nix calls from the builder. */ - if (drvOptions.getRequiredSystemFeatures(drv).count("recursive-nix")) - startDaemon(); + auto requiredFeatures = drvOptions.getRequiredSystemFeatures(drv); + if (requiredFeatures.count("recursive-nix")) + startWorkerProtoDaemon(); + this->usingSubmitted = requiredFeatures.count("builder-rpc-v1"); + if (this->usingSubmitted) + startVarlinkDaemon(); /* Run the builder. */ printMsg(lvlChatty, "executing builder '%1%'", drv.builder); @@ -782,7 +793,7 @@ void DerivationBuilderImpl::initEnv() env["TERM"] = "xterm-256color"; } -void DerivationBuilderImpl::startDaemon() +void DerivationBuilderImpl::startWorkerProtoDaemon() { experimentalFeatureSettings.require(Xp::RecursiveNix); @@ -825,7 +836,7 @@ void DerivationBuilderImpl::startDaemon() unix::closeOnExec(remote.get()); - debug("received daemon connection"); + debug("received worker protocol daemon connection"); auto doneFlag = make_ref(); @@ -833,9 +844,9 @@ void DerivationBuilderImpl::startDaemon() try { daemon::processConnection( store, FdSource(remote.get()), FdSink(remote.get()), NotTrusted, daemon::Recursive); - debug("terminated daemon connection"); + debug("terminated worker protocol daemon connection"); } catch (const Interrupted &) { - debug("interrupted daemon connection"); + debug("interrupted worker protocol daemon connection"); } catch (...) { /* Swallow all exceptions to avoid crashing the the process (exceptions that escape from the thread * trigger std::terminate()). */ @@ -864,11 +875,11 @@ void DerivationBuilderImpl::startDaemon() } } - debug("daemon shutting down"); + debug("worker protocol daemon shutting down"); }); } -void DerivationBuilderImpl::stopDaemon() +void DerivationBuilderImpl::stopWorkerProtoDaemon() { if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) { // According to the POSIX standard, the 'shutdown' function should @@ -883,7 +894,7 @@ void DerivationBuilderImpl::stopDaemon() if (errno == ENOTCONN) { daemonSocket.close(); } else { - throw SysError("shutting down daemon socket"); + throw SysError("shutting down worker protocol daemon socket"); } } @@ -898,8 +909,95 @@ void DerivationBuilderImpl::stopDaemon() daemonSocket.close(); } +void DerivationBuilderImpl::startVarlinkDaemon() +{ + experimentalFeatureSettings.require(Xp::BuilderRpc); + + auto store = makeRestrictedStore( + [&] { + auto config = make_ref(*this->store.config); + config->pathInfoCacheSize = 0; + config->stateDir = "/no-such-path"; + config->logDir = "/no-such-path"; + return config; + }(), + ref(std::dynamic_pointer_cast(this->store.shared_from_this())), + *this); + + auto socketName = ".nix-varlink-socket"; + auto socketPath = tmpDir + "/" + socketName; + env["NIX_VARLINK_REMOTE"] = (tmpDirInSandbox() / socketName).string(); + + varlinkSocket = createUnixDomainSocket(socketPath, 0600); + + chownToBuilder(socketPath); + + varlinkThread = std::thread([this, store]() { + while (true) { + + /* Accept a connection. */ + struct sockaddr_un remoteAddr; + socklen_t remoteAddrLen = sizeof(remoteAddr); + + AutoCloseFD remote = accept(varlinkSocket.get(), (struct sockaddr *) &remoteAddr, &remoteAddrLen); + if (!remote) { + if (errno == EINTR || errno == EAGAIN) + continue; + if (errno == EINVAL || errno == ECONNABORTED) + break; + throw SysError("accepting Varlink connection"); + } + + unix::closeOnExec(remote.get()); + + debug("received Varlink daemon connection"); + + auto workerThread = std::thread( + [store, drvPath{this->drvPath}, submittedOutputs{this->submittedOutputs}, remote{std::move(remote)}]() { + try { + FdSource from(remote.get()); + FdSink to(remote.get()); + processVarlinkConnection(*store, drvPath, submittedOutputs, from, to); + debug("terminated Varlink daemon connection"); + } catch (const Interrupted &) { + debug("interrupted Varlink daemon connection"); + } catch (SystemError &) { + ignoreExceptionExceptInterrupt(); + } + }); + + varlinkWorkerThreads.push_back(std::move(workerThread)); + } + + debug("Varlink daemon shutting down"); + }); +} + void DerivationBuilderImpl::addDependencyImpl(const StorePath & path) {} +void DerivationBuilderImpl::stopVarlinkDaemon() +{ + if (varlinkSocket && shutdown(varlinkSocket.get(), SHUT_RDWR) == -1) { + if (errno == ENOTCONN) { + varlinkSocket.close(); + } else { + throw SysError("shutting down Varlink daemon socket"); + } + } + + if (varlinkThread.joinable()) + varlinkThread.join(); + + // FIXME: should prune worker threads more quickly. + // FIXME: shutdown the client socket to speed up worker termination. + for (auto & thread : varlinkWorkerThreads) + thread.join(); + varlinkWorkerThreads.clear(); + + // release the socket. + varlinkSocket.close(); +} + void DerivationBuilderImpl::chownToBuilder(const std::filesystem::path & path) { if (!buildUser) @@ -1674,6 +1772,78 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() return builtOutputs; } +SingleDrvOutputs DerivationBuilderImpl::checkSubmittedOutputs() +{ + // Submitted outputs from the varlink daemon. + // It's fine to lock here since all other threads with the reference have been shut down. + auto submittedOutputs(this->submittedOutputs->lock()); + + SingleDrvOutputs builtOutputs; + + // Technically this could be done more efficiently, but use two iterations for better error messages + for (auto & [outputName, _] : *submittedOutputs) { + if (!this->drv.outputs.contains(outputName)) { + throw BuildError( + BuildResult::Failure::OutputRejected, + "builder for '%s' attempted to register an output named '%s', but it was not declared in the derivation", + store.printStorePath(drvPath), + outputName); + } + } + + for (auto & [outputName, output] : this->drv.outputs) { + if (!submittedOutputs->contains(outputName)) { + throw BuildError( + BuildResult::Failure::OutputRejected, + "builder for '%s' failed to submit output path for '%s'", + store.printStorePath(drvPath), + outputName); + } + + auto caOutput = std::get_if(&output.raw); + if (caOutput == nullptr) { + throw BuildError( + BuildResult::Failure::OutputRejected, + "builder for non-CAFloating derivation '%s' tried to submit output for '%s'", + store.printStorePath(drvPath), + outputName); + } + + auto submittedPath = get(*submittedOutputs, outputName); + + ValidPathInfo pathInfo(*this->store.queryPathInfo(*submittedPath)); + + if (!pathInfo.isContentAddressed(this->store)) { + throw BuildError( + BuildResult::Failure::OutputRejected, + "builder for '%s' tried to submit non-CA path '%s' for output '%s'", + store.printStorePath(drvPath), + store.printStorePath(*submittedPath), + outputName); + } + + // No need to sign CA outputs, only the realisation matters + + auto realisation = Realisation{ + { + .outPath = *submittedPath, + }, + DrvOutput{ + .drvPath = drvPath, + .outputName = outputName, + }, + }; + + store.signRealisation(realisation); + store.registerDrvOutput(realisation); + builtOutputs.emplace(outputName, realisation); + + // TODO: handle --check + } + + return builtOutputs; +} + void DerivationBuilderImpl::cleanupBuild(bool force) { if (force) { diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 04a6ef38afc9..21dafc8f1061 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -27,7 +27,7 @@ void MissingExperimentalFeature::anchor() {} * 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::BLAKE3Hashes); +constexpr size_t numXpFeatures = 1 + static_cast(Xp::BuilderRpc); constexpr std::array xpFeatureDetails = {{ { @@ -281,6 +281,14 @@ constexpr std::array xpFeatureDetails )", .trackingUrl = "https://github.com/NixOS/nix/milestone/60", }, + { + .tag = Xp::BuilderRpc, + .name = "builder-rpc", + .description = R"( + Enable support for submitting derivation outputs via RPC within a derivation, + instead of writing to output paths. + )", + }, }}; static_assert( diff --git a/src/libutil/include/nix/util/experimental-features.hh b/src/libutil/include/nix/util/experimental-features.hh index 057a3b0064ed..7d01b41099b4 100644 --- a/src/libutil/include/nix/util/experimental-features.hh +++ b/src/libutil/include/nix/util/experimental-features.hh @@ -38,6 +38,7 @@ enum struct ExperimentalFeature { PipeOperators, ExternalBuilders, BLAKE3Hashes, + BuilderRpc, }; /** diff --git a/tests/functional/meson.build b/tests/functional/meson.build index 7d5110ebc4da..1f7bd5af4b33 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -229,6 +229,7 @@ subdir('flakes') subdir('git') subdir('git-hashing') subdir('local-overlay-store') +subdir('varlink') foreach suite : suites workdir = suite['workdir'] diff --git a/tests/functional/varlink/common.sh b/tests/functional/varlink/common.sh new file mode 100644 index 000000000000..1f734ca6ff86 --- /dev/null +++ b/tests/functional/varlink/common.sh @@ -0,0 +1,8 @@ +# shellcheck shell=bash +source ../common.sh + +enableFeatures "builder-rpc ca-derivations dynamic-derivations" + +TODO_NixOS + +restartDaemon diff --git a/tests/functional/varlink/config.nix b/tests/functional/varlink/config.nix new file mode 100644 index 000000000000..57d64bb1a295 --- /dev/null +++ b/tests/functional/varlink/config.nix @@ -0,0 +1,2 @@ +# Shim to get generated file +import "${builtins.getEnv "_NIX_TEST_BUILD_DIR"}/varlink/config.nix" diff --git a/tests/functional/varlink/config.nix.in b/tests/functional/varlink/config.nix.in new file mode 120000 index 000000000000..af24ddb30b01 --- /dev/null +++ b/tests/functional/varlink/config.nix.in @@ -0,0 +1 @@ +../config.nix.in \ No newline at end of file diff --git a/tests/functional/varlink/meson.build b/tests/functional/varlink/meson.build new file mode 100644 index 000000000000..6eaaa101825d --- /dev/null +++ b/tests/functional/varlink/meson.build @@ -0,0 +1,23 @@ +configure_file( + input : 'config.nix.in', + output : 'config.nix', + configuration : test_confdata, +) + +nix_store = dependency('nix-store', required : false) +if nix_store.found() + add_languages('cpp') + subdir('test-varlink') + suites += { + 'name' : 'varlink', + 'deps' : [ varlink_dynamic, varlink_non_trivial, varlink_trivial ], + 'tests' : [ + 'varlink-dynamic.sh', + 'varlink-non-trivial.sh', + 'varlink-trivial.sh', + ], + 'workdir' : meson.current_source_dir(), + } + +endif + diff --git a/tests/functional/varlink/test-varlink/dynamic.cc b/tests/functional/varlink/test-varlink/dynamic.cc new file mode 100644 index 000000000000..de1c77b4d399 --- /dev/null +++ b/tests/functional/varlink/test-varlink/dynamic.cc @@ -0,0 +1,81 @@ +#include "nix/store/derivations.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/source-accessor.hh" +#include "nix/util/source-path.hh" +#include "nix/util/unix-domain-socket.hh" +#include +#include + +int main(int argc, char ** argv) +{ + if (argc != 2) + return 1; + + auto derivation = R"( + { + "version": 4, + "name": "varlink-dynamic", + "outputs": { + "out": { + "hashAlgo": "sha256", + "method": "nar" + } + }, + "inputs": { + "drvs": {}, + "srcs": [] + }, + "builder": "/bin/sh", + "args": [ + "-c", + "echo foo > $out" + ], + "env": {} + } + )"_json; + + derivation["system"] = argv[1]; + derivation["env"]["out"] = nix::hashPlaceholder("out"); + + nlohmann::json addRequest = { + {"method", "org.nix.derivation-builder.AddDerivation"}, + {"parameters", + { + {"derivation", derivation}, + }}, + + }; + + auto remote = nix::getEnv("NIX_VARLINK_REMOTE"); + if (!remote.has_value()) + return 1; + auto varlinkSocket = nix::connect(remote.value()); + + auto addRequestStr = addRequest.dump(); + + nix::write(varlinkSocket.get(), {(const std::byte *) addRequestStr.c_str(), addRequestStr.size() + 1}, false); + + auto responseLine = nix::readLine(varlinkSocket.get(), false, '\0'); + auto response = nlohmann::json::parse(responseLine); + + std::string path = response["parameters"]["path"]; + + nlohmann::json submitOutputRequest = { + {"method", "org.nix.derivation-builder.SubmitOutput"}, + {"parameters", + { + {"name", "out"}, + {"path", path}, + }}, + }; + + { + auto requestStr = submitOutputRequest.dump(); + // Add 1 byte to length to include the trailing null + nix::write(varlinkSocket.get(), {(std::byte *) requestStr.c_str(), requestStr.length() + 1}, true); + // Prevents a warning in the server + nix::readLine(varlinkSocket.get(), false, '\0'); + } + + return 0; +} diff --git a/tests/functional/varlink/test-varlink/meson.build b/tests/functional/varlink/test-varlink/meson.build new file mode 100644 index 000000000000..d81fb6e9e0aa --- /dev/null +++ b/tests/functional/varlink/test-varlink/meson.build @@ -0,0 +1,28 @@ +cxx = meson.get_compiler('cpp') + +varlink_trivial = executable( + 'varlink-trivial', + 'trivial.cc', + dependencies : deps_other + [ + dependency('nix-store'), + ], + build_by_default : false, +) + +varlink_dynamic = executable( + 'varlink-dynamic', + 'dynamic.cc', + dependencies : deps_other + [ + dependency('nix-store'), + ], + build_by_default : false, +) + +varlink_non_trivial = executable( + 'varlink-non-trivial', + 'non-trivial.cc', + dependencies : deps_other + [ + dependency('nix-store'), + ], + build_by_default : false, +) diff --git a/tests/functional/varlink/test-varlink/nix-meson-build-support b/tests/functional/varlink/test-varlink/nix-meson-build-support new file mode 120000 index 000000000000..0b157b1c517a --- /dev/null +++ b/tests/functional/varlink/test-varlink/nix-meson-build-support @@ -0,0 +1 @@ +../../../../nix-meson-build-support \ No newline at end of file diff --git a/tests/functional/varlink/test-varlink/non-trivial.cc b/tests/functional/varlink/test-varlink/non-trivial.cc new file mode 100644 index 000000000000..d992187f3fe3 --- /dev/null +++ b/tests/functional/varlink/test-varlink/non-trivial.cc @@ -0,0 +1,65 @@ +#include "nix/store/derivations.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/unix-domain-socket.hh" +#include +#include +#include + +static nlohmann::json callMethod(int fd, const std::string & method, const nlohmann::json & parameters) +{ + nlohmann::json j = {{"method", method}, {"parameters", parameters}}; + auto s = j.dump(); + nix::write(fd, {(const std::byte *) s.c_str(), s.size() + 1}, false); + + auto resp = nix::readLine(fd, false, '\0'); + return nlohmann::json::parse(resp)["parameters"]; +} + +int main(int argc, char ** argv) +{ + if (argc != 4) + return 1; + + std::string system = argv[1]; + std::string shell = argv[2]; + std::string path = argv[3]; + + auto placeholder = nix::hashPlaceholder("out"); + + auto makeDrv = [&](const std::string & name, const std::vector & deps) -> nlohmann::json { + nlohmann::json drvDeps = nlohmann::json::object(); + for (const auto & dep : deps) + drvDeps[dep] = {{"outputs", {"out"}}, {"dynamicOutputs", nlohmann::json::object()}}; + + return { + {"version", 4}, + {"name", "build-" + name}, + {"outputs", {{"out", {{"hashAlgo", "sha256"}, {"method", "nar"}}}}}, + {"inputs", {{"drvs", drvDeps}, {"srcs", nlohmann::json::array()}}}, + {"system", system}, + {"builder", shell}, + {"args", {"-c", "set -eu; echo \"word env var " + name + " is $" + name + "\" >> \"$out\""}}, + {"env", {{"out", placeholder}, {name, "hello, from " + name + "!"}, {"PATH", path}}}}; + }; + + auto remote = nix::getEnv("NIX_VARLINK_REMOTE"); + if (!remote.has_value()) + return 1; + auto fd = nix::connect(remote.value()); + + auto addDerivation = [&](const nlohmann::json & drv) -> std::string { + auto resp = callMethod(fd.get(), "org.nix.derivation-builder.AddDerivation", {{"derivation", drv}}); + return resp["path"].get(); + }; + + auto a = addDerivation(makeDrv("a", {})); + auto b = addDerivation(makeDrv("b", {a})); + auto c = addDerivation(makeDrv("c", {a})); + auto d = addDerivation(makeDrv("d", {b, c})); + auto e = addDerivation(makeDrv("e", {b, c, d})); + + // Submit e's drv path as our sole output, leaving much of it free + callMethod(fd.get(), "org.nix.derivation-builder.SubmitOutput", {{"name", "out"}, {"path", e}}); + + return 0; +} diff --git a/tests/functional/varlink/test-varlink/trivial.cc b/tests/functional/varlink/test-varlink/trivial.cc new file mode 100644 index 000000000000..98b6491b3b38 --- /dev/null +++ b/tests/functional/varlink/test-varlink/trivial.cc @@ -0,0 +1,72 @@ +#include "nix/util/environment-variables.hh" +#include "nix/util/source-accessor.hh" +#include "nix/util/source-path.hh" +#include "nix/util/unix-domain-socket.hh" +#include +#include + +static const std::string addToStoreRequest = R"( +{ + "method": "org.nix.derivation-builder.AddToStore", + "parameters": { + "name": "example-out", + "method": "nar", + "descriptor": 0 + } +} +)"; + +int main(int argc, char ** argv) +{ + if (argc != 2) + return 1; + + auto accessor = nix::makeFSSourceAccessor(std::filesystem::absolute(argv[1])); + nix::SourcePath src(accessor); + + auto remote = nix::getEnv("NIX_VARLINK_REMOTE"); + if (!remote.has_value()) + return 1; + auto varlinkSocket = nix::connect(remote.value()); + + int narSockets[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, narSockets) != 0) + return 1; + nix::AutoCloseFD narSender(narSockets[0]); + std::vector fdsToSend{narSockets[1]}; + + nix::unix::sendMessageWithFds( + varlinkSocket.get(), {(const std::byte *) addToStoreRequest.c_str(), addToStoreRequest.size() + 1}, fdsToSend); + + { + nix::FdSink narSink(narSender.get()); + + src.dumpPath(narSink); + + narSink.flush(); + narSender.close(); + } + auto responseLine = nix::readLine(varlinkSocket.get(), false, '\0'); + auto response = nlohmann::json::parse(responseLine); + + std::string path = response["parameters"]["path"]; + + nlohmann::json submitOutputRequest = { + {"method", "org.nix.derivation-builder.SubmitOutput"}, + {"parameters", + { + {"name", "out"}, + {"path", path}, + }}, + }; + + { + auto requestStr = submitOutputRequest.dump(); + // Add 1 byte to length to include the trailing null + nix::write(varlinkSocket.get(), {(std::byte *) requestStr.c_str(), requestStr.length() + 1}, true); + // Prevents a warning in the server + nix::readLine(varlinkSocket.get(), false, '\0'); + } + + return 0; +} diff --git a/tests/functional/varlink/varlink-dynamic.sh b/tests/functional/varlink/varlink-dynamic.sh new file mode 100644 index 000000000000..42d4d4e62037 --- /dev/null +++ b/tests/functional/varlink/varlink-dynamic.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source common.sh + +clearStore + +out=$(nix-build ./varlink.nix -A dynamicWrapper --no-out-link) + +test "$(cat "$out"/bar)" == "foo" diff --git a/tests/functional/varlink/varlink-non-trivial.sh b/tests/functional/varlink/varlink-non-trivial.sh new file mode 100644 index 000000000000..c1bbd84c71e1 --- /dev/null +++ b/tests/functional/varlink/varlink-non-trivial.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source common.sh + +clearStore + +out=$(nix-build ./varlink.nix -A nonTrivialOuter --no-out-link) + +test "$(cat "$out/result")" == "word env var e is hello, from e!" diff --git a/tests/functional/varlink/varlink-trivial.sh b/tests/functional/varlink/varlink-trivial.sh new file mode 100644 index 000000000000..84c288d79778 --- /dev/null +++ b/tests/functional/varlink/varlink-trivial.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source common.sh + +clearStore + +out=$(nix-build ./varlink.nix -A trivial --no-out-link) + +test "$(cat "$out"/foo)" == "bar" diff --git a/tests/functional/varlink/varlink.nix b/tests/functional/varlink/varlink.nix new file mode 100644 index 000000000000..6a2831826208 --- /dev/null +++ b/tests/functional/varlink/varlink.nix @@ -0,0 +1,56 @@ +with import ./config.nix; + +let + buildDir = "${builtins.getEnv "_NIX_TEST_BUILD_DIR"}/varlink/test-varlink"; + mkCADerivation = + args: + mkDerivation ( + { + __contentAddressed = true; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + // args + ); + dynamic = mkCADerivation { + name = "varlink-dynamic"; + requiredSystemFeatures = [ "builder-rpc-v1" ]; + buildCommand = '' + ${buildDir}/varlink-dynamic ${builtins.currentSystem} + ''; + }; + nonTrivialInner = mkCADerivation { + name = "varlink-non-trivial-inner"; + requiredSystemFeatures = [ "builder-rpc-v1" ]; + buildCommand = '' + ${buildDir}/varlink-non-trivial ${builtins.currentSystem} ${shell} ${path} + ''; + }; +in +{ + trivial = mkCADerivation { + name = "varlink-trivial"; + requiredSystemFeatures = [ "builder-rpc-v1" ]; + buildCommand = '' + mkdir out + echo "bar" > out/foo + ${buildDir}/varlink-trivial out + ''; + }; + + dynamicWrapper = mkCADerivation { + name = "varlink-dynamic-wrapper"; + buildCommand = '' + mkdir $out + cp ${builtins.outputOf dynamic.outPath "out"} $out/bar + ''; + }; + + nonTrivialOuter = mkCADerivation { + name = "varlink-non-trivial-outer"; + buildCommand = '' + mkdir $out + cp ${builtins.outputOf nonTrivialInner.outPath "out"} $out/result + ''; + }; +}