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