about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--tvix/build/src/buildservice/build_request.rs2
-rw-r--r--tvix/build/src/buildservice/dummy.rs5
-rw-r--r--tvix/build/src/buildservice/grpc.rs7
-rw-r--r--tvix/build/src/buildservice/mod.rs4
-rw-r--r--tvix/build/src/buildservice/oci.rs24
-rw-r--r--tvix/build/src/oci/bundle.rs47
-rw-r--r--tvix/build/src/oci/mod.rs7
-rw-r--r--tvix/build/src/oci/spec.rs80
-rw-r--r--tvix/build/src/proto/grpc_buildservice_wrapper.rs4
-rw-r--r--tvix/build/src/proto/mod.rs51
-rw-r--r--tvix/glue/src/tvix_build.rs158
-rw-r--r--tvix/glue/src/tvix_store_io.rs7
12 files changed, 221 insertions, 175 deletions
diff --git a/tvix/build/src/buildservice/build_request.rs b/tvix/build/src/buildservice/build_request.rs
index 78409c34e3b2..4d53ee55096c 100644
--- a/tvix/build/src/buildservice/build_request.rs
+++ b/tvix/build/src/buildservice/build_request.rs
@@ -35,7 +35,7 @@ use tvix_castore::{Node, PathComponent};
 ///
 /// As of now, we're okay to accept this, but it prevents uploading an
 /// entirely-non-IFD subgraph of BuildRequests eagerly.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Default, Debug, Clone, PartialEq)]
 pub struct BuildRequest {
     /// The list of all root nodes that should be visible in `inputs_dir` at the
     /// time of the build.
diff --git a/tvix/build/src/buildservice/dummy.rs b/tvix/build/src/buildservice/dummy.rs
index d20444755e73..3368201376ed 100644
--- a/tvix/build/src/buildservice/dummy.rs
+++ b/tvix/build/src/buildservice/dummy.rs
@@ -2,7 +2,8 @@ use tonic::async_trait;
 use tracing::instrument;
 
 use super::BuildService;
-use crate::proto::{Build, BuildRequest};
+use crate::buildservice::BuildRequest;
+use crate::proto;
 
 #[derive(Default)]
 pub struct DummyBuildService {}
@@ -10,7 +11,7 @@ pub struct DummyBuildService {}
 #[async_trait]
 impl BuildService for DummyBuildService {
     #[instrument(skip(self), ret, err)]
-    async fn do_build(&self, _request: BuildRequest) -> std::io::Result<Build> {
+    async fn do_build(&self, _request: BuildRequest) -> std::io::Result<proto::Build> {
         Err(std::io::Error::new(
             std::io::ErrorKind::Other,
             "builds are not supported with DummyBuildService",
diff --git a/tvix/build/src/buildservice/grpc.rs b/tvix/build/src/buildservice/grpc.rs
index 9d22d8397abf..14f06f0ee3e6 100644
--- a/tvix/build/src/buildservice/grpc.rs
+++ b/tvix/build/src/buildservice/grpc.rs
@@ -1,6 +1,7 @@
 use tonic::{async_trait, transport::Channel};
 
-use crate::proto::{build_service_client::BuildServiceClient, Build, BuildRequest};
+use crate::buildservice::BuildRequest;
+use crate::proto::{self, build_service_client::BuildServiceClient};
 
 use super::BuildService;
 
@@ -17,10 +18,10 @@ impl GRPCBuildService {
 
 #[async_trait]
 impl BuildService for GRPCBuildService {
-    async fn do_build(&self, request: BuildRequest) -> std::io::Result<Build> {
+    async fn do_build(&self, request: BuildRequest) -> std::io::Result<proto::Build> {
         let mut client = self.client.clone();
         client
-            .do_build(request)
+            .do_build(Into::<proto::BuildRequest>::into(request))
             .await
             .map(|resp| resp.into_inner())
             .map_err(std::io::Error::other)
diff --git a/tvix/build/src/buildservice/mod.rs b/tvix/build/src/buildservice/mod.rs
index c3c2fd6c5715..b12db6b95d13 100644
--- a/tvix/build/src/buildservice/mod.rs
+++ b/tvix/build/src/buildservice/mod.rs
@@ -1,6 +1,6 @@
 use tonic::async_trait;
 
-use crate::proto::{self, Build};
+use crate::proto;
 
 pub mod build_request;
 pub use crate::buildservice::build_request::*;
@@ -17,5 +17,5 @@ pub use from_addr::from_addr;
 #[async_trait]
 pub trait BuildService: Send + Sync {
     /// TODO: document
-    async fn do_build(&self, request: proto::BuildRequest) -> std::io::Result<Build>;
+    async fn do_build(&self, request: BuildRequest) -> std::io::Result<proto::Build>;
 }
diff --git a/tvix/build/src/buildservice/oci.rs b/tvix/build/src/buildservice/oci.rs
index 89efbb4285d0..52efede6597b 100644
--- a/tvix/build/src/buildservice/oci.rs
+++ b/tvix/build/src/buildservice/oci.rs
@@ -10,15 +10,15 @@ use tvix_castore::{
     fs::fuse::FuseDaemon,
     import::fs::ingest_path,
     refscan::{ReferencePattern, ReferenceScanner},
-    Node, PathComponent,
 };
 use uuid::Uuid;
 
+use crate::buildservice::BuildRequest;
 use crate::{
     oci::{get_host_output_paths, make_bundle, make_spec},
-    proto::{build::OutputNeedles, Build, BuildRequest},
+    proto::{self, build::OutputNeedles},
 };
-use std::{collections::BTreeMap, ffi::OsStr, path::PathBuf, process::Stdio};
+use std::{ffi::OsStr, path::PathBuf, process::Stdio};
 
 use super::BuildService;
 
@@ -95,7 +95,7 @@ where
     DS: DirectoryService + Clone + 'static,
 {
     #[instrument(skip_all, err)]
-    async fn do_build(&self, request: BuildRequest) -> std::io::Result<Build> {
+    async fn do_build(&self, request: BuildRequest) -> std::io::Result<proto::Build> {
         let _permit = self.concurrent_builds.acquire().await.unwrap();
 
         let bundle_name = Uuid::new_v4();
@@ -128,26 +128,20 @@ where
             .map_err(std::io::Error::other)?;
 
         // assemble a BTreeMap of Nodes to pass into TvixStoreFs.
-        let root_nodes: BTreeMap<PathComponent, Node> =
-            BTreeMap::from_iter(request.inputs.iter().map(|input| {
-                // We know from validation this is Some.
-                input.clone().try_into_name_and_node().unwrap()
-            }));
         let patterns = ReferencePattern::new(request.refscan_needles.clone());
         // NOTE: impl Drop for FuseDaemon unmounts, so if the call is cancelled, umount.
         let _fuse_daemon = tokio::task::spawn_blocking({
             let blob_service = self.blob_service.clone();
             let directory_service = self.directory_service.clone();
 
-            debug!(inputs=?root_nodes.keys(), "got inputs");
-
             let dest = bundle_path.join("inputs");
 
+            let root_nodes = Box::new(request.inputs.clone());
             move || {
                 let fs = tvix_castore::fs::TvixStoreFs::new(
                     blob_service,
                     directory_service,
-                    Box::new(root_nodes),
+                    root_nodes,
                     true,
                     false,
                 );
@@ -223,7 +217,7 @@ where
 
                     Ok::<_, std::io::Error>((
                         tvix_castore::proto::Node::from_name_and_node(
-                            PathBuf::from(output_path)
+                            output_path
                                 .file_name()
                                 .and_then(|s| s.to_str())
                                 .map(|s| s.to_string())
@@ -240,8 +234,8 @@ where
         .into_iter()
         .unzip();
 
-        Ok(Build {
-            build_request: Some(request.clone()),
+        Ok(proto::Build {
+            build_request: Some(request.into()),
             outputs,
             outputs_needles,
         })
diff --git a/tvix/build/src/oci/bundle.rs b/tvix/build/src/oci/bundle.rs
index c3c2e83e89e5..52789362a58d 100644
--- a/tvix/build/src/oci/bundle.rs
+++ b/tvix/build/src/oci/bundle.rs
@@ -5,7 +5,7 @@ use std::{
 };
 
 use super::scratch_name;
-use crate::proto::BuildRequest;
+use crate::buildservice::BuildRequest;
 use anyhow::{bail, Context};
 use tracing::{debug, instrument};
 
@@ -60,12 +60,10 @@ pub(crate) fn get_host_output_paths(
 
     for output_path in request.outputs.iter() {
         // calculate the location of the path.
-        if let Some((mp, relpath)) =
-            find_path_in_scratchs(output_path, request.scratch_paths.as_slice())
-        {
+        if let Some((mp, relpath)) = find_path_in_scratchs(output_path, &request.scratch_paths) {
             host_output_paths.push(scratch_root.join(scratch_name(mp)).join(relpath));
         } else {
-            bail!("unable to find path {}", output_path);
+            bail!("unable to find path {output_path:?}");
         }
     }
 
@@ -77,16 +75,18 @@ pub(crate) fn get_host_output_paths(
 /// relative path from there to the search_path.
 /// mountpoints must be sorted, so we can iterate over the list from the back
 /// and match on the prefix.
-fn find_path_in_scratchs<'a, 'b>(
-    search_path: &'a str,
-    mountpoints: &'b [String],
-) -> Option<(&'b str, &'a str)> {
-    mountpoints.iter().rev().find_map(|mp| {
-        Some((
-            mp.as_str(),
-            search_path.strip_prefix(mp)?.strip_prefix('/')?,
-        ))
-    })
+fn find_path_in_scratchs<'a, 'b, I>(
+    search_path: &'a Path,
+    mountpoints: I,
+) -> Option<(&'b Path, &'a Path)>
+where
+    I: IntoIterator<Item = &'b PathBuf>,
+    I::IntoIter: DoubleEndedIterator,
+{
+    mountpoints
+        .into_iter()
+        .rev()
+        .find_map(|mp| Some((mp.as_path(), search_path.strip_prefix(mp).ok()?)))
 }
 
 #[cfg(test)]
@@ -95,7 +95,7 @@ mod tests {
 
     use rstest::rstest;
 
-    use crate::{oci::scratch_name, proto::BuildRequest};
+    use crate::{buildservice::BuildRequest, oci::scratch_name};
 
     use super::{find_path_in_scratchs, get_host_output_paths};
 
@@ -108,7 +108,18 @@ mod tests {
         #[case] mountpoints: &[String],
         #[case] expected: Option<(&str, &str)>,
     ) {
-        assert_eq!(find_path_in_scratchs(search_path, mountpoints), expected);
+        let expected = expected.map(|e| (Path::new(e.0), Path::new(e.1)));
+        assert_eq!(
+            find_path_in_scratchs(
+                Path::new(search_path),
+                mountpoints
+                    .iter()
+                    .map(PathBuf::from)
+                    .collect::<Vec<_>>()
+                    .as_slice()
+            ),
+            expected
+        );
     }
 
     #[test]
@@ -125,7 +136,7 @@ mod tests {
         let mut expected_path = PathBuf::new();
         expected_path.push("bundle-root");
         expected_path.push("scratch");
-        expected_path.push(scratch_name("nix/store"));
+        expected_path.push(scratch_name(Path::new("nix/store")));
         expected_path.push("fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo");
 
         assert_eq!(vec![expected_path], paths)
diff --git a/tvix/build/src/oci/mod.rs b/tvix/build/src/oci/mod.rs
index 26dab3059a58..a2400c4a6eba 100644
--- a/tvix/build/src/oci/mod.rs
+++ b/tvix/build/src/oci/mod.rs
@@ -5,9 +5,12 @@ pub(crate) use bundle::get_host_output_paths;
 pub(crate) use bundle::make_bundle;
 pub(crate) use spec::make_spec;
 
+use std::path::Path;
+
 /// For a given scratch path, return the scratch_name that's allocated.
 // We currently use use lower hex encoding of the b3 digest of the scratch
 // path, so we don't need to globally allocate and pass down some uuids.
-pub(crate) fn scratch_name(scratch_path: &str) -> String {
-    data_encoding::BASE32.encode(blake3::hash(scratch_path.as_bytes()).as_bytes())
+pub(crate) fn scratch_name(scratch_path: &Path) -> String {
+    data_encoding::BASE32
+        .encode(blake3::hash(scratch_path.as_os_str().as_encoded_bytes()).as_bytes())
 }
diff --git a/tvix/build/src/oci/spec.rs b/tvix/build/src/oci/spec.rs
index e80e442f0350..e3b6172def8a 100644
--- a/tvix/build/src/oci/spec.rs
+++ b/tvix/build/src/oci/spec.rs
@@ -1,11 +1,10 @@
 //! Module to create a OCI runtime spec for a given [BuildRequest].
-use crate::proto::BuildRequest;
+use crate::buildservice::{BuildConstraints, BuildRequest};
 use oci_spec::{
     runtime::{Capability, LinuxNamespace, LinuxNamespaceBuilder, LinuxNamespaceType},
     OciSpecError,
 };
 use std::{collections::HashSet, path::Path};
-use tvix_castore::proto as castorepb;
 
 use super::scratch_name;
 
@@ -35,33 +34,26 @@ pub(crate) fn make_spec(
     rootless: bool,
     sandbox_shell: &str,
 ) -> Result<oci_spec::runtime::Spec, oci_spec::OciSpecError> {
-    // TODO: add BuildRequest validations. BuildRequest must contain strings as inputs
-
     let allow_network = request
         .constraints
-        .as_ref()
-        .is_some_and(|c| c.network_access);
+        .contains(&BuildConstraints::NetworkAccess);
 
     // Assemble ro_host_mounts. Start with constraints.available_ro_paths.
-    let mut ro_host_mounts = request
+    let mut ro_host_mounts: Vec<_> = request
         .constraints
-        .as_ref()
-        .map(|constraints| {
-            constraints
-                .available_ro_paths
-                .iter()
-                .map(|e| (e.as_str(), e.as_str()))
-                .collect::<Vec<_>>()
+        .iter()
+        .filter_map(|constraint| match constraint {
+            BuildConstraints::AvailableReadOnlyPath(path) => Some((path.as_path(), path.as_path())),
+            _ => None,
         })
-        .unwrap_or_default();
+        .collect();
 
     // If provide_bin_sh is set, mount sandbox_shell to /bin/sh
     if request
         .constraints
-        .as_ref()
-        .is_some_and(|c| c.provide_bin_sh)
+        .contains(&BuildConstraints::ProvideBinSh)
     {
-        ro_host_mounts.push((sandbox_shell, "/bin/sh"))
+        ro_host_mounts.push((Path::new(sandbox_shell), Path::new("/bin/sh")))
     }
 
     oci_spec::runtime::SpecBuilder::default()
@@ -92,9 +84,9 @@ pub(crate) fn make_spec(
         .mounts(configure_mounts(
             rootless,
             allow_network,
-            request.scratch_paths.iter().map(|e| e.as_str()),
+            request.scratch_paths.iter().map(|e| e.as_path()),
             request.inputs.iter(),
-            &request.inputs_dir, // TODO: validate
+            &request.inputs_dir,
             ro_host_mounts,
         )?)
         .build()
@@ -106,7 +98,7 @@ pub(crate) fn make_spec(
 /// Capabilities are a bit more complicated in case rootless building is requested.
 fn configure_process<'a>(
     command_args: &[String],
-    cwd: &String,
+    cwd: &Path,
     env: impl IntoIterator<Item = (&'a str, String)>,
     rootless: bool,
 ) -> Result<oci_spec::runtime::Process, oci_spec::OciSpecError> {
@@ -232,10 +224,11 @@ fn configure_linux(
 fn configure_mounts<'a>(
     rootless: bool,
     allow_network: bool,
-    scratch_paths: impl IntoIterator<Item = &'a str>,
-    inputs: impl Iterator<Item = &'a castorepb::Node>,
-    inputs_dir: &str,
-    ro_host_mounts: impl IntoIterator<Item = (&'a str, &'a str)>,
+    scratch_paths: impl IntoIterator<Item = &'a Path>,
+    inputs: impl Iterator<Item = (&'a tvix_castore::PathComponent, &'a tvix_castore::Node)>,
+
+    inputs_dir: &Path,
+    ro_host_mounts: impl IntoIterator<Item = (&'a Path, &'a Path)>,
 ) -> Result<Vec<oci_spec::runtime::Mount>, oci_spec::OciSpecError> {
     let mut mounts: Vec<_> = if rootless {
         oci_spec::runtime::get_rootless_mounts()
@@ -244,8 +237,8 @@ fn configure_mounts<'a>(
     };
 
     mounts.push(configure_mount(
-        "tmpfs",
-        "/tmp",
+        Path::new("tmpfs"),
+        Path::new("/tmp"),
         "tmpfs",
         &["nosuid", "noatime", "mode=700"],
     )?);
@@ -255,28 +248,19 @@ fn configure_mounts<'a>(
     for scratch_path in scratch_paths.into_iter() {
         let src = scratch_root.join(scratch_name(scratch_path));
         mounts.push(configure_mount(
-            src.to_str().unwrap(),
-            Path::new("/").join(scratch_path).to_str().unwrap(),
+            &src,
+            &Path::new("/").join(scratch_path),
             "none",
             &["rbind", "rw"],
         )?);
     }
 
     // For each input, create a bind mount from inputs/$name into $inputs_dir/$name.
-    for input in inputs {
-        let (input_name, _input) = input
-            .clone()
-            .try_into_name_and_node()
-            .expect("invalid input name");
-
+    for (input_name, _input) in inputs {
         let input_name = std::str::from_utf8(input_name.as_ref()).expect("invalid input name");
         mounts.push(configure_mount(
-            Path::new("inputs").join(input_name).to_str().unwrap(),
-            Path::new("/")
-                .join(inputs_dir)
-                .join(input_name)
-                .to_str()
-                .unwrap(),
+            &Path::new("inputs").join(input_name),
+            &Path::new("/").join(inputs_dir).join(input_name),
             "none",
             &[
                 "rbind", "ro",
@@ -295,7 +279,11 @@ fn configure_mounts<'a>(
 
     // In case network is enabled, also mount in /etc/{resolv.conf,services,hosts}
     if allow_network {
-        for p in ["/etc/resolv.conf", "/etc/services", "/etc/hosts"] {
+        for p in [
+            Path::new("/etc/resolv.conf"),
+            Path::new("/etc/services"),
+            Path::new("/etc/hosts"),
+        ] {
             mounts.push(configure_mount(p, p, "none", &["rbind", "ro"])?);
         }
     }
@@ -305,15 +293,15 @@ fn configure_mounts<'a>(
 
 /// Helper function to produce a mount.
 fn configure_mount(
-    source: &str,
-    destination: &str,
+    source: &Path,
+    destination: &Path,
     typ: &str,
     options: &[&str],
 ) -> Result<oci_spec::runtime::Mount, oci_spec::OciSpecError> {
     oci_spec::runtime::MountBuilder::default()
-        .destination(destination.to_string())
+        .destination(destination)
         .typ(typ.to_string())
-        .source(source.to_string())
+        .source(source)
         .options(options.iter().map(|e| e.to_string()).collect::<Vec<_>>())
         .build()
 }
diff --git a/tvix/build/src/proto/grpc_buildservice_wrapper.rs b/tvix/build/src/proto/grpc_buildservice_wrapper.rs
index 024f075de9ad..8a2d36ac569f 100644
--- a/tvix/build/src/proto/grpc_buildservice_wrapper.rs
+++ b/tvix/build/src/proto/grpc_buildservice_wrapper.rs
@@ -27,7 +27,9 @@ where
         &self,
         request: tonic::Request<BuildRequest>,
     ) -> Result<tonic::Response<Build>, tonic::Status> {
-        match self.inner.do_build(request.into_inner()).await {
+        let request = TryInto::<crate::buildservice::BuildRequest>::try_into(request.into_inner())
+            .map_err(|err| tonic::Status::new(tonic::Code::InvalidArgument, err.to_string()))?;
+        match self.inner.do_build(request).await {
             Ok(resp) => Ok(tonic::Response::new(resp)),
             Err(e) => Err(tonic::Status::internal(e.to_string())),
         }
diff --git a/tvix/build/src/proto/mod.rs b/tvix/build/src/proto/mod.rs
index 0e106bb4cf63..be757bb56272 100644
--- a/tvix/build/src/proto/mod.rs
+++ b/tvix/build/src/proto/mod.rs
@@ -118,6 +118,57 @@ where
     data.tuple_windows().all(|(a, b)| a <= b)
 }
 
+fn path_to_string(path: &Path) -> String {
+    path.to_str()
+        .expect("Tvix Bug: unable to convert Path to String")
+        .to_string()
+}
+
+impl From<crate::buildservice::BuildRequest> for BuildRequest {
+    fn from(value: crate::buildservice::BuildRequest) -> Self {
+        let constraints = if value.constraints.is_empty() {
+            None
+        } else {
+            let mut constraints = build_request::BuildConstraints::default();
+            for constraint in value.constraints {
+                use crate::buildservice::BuildConstraints;
+                match constraint {
+                    BuildConstraints::System(system) => constraints.system = system,
+                    BuildConstraints::MinMemory(min_memory) => constraints.min_memory = min_memory,
+                    BuildConstraints::AvailableReadOnlyPath(path) => {
+                        constraints.available_ro_paths.push(path_to_string(&path))
+                    }
+                    BuildConstraints::ProvideBinSh => constraints.provide_bin_sh = true,
+                    BuildConstraints::NetworkAccess => constraints.network_access = true,
+                }
+            }
+            Some(constraints)
+        };
+        Self {
+            inputs: value
+                .inputs
+                .into_iter()
+                .map(|(name, node)| {
+                    tvix_castore::proto::Node::from_name_and_node(name.into(), node)
+                })
+                .collect(),
+            command_args: value.command_args,
+            working_dir: path_to_string(&value.working_dir),
+            scratch_paths: value
+                .scratch_paths
+                .iter()
+                .map(|p| path_to_string(p))
+                .collect(),
+            inputs_dir: path_to_string(&value.inputs_dir),
+            outputs: value.outputs.iter().map(|p| path_to_string(p)).collect(),
+            environment_vars: value.environment_vars.into_iter().map(Into::into).collect(),
+            constraints,
+            additional_files: value.additional_files.into_iter().map(Into::into).collect(),
+            refscan_needles: value.refscan_needles,
+        }
+    }
+}
+
 impl TryFrom<BuildRequest> for crate::buildservice::BuildRequest {
     type Error = ValidateBuildRequestError;
     fn try_from(value: BuildRequest) -> Result<Self, Self::Error> {
diff --git a/tvix/glue/src/tvix_build.rs b/tvix/glue/src/tvix_build.rs
index 6a3c6ab40ccc..fa73224992e6 100644
--- a/tvix/glue/src/tvix_build.rs
+++ b/tvix/glue/src/tvix_build.rs
@@ -1,16 +1,13 @@
 //! This module contains glue code translating from
-//! [nix_compat::derivation::Derivation] to [tvix_build::proto::BuildRequest].
+//! [nix_compat::derivation::Derivation] to [tvix_build::buildservice::BuildRequest].
 
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashSet};
+use std::path::PathBuf;
 
 use bytes::Bytes;
 use nix_compat::{derivation::Derivation, nixbase32, store_path::StorePath};
 use sha2::{Digest, Sha256};
-use tvix_build::buildservice::BuildRequest;
-use tvix_build::proto::{
-    self,
-    build_request::{AdditionalFile, BuildConstraints, EnvVar},
-};
+use tvix_build::buildservice::{AdditionalFile, BuildConstraints, BuildRequest, EnvVar};
 use tvix_castore::Node;
 
 /// These are the environment variables that Nix sets in its sandbox for every
@@ -31,7 +28,7 @@ const NIX_ENVIRONMENT_VARS: [(&str, &str); 12] = [
 ];
 
 /// Get an iterator of store paths whose nixbase32 hashes will be the needles for refscanning
-/// Importantly, the returned order will match the one used by derivation_to_build_request
+/// Importantly, the returned order will match the one used by [derivation_to_build_request]
 /// so users may use this function to map back from the found needles to a store path
 pub(crate) fn get_refscan_needles(
     derivation: &Derivation,
@@ -44,7 +41,7 @@ pub(crate) fn get_refscan_needles(
         .chain(derivation.input_derivations.keys())
 }
 
-/// Takes a [Derivation] and turns it into a [proto::BuildRequest].
+/// Takes a [Derivation] and turns it into a [buildservice::BuildRequest].
 /// It assumes the Derivation has been validated.
 /// It needs two lookup functions:
 /// - one translating input sources to a castore node
@@ -53,8 +50,8 @@ pub(crate) fn get_refscan_needles(
 ///   castore nodes of the selected outpus (`fn_input_drvs_to_output_nodes`).
 pub(crate) fn derivation_to_build_request(
     derivation: &Derivation,
-    inputs: BTreeMap<bytes::Bytes, Node>,
-) -> std::io::Result<proto::BuildRequest> {
+    inputs: BTreeMap<StorePath<String>, Node>,
+) -> std::io::Result<BuildRequest> {
     debug_assert!(derivation.validate(true).is_ok(), "drv must validate");
 
     // produce command_args, which is builder and arguments in a Vec.
@@ -62,16 +59,6 @@ pub(crate) fn derivation_to_build_request(
     command_args.push(derivation.builder.clone());
     command_args.extend_from_slice(&derivation.arguments);
 
-    // produce output_paths, which is the absolute path of each output (sorted)
-    let mut output_paths: Vec<String> = derivation
-        .outputs
-        .values()
-        .map(|e| e.path_str()[1..].to_owned())
-        .collect();
-
-    // Sort the outputs. We can use sort_unstable, as these are unique strings.
-    output_paths.sort_unstable();
-
     // Produce environment_vars and additional files.
     // We use a BTreeMap while producing, and only realize the resulting Vec
     // while populating BuildRequest, so we don't need to worry about ordering.
@@ -100,28 +87,41 @@ pub(crate) fn derivation_to_build_request(
     // TODO: handle __json (structured attrs, provide JSON file and source-able bash script)
 
     // Produce constraints.
-    let constraints = Some(BuildConstraints {
-        system: derivation.system.clone(),
-        min_memory: 0,
-        available_ro_paths: vec![],
-        // in case this is a fixed-output derivation, allow network access.
-        network_access: derivation.outputs.len() == 1
-            && derivation
-                .outputs
-                .get("out")
-                .expect("invalid derivation")
-                .is_fixed(),
-        provide_bin_sh: true,
-    });
+    let mut constraints = HashSet::from([
+        BuildConstraints::System(derivation.system.clone()),
+        BuildConstraints::ProvideBinSh,
+    ]);
+
+    if derivation.outputs.len() == 1
+        && derivation
+            .outputs
+            .get("out")
+            .expect("Tvix bug: Derivation has no out output")
+            .is_fixed()
+    {
+        constraints.insert(BuildConstraints::NetworkAccess);
+    }
 
-    let build_request = proto::BuildRequest {
+    Ok(BuildRequest {
         // Importantly, this must match the order of get_refscan_needles, since users may use that
         // function to map back from the found needles to a store path
         refscan_needles: get_refscan_needles(derivation)
             .map(|path| nixbase32::encode(path.digest()))
             .collect(),
         command_args,
-        outputs: output_paths,
+
+        outputs: {
+            // produce output_paths, which is the absolute path of each output (sorted)
+            let mut output_paths: Vec<PathBuf> = derivation
+                .outputs
+                .values()
+                .map(|e| PathBuf::from(e.path_str()[1..].to_owned()))
+                .collect();
+
+            // Sort the outputs. We can use sort_unstable, as these are unique strings.
+            output_paths.sort_unstable();
+            output_paths
+        },
 
         // Turn this into a sorted-by-key Vec<EnvVar>.
         environment_vars: environment_vars
@@ -130,7 +130,15 @@ pub(crate) fn derivation_to_build_request(
             .collect(),
         inputs: inputs
             .into_iter()
-            .map(|(name, node)| tvix_castore::proto::Node::from_name_and_node(name, node))
+            .map(|(path, node)| {
+                (
+                    path.to_string()
+                        .as_str()
+                        .try_into()
+                        .expect("Tvix bug: unable to convert store path basename to PathComponent"),
+                    node,
+                )
+            })
             .collect(),
         inputs_dir: nix_compat::store_path::STORE_DIR[1..].into(),
         constraints,
@@ -138,17 +146,12 @@ pub(crate) fn derivation_to_build_request(
         scratch_paths: vec!["build".into(), "nix/store".into()],
         additional_files: additional_files
             .into_iter()
-            .map(|(path, contents)| AdditionalFile { path, contents })
+            .map(|(path, contents)| AdditionalFile {
+                path: PathBuf::from(path),
+                contents,
+            })
             .collect(),
-    };
-
-    // FUTUREWORK: switch this function to construct the stricter BuildRequest directly.
-    debug_assert!(
-        BuildRequest::try_from(build_request.clone()).is_ok(),
-        "Tvix bug: BuildRequest would not be valid"
-    );
-
-    Ok(build_request)
+    })
 }
 
 /// handle passAsFile, if set.
@@ -212,15 +215,13 @@ fn calculate_pass_as_file_env(k: &str) -> (String, String) {
 #[cfg(test)]
 mod test {
     use bytes::Bytes;
-    use nix_compat::derivation::Derivation;
-    use std::collections::BTreeMap;
+    use nix_compat::{derivation::Derivation, store_path::StorePath};
+    use std::collections::{BTreeMap, HashSet};
     use std::sync::LazyLock;
-    use tvix_build::proto::{
-        build_request::{AdditionalFile, BuildConstraints, EnvVar},
-        BuildRequest,
-    };
     use tvix_castore::fixtures::DUMMY_DIGEST;
-    use tvix_castore::Node;
+    use tvix_castore::{Node, PathComponent};
+
+    use tvix_build::buildservice::{AdditionalFile, BuildConstraints, BuildRequest, EnvVar};
 
     use crate::tvix_build::NIX_ENVIRONMENT_VARS;
 
@@ -242,7 +243,10 @@ mod test {
 
         let build_request = derivation_to_build_request(
             &derivation,
-            BTreeMap::from([(INPUT_NODE_FOO_NAME.clone(), INPUT_NODE_FOO.clone())]),
+            BTreeMap::from([(
+                StorePath::<String>::from_bytes(&INPUT_NODE_FOO_NAME.clone()).unwrap(),
+                INPUT_NODE_FOO.clone(),
+            )]),
         )
         .expect("must succeed");
 
@@ -281,18 +285,15 @@ mod test {
                 command_args: vec![":".into()],
                 outputs: vec!["nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo".into()],
                 environment_vars: expected_environment_vars,
-                inputs: vec![tvix_castore::proto::Node::from_name_and_node(
-                    INPUT_NODE_FOO_NAME.clone(),
+                inputs: BTreeMap::from([(
+                    PathComponent::try_from(INPUT_NODE_FOO_NAME.clone()).unwrap(),
                     INPUT_NODE_FOO.clone()
-                )],
+                )]),
                 inputs_dir: "nix/store".into(),
-                constraints: Some(BuildConstraints {
-                    system: derivation.system.clone(),
-                    min_memory: 0,
-                    network_access: false,
-                    available_ro_paths: vec![],
-                    provide_bin_sh: true,
-                }),
+                constraints: HashSet::from([
+                    BuildConstraints::System(derivation.system.clone()),
+                    BuildConstraints::ProvideBinSh
+                ]),
                 additional_files: vec![],
                 working_dir: "build".into(),
                 scratch_paths: vec!["build".into(), "nix/store".into()],
@@ -357,15 +358,13 @@ mod test {
                 command_args: vec![":".to_string()],
                 outputs: vec!["nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar".into()],
                 environment_vars: expected_environment_vars,
-                inputs: vec![],
+                inputs: BTreeMap::new(),
                 inputs_dir: "nix/store".into(),
-                constraints: Some(BuildConstraints {
-                    system: derivation.system.clone(),
-                    min_memory: 0,
-                    network_access: true,
-                    available_ro_paths: vec![],
-                    provide_bin_sh: true,
-                }),
+                constraints: HashSet::from([
+                    BuildConstraints::System(derivation.system.clone()),
+                    BuildConstraints::NetworkAccess,
+                    BuildConstraints::ProvideBinSh
+                ]),
                 additional_files: vec![],
                 working_dir: "build".into(),
                 scratch_paths: vec!["build".into(), "nix/store".into()],
@@ -431,15 +430,12 @@ mod test {
                 command_args: vec![":".to_string()],
                 outputs: vec!["nix/store/pp17lwra2jkx8rha15qabg2q3wij72lj-foo".into()],
                 environment_vars: expected_environment_vars,
-                inputs: vec![],
+                inputs: BTreeMap::new(),
                 inputs_dir: "nix/store".into(),
-                constraints: Some(BuildConstraints {
-                    system: derivation.system.clone(),
-                    min_memory: 0,
-                    network_access: false,
-                    available_ro_paths: vec![],
-                    provide_bin_sh: true,
-                }),
+                constraints: HashSet::from([
+                    BuildConstraints::System(derivation.system.clone()),
+                    BuildConstraints::ProvideBinSh,
+                ]),
                 additional_files: vec![
                     // baz env
                     AdditionalFile {
diff --git a/tvix/glue/src/tvix_store_io.rs b/tvix/glue/src/tvix_store_io.rs
index c1fe53aa873f..67a88e13c54b 100644
--- a/tvix/glue/src/tvix_store_io.rs
+++ b/tvix/glue/src/tvix_store_io.rs
@@ -1,5 +1,4 @@
 //! This module provides an implementation of EvalIO talking to tvix-store.
-use bytes::Bytes;
 use futures::{StreamExt, TryStreamExt};
 use nix_compat::{nixhash::CAHash, store_path::StorePath};
 use std::collections::BTreeMap;
@@ -181,7 +180,7 @@ impl TvixStoreIO {
                         // derivation_to_build_request needs castore nodes for all inputs.
                         // Provide them, which means, here is where we recursively build
                         // all dependencies.
-                        let mut inputs: BTreeMap<Bytes, Node> =
+                        let mut inputs: BTreeMap<StorePath<String>, Node> =
                             futures::stream::iter(drv.input_derivations.iter())
                                 .map(|(input_drv_path, output_names)| {
                                     // look up the derivation object
@@ -224,7 +223,7 @@ impl TvixStoreIO {
                                                 .await?;
 
                                             if let Some(node) = node {
-                                                Ok((output_path.to_string().into(), node))
+                                                Ok((output_path, node))
                                             } else {
                                                 Err(io::Error::other("no node produced"))
                                             }
@@ -250,7 +249,7 @@ impl TvixStoreIO {
                                                 .store_path_to_node(&input_source, Path::new(""))
                                                 .await?;
                                             if let Some(node) = node {
-                                                Ok((input_source.to_string().into(), node))
+                                                Ok((input_source, node))
                                             } else {
                                                 Err(io::Error::other("no node produced"))
                                             }