Skip to main content

snix_build_glue/builder/
mod.rs

1//! This module contains glue code translating from
2//! [nix_compat::derivation::Derivation] to [snix_build::buildservice::BuildRequest].
3
4use std::collections::{BTreeMap, HashSet, VecDeque};
5use std::future::Future;
6use std::path::PathBuf;
7
8use async_stream::try_stream;
9use bstr::BString;
10use bytes::Bytes;
11use futures::Stream;
12use nix_compat::derivation::{OutputName, UnverifiedDerivation};
13use nix_compat::nixhash::Sha256;
14use nix_compat::store_path::hash_placeholder;
15use nix_compat::{derivation::Derivation, nixbase32, store_path::StorePath};
16use snix_build::buildservice::{AdditionalFile, BuildConstraints, BuildRequest, EnvVar};
17use snix_castore::Node;
18use snix_store::path_info::PathInfo;
19
20use crate::builder::structured_attrs::handle_structured_attrs;
21use crate::known_paths::KnownPaths;
22
23pub mod structured_attrs;
24
25/// These are the environment variables that Nix sets in its sandbox for every
26/// build.
27const NIX_ENVIRONMENT_VARS: [(&str, &str); 12] = [
28    ("HOME", "/homeless-shelter"),
29    ("NIX_BUILD_CORES", "0"), // TODO: make this configurable?
30    ("NIX_BUILD_TOP", "/build"),
31    ("NIX_LOG_FD", "2"),
32    ("NIX_STORE", "/nix/store"),
33    ("PATH", "/path-not-set"),
34    ("PWD", "/build"),
35    ("TEMP", "/build"),
36    ("TEMPDIR", "/build"),
37    ("TERM", "xterm-256color"),
38    ("TMP", "/build"),
39    ("TMPDIR", "/build"),
40];
41
42/// Get a stream of a transitive input closure for a derivation.
43/// It's used for input propagation into the build and nixbase32 needle propagation
44/// for build output refscanning.
45pub(crate) fn get_all_inputs<'a, F, Fut>(
46    derivation: &'a Derivation,
47    known_paths: &'a KnownPaths,
48    get_path_info: F,
49) -> impl Stream<Item = Result<(StorePath, Node), std::io::Error>> + use<F, Fut>
50where
51    F: Fn(StorePath) -> Fut,
52    Fut: Future<Output = std::io::Result<Option<PathInfo>>>,
53{
54    let mut visited: HashSet<StorePath> = HashSet::new();
55    let mut queue: VecDeque<StorePath> = derivation
56        .input_sources()
57        .iter()
58        .cloned()
59        .chain(
60            derivation
61                .input_derivations()
62                .iter()
63                .flat_map(|(drv_path, outs)| {
64                    let drv = known_paths
65                        .get_drv_by_drvpath(&drv_path.as_ref())
66                        .expect("drv Bug!!");
67                    outs.iter().map(move |output| {
68                        drv.outputs().get(output).expect("No output bug!").clone()
69                    })
70                }),
71        )
72        .collect();
73    try_stream! {
74        while let Some(store_path) = queue.pop_front() {
75                let info = get_path_info(store_path).await?.ok_or(std::io::Error::other("path_info not present"))?;
76                    for reference in info.references {
77                        if visited.insert(reference.clone()) {
78                            queue.push_back(reference);
79                        }
80                    }
81
82                    yield (info.store_path, info.node);
83
84
85        }
86    }
87}
88
89/// Takes a [`UnverifiedDerivation`] and turns it into a [`BuildRequest`].
90///
91/// **NOTE:** This takes a [`UnverifiedDerivation`] and not a [`Derivation`] because we want
92/// to support the `BuildDerivation` daemon operation. And that operation sends a modified derivation
93/// where `input_derivations` have been converted to output store paths and moved to `input_sources`
94/// which can therefore not be verified.
95pub(crate) fn derivation_into_build_request<D: Into<UnverifiedDerivation>>(
96    derivation: D,
97    inputs: &BTreeMap<StorePath, Node>,
98) -> std::io::Result<BuildRequest> {
99    let derivation = derivation.into();
100
101    // produce command_args, which is builder and arguments in a Vec, replacing any placeholders.
102    let command_args: Vec<String> = Vec::from_iter(
103        std::iter::once(derivation.command())
104            .chain(derivation.arguments().iter().map(|a| a.as_str()))
105            .map(|s| replace_placeholders(s, derivation.outputs())),
106    );
107
108    // Produce environment_vars and additional files.
109    // We use a BTreeMap while producing, and only realize the resulting Vec
110    // while populating BuildRequest, so we don't need to worry about ordering.
111    let mut environment_vars: BTreeMap<String, Vec<u8>> = BTreeMap::new();
112    let mut additional_files: BTreeMap<String, Bytes> = BTreeMap::new();
113
114    // Start with some the ones that nix magically sets:
115    environment_vars.extend(
116        NIX_ENVIRONMENT_VARS
117            .iter()
118            .map(|(k, v)| (k.to_string(), v.to_owned().into())),
119    );
120
121    if let Some(json_str) = derivation.environment().get(structured_attrs::JSON_KEY) {
122        // Replace placeholders directly inside json, if any.
123        let json_str = replace_placeholders_b(json_str, derivation.outputs());
124        handle_structured_attrs(
125            &json_str,
126            derivation
127                .outputs()
128                .iter()
129                .map(|(out_name, path)| (out_name.as_str(), path.as_ref())),
130            &mut environment_vars,
131            &mut additional_files,
132        )?;
133    } else {
134        // If we're not in the structured_attrs case, add other keys set in the
135        // derivation environment itself.
136        environment_vars.extend(derivation.environment().iter().map(|(k, v)| {
137            (
138                k.clone(),
139                replace_placeholders_b(v, derivation.outputs()).into(),
140            )
141        }));
142
143        // passAsFile is only treated specially in the non-SA case.
144        handle_pass_as_file(&mut environment_vars, &mut additional_files)?;
145    }
146
147    // Produce constraints.
148    let mut constraints = HashSet::from([
149        BuildConstraints::System(derivation.system().to_owned()),
150        BuildConstraints::ProvideBinSh,
151    ]);
152
153    if derivation.outputs().is_fixed() {
154        constraints.insert(BuildConstraints::NetworkAccess);
155    }
156
157    Ok(BuildRequest {
158        // Importantly, this must match the order of get_refscan_needles, since users may use that
159        // function to map back from the found needles to a store path
160        refscan_needles: derivation
161            .outputs()
162            .store_paths()
163            .map(|path| nixbase32::encode(path.digest()))
164            .chain(inputs.keys().map(|path| nixbase32::encode(path.digest())))
165            .collect(),
166        command_args,
167
168        outputs: derivation
169            .outputs()
170            .store_paths()
171            .map(|path| path.to_absolute_path()[1..].to_owned())
172            .collect(),
173
174        // Turn this into a sorted-by-key Vec<EnvVar>.
175        environment_vars: environment_vars
176            .into_iter()
177            .map(|(key, value)| EnvVar {
178                key,
179                value: Bytes::from(value),
180            })
181            .collect(),
182        inputs: inputs
183            .iter()
184            .map(|(path, node)| {
185                (
186                    path.to_string()
187                        .as_str()
188                        .try_into()
189                        .expect("Snix bug: unable to convert store path basename to PathComponent"),
190                    node.clone(),
191                )
192            })
193            .collect(),
194        inputs_dir: nix_compat::store_path::STORE_DIR[1..].into(),
195        constraints,
196        working_dir: "build".into(),
197        scratch_paths: vec![
198            "build".into(),
199            // This is in here because Nix allows you to do
200            // `pkgs.runCommand "foo" {} "mkdir -p $out;touch /nix/store/aaaa"`
201            // (throwing away the /nix/store/aaaa post-build),
202            // not because it's a sane thing to do.
203            // FUTUREWORK: check if nothing exploits this.
204            "nix/store".into(),
205        ],
206        additional_files: additional_files
207            .into_iter()
208            .map(|(path, contents)| AdditionalFile {
209                path: PathBuf::from(path),
210                contents,
211            })
212            .collect(),
213    })
214}
215
216/// handle passAsFile, if set.
217/// For each env $x in that list, the original env is removed, and a $xPath
218/// environment var added instead, referring to a path inside the build with
219/// the contents from the original env var.
220fn handle_pass_as_file(
221    environment_vars: &mut BTreeMap<String, Vec<u8>>,
222    additional_files: &mut BTreeMap<String, Bytes>,
223) -> std::io::Result<()> {
224    let pass_as_file = environment_vars.get("passAsFile").map(|v| {
225        // Convert pass_as_file to string.
226        // When it gets here, it contains a space-separated list of env var
227        // keys, which must be strings.
228        String::from_utf8(v.to_vec())
229    });
230
231    if let Some(pass_as_file) = pass_as_file {
232        let pass_as_file = pass_as_file.map_err(|_| {
233            std::io::Error::new(
234                std::io::ErrorKind::InvalidInput,
235                "passAsFile elements are no valid utf8 strings",
236            )
237        })?;
238
239        for x in pass_as_file.split(' ') {
240            match environment_vars.remove_entry(x) {
241                Some((k, contents)) => {
242                    let (new_k, path) = calculate_pass_as_file_env(&k);
243
244                    additional_files.insert(path[1..].to_string(), Bytes::from(contents));
245                    environment_vars.insert(new_k, path.into());
246                }
247                None => {
248                    return Err(std::io::Error::new(
249                        std::io::ErrorKind::InvalidData,
250                        "passAsFile refers to non-existent env key",
251                    ));
252                }
253            }
254        }
255    }
256
257    Ok(())
258}
259
260/// For a given key k in a derivation environment that's supposed to be passed as file,
261/// calculate the ${k}Path key and filepath value that it's being replaced with
262/// while preparing the build.
263/// The filepath is `/build/.attrs-${nixbase32(sha256(key))`.
264fn calculate_pass_as_file_env(k: &str) -> (String, String) {
265    (
266        format!("{k}Path"),
267        format!(
268            "/build/.attr-{}",
269            nixbase32::encode(&Sha256::digest_bytes(k))
270        ),
271    )
272}
273
274/// Replace all references to `placeholder outputName` inside the derivation
275fn replace_placeholders<'i, I>(s: &str, outputs: I) -> String
276where
277    I: IntoIterator<Item = (&'i OutputName, &'i StorePath)>,
278{
279    let mut s = s.to_owned();
280    for (out_name, path) in outputs {
281        let placeholder = hash_placeholder(out_name.as_str());
282        s = s.replace(&placeholder, &path.to_absolute_path());
283    }
284    s
285}
286
287/// Replace all references to `placeholder outputName` inside the derivation
288fn replace_placeholders_b<'i, I>(s: &BString, outputs: I) -> BString
289where
290    I: IntoIterator<Item = (&'i OutputName, &'i StorePath)>,
291{
292    use bstr::ByteSlice;
293    let mut s = s.clone();
294    for (out_name, path) in outputs {
295        let placeholder = hash_placeholder(out_name.as_str());
296        s = s
297            .replace(placeholder.as_bytes(), path.to_absolute_path().as_bytes())
298            .into();
299    }
300    s
301}
302
303#[cfg(test)]
304mod test {
305    use bytes::Bytes;
306    use nix_compat::derivation::UnverifiedDerivation;
307    use nix_compat::store_path::{StorePath, hash_placeholder};
308    use snix_castore::fixtures::DUMMY_DIGEST;
309    use snix_castore::{Node, PathComponent};
310    use std::collections::{BTreeMap, HashSet};
311    use std::sync::LazyLock;
312
313    use snix_build::buildservice::{AdditionalFile, BuildConstraints, BuildRequest, EnvVar};
314
315    use crate::builder::NIX_ENVIRONMENT_VARS;
316    use crate::known_paths::KnownPaths;
317
318    use super::derivation_into_build_request;
319
320    static INPUT_NODE_FOO_NAME: LazyLock<Bytes> =
321        LazyLock::new(|| "mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar".into());
322
323    static INPUT_NODE_FOO: LazyLock<Node> = LazyLock::new(|| Node::Directory {
324        digest: *DUMMY_DIGEST,
325        size: 42,
326    });
327
328    #[test]
329    fn test_derivation_to_build_request() {
330        let aterm_bytes =
331            include_bytes!("../../test-data/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv");
332
333        let dep_drv_bytes =
334            include_bytes!("../../test-data/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv");
335
336        let derivation1 =
337            UnverifiedDerivation::from_aterm_bytes(aterm_bytes).expect("drv1 must parse");
338        let drv_path1 =
339            StorePath::from_bytes("ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv".as_bytes())
340                .expect("drv path1 must parse");
341        let derivation2 =
342            UnverifiedDerivation::from_aterm_bytes(dep_drv_bytes).expect("drv2 must parse");
343        let drv_path2 =
344            StorePath::from_bytes("ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv".as_bytes())
345                .expect("drv path2 must parse");
346
347        let mut known_paths = KnownPaths::default();
348
349        known_paths
350            .add_derivation(drv_path2, derivation2)
351            .expect("must succeed");
352        known_paths
353            .add_derivation(drv_path1, derivation1.clone())
354            .expect("must succeed");
355
356        let build_request = derivation_into_build_request(
357            derivation1.clone(),
358            &BTreeMap::from([(
359                StorePath::from_bytes(&INPUT_NODE_FOO_NAME.clone()).unwrap(),
360                INPUT_NODE_FOO.clone(),
361            )]),
362        )
363        .expect("must succeed");
364
365        let mut expected_environment_vars = BTreeMap::from_iter(NIX_ENVIRONMENT_VARS);
366        expected_environment_vars.extend([
367            ("bar", "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"),
368            ("builder", ":"),
369            ("name", "foo"),
370            ("out", "/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo"),
371            ("system", ":"),
372        ]);
373
374        assert_eq!(
375            BuildRequest {
376                command_args: vec![":".into()],
377                outputs: vec!["nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo".into()],
378                environment_vars: Vec::from_iter(expected_environment_vars.into_iter().map(
379                    |(k, v)| EnvVar {
380                        key: k.into(),
381                        value: v.into(),
382                    }
383                )),
384                inputs: BTreeMap::from([(
385                    PathComponent::try_from(INPUT_NODE_FOO_NAME.clone()).unwrap(),
386                    INPUT_NODE_FOO.clone()
387                )]),
388                inputs_dir: "nix/store".into(),
389                constraints: HashSet::from([
390                    BuildConstraints::System(derivation1.system().to_owned()),
391                    BuildConstraints::ProvideBinSh
392                ]),
393                additional_files: vec![],
394                working_dir: "build".into(),
395                scratch_paths: vec!["build".into(), "nix/store".into()],
396                refscan_needles: vec![
397                    "fhaj6gmwns62s6ypkcldbaj2ybvkhx3p".into(),
398                    "mp57d33657rf34lzvlbpfa1gjfv5gmpg".into()
399                ],
400            },
401            build_request
402        );
403    }
404
405    #[test]
406    fn test_drv_with_placeholders_to_build_request() {
407        let aterm_bytes = include_bytes!(
408            "../../test-data/18m7y1d025lqgrzx8ypnhjbvq23z2kda-with-placeholders.drv"
409        );
410        let derivation = UnverifiedDerivation::from_aterm_bytes(aterm_bytes).expect("must parse");
411
412        let mut expected_environment_vars: BTreeMap<&str, String> =
413            BTreeMap::from_iter(NIX_ENVIRONMENT_VARS.map(|(k, v)| (k, v.to_owned())));
414
415        expected_environment_vars.extend([
416            (
417                "FOO",
418                "/nix/store/dgapb8kh5gis4w7hzfl5725sx5gam0nz-with-placeholders".to_owned(),
419            ),
420            (
421                "BAR",
422                // Non existent output placeholders should not get replaced.
423                hash_placeholder("non-existent"),
424            ),
425            ("builder", "/bin/sh".to_owned()),
426            ("name", "with-placeholders".to_owned()),
427            (
428                "out",
429                "/nix/store/dgapb8kh5gis4w7hzfl5725sx5gam0nz-with-placeholders".to_owned(),
430            ),
431            ("system", "x86_64-linux".to_owned()),
432        ]);
433
434        let exp_build_request = BuildRequest {
435            command_args: vec![
436                "/bin/sh".into(),
437                "-c".into(),
438                "/nix/store/dgapb8kh5gis4w7hzfl5725sx5gam0nz-with-placeholders".into(),
439                // Non existent output placeholders should not get replaced.
440                hash_placeholder("non-existent"),
441            ],
442            outputs: vec!["nix/store/dgapb8kh5gis4w7hzfl5725sx5gam0nz-with-placeholders".into()],
443            environment_vars: Vec::from_iter(expected_environment_vars.into_iter().map(
444                |(k, v)| EnvVar {
445                    key: k.into(),
446                    value: v.into(),
447                },
448            )),
449            inputs: BTreeMap::new(),
450            inputs_dir: "nix/store".into(),
451            constraints: HashSet::from([
452                BuildConstraints::System(derivation.system().to_owned()),
453                BuildConstraints::ProvideBinSh,
454            ]),
455            additional_files: vec![],
456            working_dir: "build".into(),
457            scratch_paths: vec!["build".into(), "nix/store".into()],
458            refscan_needles: vec!["dgapb8kh5gis4w7hzfl5725sx5gam0nz".into()],
459        };
460
461        assert_eq!(
462            exp_build_request,
463            derivation_into_build_request(derivation.clone(), &BTreeMap::from([]))
464                .expect("must succeed"),
465        );
466    }
467
468    #[test]
469    fn test_fod_to_build_request() {
470        let aterm_bytes =
471            include_bytes!("../../test-data/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv");
472
473        let derivation = UnverifiedDerivation::from_aterm_bytes(aterm_bytes).expect("must parse");
474
475        let mut expected_environment_vars = BTreeMap::from_iter(NIX_ENVIRONMENT_VARS);
476        expected_environment_vars.extend([
477            ("builder", ":"),
478            ("name", "bar"),
479            ("out", "/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar"),
480            (
481                "outputHash",
482                "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
483            ),
484            ("outputHashAlgo", "sha256"),
485            ("outputHashMode", "recursive"),
486            ("system", ":"),
487        ]);
488
489        let exp_build_request = BuildRequest {
490            command_args: vec![":".to_string()],
491            outputs: vec!["nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar".into()],
492            environment_vars: Vec::from_iter(expected_environment_vars.into_iter().map(
493                |(k, v)| EnvVar {
494                    key: k.into(),
495                    value: v.into(),
496                },
497            )),
498            inputs: BTreeMap::new(),
499            inputs_dir: "nix/store".into(),
500            constraints: HashSet::from([
501                BuildConstraints::System(derivation.system().to_owned()),
502                BuildConstraints::System(derivation.system().to_owned()),
503                BuildConstraints::NetworkAccess,
504                BuildConstraints::ProvideBinSh,
505            ]),
506            additional_files: vec![],
507            working_dir: "build".into(),
508            scratch_paths: vec!["build".into(), "nix/store".into()],
509            refscan_needles: vec!["4q0pg5zpfmznxscq3avycvf9xdvx50n3".into()],
510        };
511
512        assert_eq!(
513            exp_build_request,
514            derivation_into_build_request(derivation, &BTreeMap::from([])).expect("must succeed")
515        );
516    }
517
518    #[test]
519    fn test_pass_as_file() {
520        // (builtins.derivation { "name" = "foo"; passAsFile = ["bar" "baz"]; bar = "baz"; baz = "bar"; system = ":"; builder = ":";}).drvPath
521        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();
522
523        let derivation = UnverifiedDerivation::from_aterm_bytes(aterm_bytes).expect("must parse");
524
525        let mut expected_environment_vars = BTreeMap::from_iter(NIX_ENVIRONMENT_VARS);
526        expected_environment_vars.extend([
527            // Note how bar and baz are not present in the env anymore,
528            // but replaced with barPath, bazPath respectively.
529            (
530                "barPath",
531                "/build/.attr-1fcgpy7vc4ammr7s17j2xq88scswkgz23dqzc04g8sx5vcp2pppw",
532            ),
533            (
534                "bazPath",
535                "/build/.attr-15l04iksj1280dvhbzdq9ai3wlf8ac2188m9qv0gn81k9nba19ds",
536            ),
537            ("builder", ":"),
538            ("name", "foo"),
539            ("out", "/nix/store/pp17lwra2jkx8rha15qabg2q3wij72lj-foo"),
540            // passAsFile stays around
541            ("passAsFile", "bar baz"),
542            ("system", ":"),
543        ]);
544
545        let exp_build_request = BuildRequest {
546            command_args: vec![":".to_string()],
547            outputs: vec!["nix/store/pp17lwra2jkx8rha15qabg2q3wij72lj-foo".into()],
548            environment_vars: Vec::from_iter(expected_environment_vars.into_iter().map(
549                |(k, v)| EnvVar {
550                    key: k.into(),
551                    value: v.into(),
552                },
553            )),
554            inputs: BTreeMap::new(),
555            inputs_dir: "nix/store".into(),
556            constraints: HashSet::from([
557                BuildConstraints::System(derivation.system().to_owned()),
558                BuildConstraints::ProvideBinSh,
559            ]),
560            additional_files: vec![
561                // baz env
562                AdditionalFile {
563                    path: "build/.attr-15l04iksj1280dvhbzdq9ai3wlf8ac2188m9qv0gn81k9nba19ds".into(),
564                    contents: "bar".into(),
565                },
566                // bar env
567                AdditionalFile {
568                    path: "build/.attr-1fcgpy7vc4ammr7s17j2xq88scswkgz23dqzc04g8sx5vcp2pppw".into(),
569                    contents: "baz".into(),
570                },
571            ],
572            working_dir: "build".into(),
573            scratch_paths: vec!["build".into(), "nix/store".into()],
574            refscan_needles: vec!["pp17lwra2jkx8rha15qabg2q3wij72lj".into()],
575        };
576
577        assert_eq!(
578            exp_build_request,
579            derivation_into_build_request(derivation, &BTreeMap::from([])).expect("must succeed")
580        );
581    }
582
583    #[test]
584    fn test_structured_attrs() {
585        // (builtins.derivation { name = "script.sh"; system = builtins.currentSystem; PATH = lib.makeBinPath [pkgs.coreutils]; ""="bar"; k = {"bar" = true; b =1.0; c = false; d = true;}; l = 42; m = false; n = 1.1; builder="${bash}/bin/bash"; hello = placeholder "out"; args = ["-xc" "source \${NIX_ATTRS_SH_FILE:-/dev/null}; cat \${NIX_ATTRS_JSON_FILE:-/dev/null}; out=\${out:-\${outputs[out]}}; cat \${NIX_ATTRS_JSON_FILE:-/dev/null} >\$out; exit 0"];__structuredAttrs = true;})
586        let aterm_bytes = r#"Derive([("out","/nix/store/knq92bscsfi5xzvhf8icj2kbwddkk5m4-script.sh","","")],[("/nix/store/l6bi2ln3vlv6mkkw95bvh09pgy4d3xra-coreutils-9.10.drv",["out"]),("/nix/store/s9b1a2zhv6l7x8ady5vfbj5kg8rkvznx-bash-interactive-5.3p9.drv",["out"])],[],"x86_64-linux","/nix/store/sfvyavxai6qvzmv9p9x6mp4wwdz4v41m-bash-interactive-5.3p9/bin/bash",["-xc","source ${NIX_ATTRS_SH_FILE:-/dev/null}; cat ${NIX_ATTRS_JSON_FILE:-/dev/null}; out=${out:-${outputs[out]}}; cat ${NIX_ATTRS_JSON_FILE:-/dev/null} >$out; exit 0"],[("__json","{\"\":\"bar\",\"PATH\":\"/nix/store/74sind1d6vf2bfwd7yklg8chsvzqxmmq-coreutils-9.10/bin\",\"builder\":\"/nix/store/sfvyavxai6qvzmv9p9x6mp4wwdz4v41m-bash-interactive-5.3p9/bin/bash\",\"hello\":\"/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9\",\"k\":{\"b\":1.0,\"bar\":true,\"c\":false,\"d\":true},\"l\":42,\"m\":false,\"n\":1.1,\"name\":\"script.sh\",\"system\":\"x86_64-linux\"}"),("out","/nix/store/knq92bscsfi5xzvhf8icj2kbwddkk5m4-script.sh")])"#.as_bytes();
587
588        let derivation = UnverifiedDerivation::from_aterm_bytes(aterm_bytes).expect("must parse");
589
590        let mut expected_environment_vars = BTreeMap::from_iter(NIX_ENVIRONMENT_VARS);
591        expected_environment_vars.extend([
592            ("NIX_ATTRS_JSON_FILE", "/build/.attrs.json"),
593            ("NIX_ATTRS_SH_FILE", "/build/.attrs.sh"),
594            // PATH is `/path-not-set`, all $PATH setup happens by sourcing of /build/.attrs.sh!
595            // Compare with the build log from a structured attr build only calling env:
596            // builtins.derivation { name = "script.sh"; system = builtins.currentSystem; PATH = lib.makeBinPath [pkgs.coreutils]; ""="bar"; k = {"bar" = true; b =1.0; c = false; d = true;}; l = 42; m = false; n = 1.1; builder="${coreutils}/bin/env"; __structuredAttrs = true;}
597            // ```
598            // HOME=/homeless-shelter
599            // NIX_ATTRS_JSON_FILE=/build/.attrs.json
600            // NIX_ATTRS_SH_FILE=/build/.attrs.sh
601            // NIX_BUILD_CORES=0
602            // NIX_BUILD_TOP=/build
603            // NIX_LOG_FD=2
604            // NIX_STORE=/nix/store
605            // PATH=/path-not-set
606            // PWD=/build
607            // TEMP=/build
608            // TEMPDIR=/build
609            // TERM=xterm-256color
610            // TMP=/build
611            // TMPDIR=/build
612            // ```
613        ]);
614
615        let exp_build_request =  BuildRequest {
616                command_args: vec![
617                    "/nix/store/sfvyavxai6qvzmv9p9x6mp4wwdz4v41m-bash-interactive-5.3p9/bin/bash".to_string(),
618                    "-xc".to_string(),
619                    r#"source ${NIX_ATTRS_SH_FILE:-/dev/null}; cat ${NIX_ATTRS_JSON_FILE:-/dev/null}; out=${out:-${outputs[out]}}; cat ${NIX_ATTRS_JSON_FILE:-/dev/null} >$out; exit 0"#.to_string()
620                ],
621                outputs: vec!["nix/store/knq92bscsfi5xzvhf8icj2kbwddkk5m4-script.sh".into()],
622                environment_vars: Vec::from_iter(expected_environment_vars.into_iter().map(
623                    |(k, v)| EnvVar {
624                        key: k.into(),
625                        value: v.into(),
626                    }
627                )),
628                inputs: BTreeMap::new(),
629                inputs_dir: "nix/store".into(),
630                constraints: HashSet::from([
631                BuildConstraints::System(derivation.system().to_owned()),
632                    BuildConstraints::ProvideBinSh,
633                ]),
634                additional_files: vec![
635                    AdditionalFile {
636                        path: "build/.attrs.json".into(),
637                        contents: Bytes::from_static(br#"{"":"bar","PATH":"/nix/store/74sind1d6vf2bfwd7yklg8chsvzqxmmq-coreutils-9.10/bin","builder":"/nix/store/sfvyavxai6qvzmv9p9x6mp4wwdz4v41m-bash-interactive-5.3p9/bin/bash","hello":"/nix/store/knq92bscsfi5xzvhf8icj2kbwddkk5m4-script.sh","k":{"b":1.0,"bar":true,"c":false,"d":true},"l":42,"m":false,"n":1.1,"name":"script.sh","outputs":{"out":"/nix/store/knq92bscsfi5xzvhf8icj2kbwddkk5m4-script.sh"},"system":"x86_64-linux"}"#)
638                    },
639                    AdditionalFile {
640                        path: "build/.attrs.sh".into(),
641                        contents: Bytes::from_static(br#"declare PATH='/nix/store/74sind1d6vf2bfwd7yklg8chsvzqxmmq-coreutils-9.10/bin'
642declare builder='/nix/store/sfvyavxai6qvzmv9p9x6mp4wwdz4v41m-bash-interactive-5.3p9/bin/bash'
643declare hello='/nix/store/knq92bscsfi5xzvhf8icj2kbwddkk5m4-script.sh'
644declare -A k=(['b']=1 ['bar']=1 ['c']= ['d']=1 )
645declare l=42
646declare m=
647declare name='script.sh'
648declare -A outputs=(['out']='/nix/store/knq92bscsfi5xzvhf8icj2kbwddkk5m4-script.sh' )
649declare system='x86_64-linux'
650"#)
651                    }
652                ],
653                working_dir: "build".into(),
654                scratch_paths: vec!["build".into(), "nix/store".into()],
655                refscan_needles: vec!["knq92bscsfi5xzvhf8icj2kbwddkk5m4".into()],
656            };
657
658        assert_eq!(
659            exp_build_request,
660            derivation_into_build_request(derivation, &BTreeMap::from([])).expect("must succeed")
661        );
662    }
663}