Skip to main content

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