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