about summary refs log tree commit diff
path: root/tvix/glue/src/tvix_build.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tvix/glue/src/tvix_build.rs')
-rw-r--r--tvix/glue/src/tvix_build.rs439
1 files changed, 439 insertions, 0 deletions
diff --git a/tvix/glue/src/tvix_build.rs b/tvix/glue/src/tvix_build.rs
new file mode 100644
index 0000000000..e9eb1725ef
--- /dev/null
+++ b/tvix/glue/src/tvix_build.rs
@@ -0,0 +1,439 @@
+//! This module contains glue code translating from
+//! [nix_compat::derivation::Derivation] to [tvix_build::proto::BuildRequest].
+
+use std::collections::{BTreeMap, BTreeSet};
+
+use bytes::Bytes;
+use nix_compat::{derivation::Derivation, nixbase32};
+use sha2::{Digest, Sha256};
+use tvix_build::proto::{
+    build_request::{AdditionalFile, BuildConstraints, EnvVar},
+    BuildRequest,
+};
+use tvix_castore::proto::{self, node::Node};
+
+/// These are the environment variables that Nix sets in its sandbox for every
+/// build.
+const NIX_ENVIRONMENT_VARS: [(&str, &str); 12] = [
+    ("HOME", "/homeless-shelter"),
+    ("NIX_BUILD_CORES", "0"), // TODO: make this configurable?
+    ("NIX_BUILD_TOP", "/"),
+    ("NIX_LOG_FD", "2"),
+    ("NIX_STORE", "/nix/store"),
+    ("PATH", "/path-not-set"),
+    ("PWD", "/build"),
+    ("TEMP", "/build"),
+    ("TEMPDIR", "/build"),
+    ("TERM", "xterm-256color"),
+    ("TMP", "/build"),
+    ("TMPDIR", "/build"),
+];
+
+/// Takes a [Derivation] and turns it into a [BuildRequest].
+/// It assumes the Derivation has been validated.
+/// It needs two lookup functions:
+/// - one translating input sources to a castore node
+///   (`fn_input_sources_to_node`)
+/// - one translating a tuple of drv path and (a subset of their) output names to
+///   castore nodes of the selected outpus (`fn_input_drvs_to_output_nodes`).
+#[allow(clippy::mutable_key_type)]
+pub(crate) fn derivation_to_build_request(
+    derivation: &Derivation,
+    inputs: BTreeSet<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.
+    let mut command_args: Vec<String> = Vec::with_capacity(derivation.arguments.len() + 1);
+    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.
+    let mut environment_vars: BTreeMap<String, Bytes> = BTreeMap::new();
+    let mut additional_files: BTreeMap<String, Bytes> = BTreeMap::new();
+
+    // Start with some the ones that nix magically sets:
+    environment_vars.extend(
+        NIX_ENVIRONMENT_VARS
+            .iter()
+            .map(|(k, v)| (k.to_string(), Bytes::from_static(v.as_bytes()))),
+    );
+
+    // extend / overwrite with the keys set in the derivation environment itself.
+    // TODO: check if this order is correct, and environment vars set in the
+    // *Derivation actually* have priority.
+    environment_vars.extend(
+        derivation
+            .environment
+            .iter()
+            .map(|(k, v)| (k.clone(), Bytes::from(v.to_vec()))),
+    );
+
+    handle_pass_as_file(&mut environment_vars, &mut additional_files)?;
+
+    // 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 build_request = BuildRequest {
+        command_args,
+        outputs: output_paths,
+
+        // Turn this into a sorted-by-key Vec<EnvVar>.
+        environment_vars: environment_vars
+            .into_iter()
+            .map(|(key, value)| EnvVar { key, value })
+            .collect(),
+        inputs: inputs
+            .into_iter()
+            .map(|n| proto::Node { node: Some(n) })
+            .collect(),
+        inputs_dir: nix_compat::store_path::STORE_DIR[1..].into(),
+        constraints,
+        working_dir: "build".into(),
+        scratch_paths: vec!["build".into(), "nix/store".into()],
+        additional_files: additional_files
+            .into_iter()
+            .map(|(path, contents)| AdditionalFile { path, contents })
+            .collect(),
+    };
+
+    debug_assert!(
+        build_request.validate().is_ok(),
+        "invalid BuildRequest: {}",
+        build_request.validate().unwrap_err()
+    );
+
+    Ok(build_request)
+}
+
+/// handle passAsFile, if set.
+/// For each env $x in that list, the original env is removed, and a $xPath
+/// environment var added instead, referring to a path inside the build with
+/// the contents from the original env var.
+fn handle_pass_as_file(
+    environment_vars: &mut BTreeMap<String, Bytes>,
+    additional_files: &mut BTreeMap<String, Bytes>,
+) -> std::io::Result<()> {
+    let pass_as_file = environment_vars.get("passAsFile").map(|v| {
+        // Convert pass_as_file to string.
+        // When it gets here, it contains a space-separated list of env var
+        // keys, which must be strings.
+        String::from_utf8(v.to_vec())
+    });
+
+    if let Some(pass_as_file) = pass_as_file {
+        let pass_as_file = pass_as_file.map_err(|_| {
+            std::io::Error::new(
+                std::io::ErrorKind::InvalidInput,
+                "passAsFile elements are no valid utf8 strings",
+            )
+        })?;
+
+        for x in pass_as_file.split(' ') {
+            match environment_vars.remove_entry(x) {
+                Some((k, contents)) => {
+                    let (new_k, path) = calculate_pass_as_file_env(&k);
+
+                    additional_files.insert(path[1..].to_string(), contents);
+                    environment_vars.insert(new_k, Bytes::from(path));
+                }
+                None => {
+                    return Err(std::io::Error::new(
+                        std::io::ErrorKind::InvalidData,
+                        "passAsFile refers to non-existent env key",
+                    ));
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// For a given key k in a derivation environment that's supposed to be passed as file,
+/// calculate the ${k}Path key and filepath value that it's being replaced with
+/// while preparing the build.
+/// The filepath is `/build/.attrs-${nixbase32(sha256(key))`.
+fn calculate_pass_as_file_env(k: &str) -> (String, String) {
+    (
+        format!("{}Path", k),
+        format!(
+            "/build/.attr-{}",
+            nixbase32::encode(&Sha256::new_with_prefix(k).finalize())
+        ),
+    )
+}
+
+#[cfg(test)]
+mod test {
+    use std::collections::BTreeSet;
+
+    use bytes::Bytes;
+    use nix_compat::derivation::Derivation;
+    use tvix_build::proto::{
+        build_request::{AdditionalFile, BuildConstraints, EnvVar},
+        BuildRequest,
+    };
+    use tvix_castore::{
+        fixtures::DUMMY_DIGEST,
+        proto::{self, node::Node, DirectoryNode},
+    };
+
+    use crate::tvix_build::NIX_ENVIRONMENT_VARS;
+
+    use super::derivation_to_build_request;
+    use lazy_static::lazy_static;
+
+    lazy_static! {
+        static ref INPUT_NODE_FOO: Node = Node::Directory(DirectoryNode {
+            name: Bytes::from("mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"),
+            digest: DUMMY_DIGEST.clone().into(),
+            size: 42,
+        });
+    }
+
+    #[test]
+    fn test_derivation_to_build_request() {
+        let aterm_bytes = include_bytes!("tests/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv");
+
+        let derivation = Derivation::from_aterm_bytes(aterm_bytes).expect("must parse");
+
+        let build_request =
+            derivation_to_build_request(&derivation, BTreeSet::from([INPUT_NODE_FOO.clone()]))
+                .expect("must succeed");
+
+        let mut expected_environment_vars = vec![
+            EnvVar {
+                key: "bar".into(),
+                value: "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar".into(),
+            },
+            EnvVar {
+                key: "builder".into(),
+                value: ":".into(),
+            },
+            EnvVar {
+                key: "name".into(),
+                value: "foo".into(),
+            },
+            EnvVar {
+                key: "out".into(),
+                value: "/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo".into(),
+            },
+            EnvVar {
+                key: "system".into(),
+                value: ":".into(),
+            },
+        ];
+
+        expected_environment_vars.extend(NIX_ENVIRONMENT_VARS.iter().map(|(k, v)| EnvVar {
+            key: k.to_string(),
+            value: Bytes::from_static(v.as_bytes()),
+        }));
+
+        expected_environment_vars.sort_unstable_by_key(|e| e.key.to_owned());
+
+        assert_eq!(
+            BuildRequest {
+                command_args: vec![":".into()],
+                outputs: vec!["nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo".into()],
+                environment_vars: expected_environment_vars,
+                inputs: vec![proto::Node {
+                    node: Some(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,
+                }),
+                additional_files: vec![],
+                working_dir: "build".into(),
+                scratch_paths: vec!["build".into(), "nix/store".into()],
+            },
+            build_request
+        );
+    }
+
+    #[test]
+    fn test_fod_to_build_request() {
+        let aterm_bytes = include_bytes!("tests/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv");
+
+        let derivation = Derivation::from_aterm_bytes(aterm_bytes).expect("must parse");
+
+        let build_request =
+            derivation_to_build_request(&derivation, BTreeSet::from([])).expect("must succeed");
+
+        let mut expected_environment_vars = vec![
+            EnvVar {
+                key: "builder".into(),
+                value: ":".into(),
+            },
+            EnvVar {
+                key: "name".into(),
+                value: "bar".into(),
+            },
+            EnvVar {
+                key: "out".into(),
+                value: "/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar".into(),
+            },
+            EnvVar {
+                key: "outputHash".into(),
+                value: "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba".into(),
+            },
+            EnvVar {
+                key: "outputHashAlgo".into(),
+                value: "sha256".into(),
+            },
+            EnvVar {
+                key: "outputHashMode".into(),
+                value: "recursive".into(),
+            },
+            EnvVar {
+                key: "system".into(),
+                value: ":".into(),
+            },
+        ];
+
+        expected_environment_vars.extend(NIX_ENVIRONMENT_VARS.iter().map(|(k, v)| EnvVar {
+            key: k.to_string(),
+            value: Bytes::from_static(v.as_bytes()),
+        }));
+
+        expected_environment_vars.sort_unstable_by_key(|e| e.key.to_owned());
+
+        assert_eq!(
+            BuildRequest {
+                command_args: vec![":".to_string()],
+                outputs: vec!["nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar".into()],
+                environment_vars: expected_environment_vars,
+                inputs: vec![],
+                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,
+                }),
+                additional_files: vec![],
+                working_dir: "build".into(),
+                scratch_paths: vec!["build".into(), "nix/store".into()],
+            },
+            build_request
+        );
+    }
+
+    #[test]
+    fn test_pass_as_file() {
+        // (builtins.derivation { "name" = "foo"; passAsFile = ["bar" "baz"]; bar = "baz"; baz = "bar"; system = ":"; builder = ":";}).drvPath
+        let aterm_bytes = r#"Derive([("out","/nix/store/pp17lwra2jkx8rha15qabg2q3wij72lj-foo","","")],[],[],":",":",[],[("bar","baz"),("baz","bar"),("builder",":"),("name","foo"),("out","/nix/store/pp17lwra2jkx8rha15qabg2q3wij72lj-foo"),("passAsFile","bar baz"),("system",":")])"#.as_bytes();
+
+        let derivation = Derivation::from_aterm_bytes(aterm_bytes).expect("must parse");
+
+        let build_request =
+            derivation_to_build_request(&derivation, BTreeSet::from([])).expect("must succeed");
+
+        let mut expected_environment_vars = vec![
+            // Note how bar and baz are not present in the env anymore,
+            // but replaced with barPath, bazPath respectively.
+            EnvVar {
+                key: "barPath".into(),
+                value: "/build/.attr-1fcgpy7vc4ammr7s17j2xq88scswkgz23dqzc04g8sx5vcp2pppw".into(),
+            },
+            EnvVar {
+                key: "bazPath".into(),
+                value: "/build/.attr-15l04iksj1280dvhbzdq9ai3wlf8ac2188m9qv0gn81k9nba19ds".into(),
+            },
+            EnvVar {
+                key: "builder".into(),
+                value: ":".into(),
+            },
+            EnvVar {
+                key: "name".into(),
+                value: "foo".into(),
+            },
+            EnvVar {
+                key: "out".into(),
+                value: "/nix/store/pp17lwra2jkx8rha15qabg2q3wij72lj-foo".into(),
+            },
+            // passAsFile stays around
+            EnvVar {
+                key: "passAsFile".into(),
+                value: "bar baz".into(),
+            },
+            EnvVar {
+                key: "system".into(),
+                value: ":".into(),
+            },
+        ];
+
+        expected_environment_vars.extend(NIX_ENVIRONMENT_VARS.iter().map(|(k, v)| EnvVar {
+            key: k.to_string(),
+            value: Bytes::from_static(v.as_bytes()),
+        }));
+
+        expected_environment_vars.sort_unstable_by_key(|e| e.key.to_owned());
+
+        assert_eq!(
+            BuildRequest {
+                command_args: vec![":".to_string()],
+                outputs: vec!["nix/store/pp17lwra2jkx8rha15qabg2q3wij72lj-foo".into()],
+                environment_vars: expected_environment_vars,
+                inputs: vec![],
+                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,
+                }),
+                additional_files: vec![
+                    // baz env
+                    AdditionalFile {
+                        path: "build/.attr-15l04iksj1280dvhbzdq9ai3wlf8ac2188m9qv0gn81k9nba19ds"
+                            .into(),
+                        contents: "bar".into()
+                    },
+                    // bar env
+                    AdditionalFile {
+                        path: "build/.attr-1fcgpy7vc4ammr7s17j2xq88scswkgz23dqzc04g8sx5vcp2pppw"
+                            .into(),
+                        contents: "baz".into(),
+                    },
+                ],
+                working_dir: "build".into(),
+                scratch_paths: vec!["build".into(), "nix/store".into()],
+            },
+            build_request
+        );
+    }
+}