Skip to main content

snix_glue/builtins/
derivation.rs

1//! Implements `builtins.derivation`, the core of what makes Nix build packages.
2use crate::builtins::DerivationError;
3use crate::snix_store_io::SnixStoreIO;
4use bstr::BString;
5use nix_compat::derivation::{Derivation, OutputHash, OutputName};
6use nix_compat::store_path::{StorePath, StorePathRef};
7use snix_build_glue::known_paths::KnownPaths;
8use snix_eval::builtin_macros::builtins;
9use snix_eval::generators::{self, GenCo, emit_warning_kind};
10use snix_eval::{
11    AddContext, ErrorKind, NixAttrs, NixContext, NixContextElement, Value, WarningKind,
12};
13use std::collections::{BTreeSet, btree_map};
14use std::rc::Rc;
15
16// Constants used for strangely named fields in derivation inputs.
17const IGNORE_NULLS: &str = "__ignoreNulls";
18pub const STRUCTURED_ATTRS_ENABLE_KEY: &str = "__structuredAttrs";
19
20/// Populate the inputs of a derivation from the build references
21/// found when scanning the derivation's parameters and extracting their contexts.
22fn populate_inputs(drv: &mut Derivation, full_context: NixContext, known_paths: &KnownPaths) {
23    for element in full_context.iter() {
24        match element {
25            NixContextElement::Plain(source) => {
26                let sp = StorePathRef::from_absolute_path(source.as_bytes())
27                    .expect("invalid store path")
28                    .to_owned();
29                drv.input_sources.insert(sp);
30            }
31
32            NixContextElement::Single {
33                name,
34                derivation: derivation_str,
35            } => {
36                // TODO: b/264
37                // We assume derivations to be passed validated, so ignoring rest
38                // and expecting parsing is ok.
39                let (derivation, _rest) =
40                    StorePath::from_absolute_path_full(derivation_str).expect("valid store path");
41
42                #[cfg(debug_assertions)]
43                assert!(
44                    _rest.iter().next().is_none(),
45                    "Extra path not empty for {derivation_str}"
46                );
47
48                let name: OutputName = name
49                    .parse()
50                    .expect("Snix bug: output name in context invalid");
51
52                match drv.input_derivations.entry(derivation.clone()) {
53                    btree_map::Entry::Vacant(entry) => {
54                        entry.insert(BTreeSet::from([name]));
55                    }
56
57                    btree_map::Entry::Occupied(mut entry) => {
58                        entry.get_mut().insert(name);
59                    }
60                }
61            }
62
63            NixContextElement::Derivation(drv_path) => {
64                let (derivation, _rest) =
65                    StorePath::from_absolute_path_full(drv_path).expect("valid store path");
66
67                #[cfg(debug_assertions)]
68                assert!(
69                    _rest.iter().next().is_none(),
70                    "Extra path not empty for {drv_path}"
71                );
72
73                // We need to know all the outputs *names* of that derivation.
74                let output_names = known_paths
75                    .get_drv_by_drvpath(&derivation.as_ref())
76                    .expect("no known derivation associated to that derivation path")
77                    .outputs
78                    .keys();
79
80                // FUTUREWORK(performance): ideally, we should be able to clone
81                // cheaply those outputs rather than duplicate them all around.
82                match drv.input_derivations.entry(derivation.clone()) {
83                    btree_map::Entry::Vacant(entry) => {
84                        entry.insert(output_names.cloned().collect());
85                    }
86
87                    btree_map::Entry::Occupied(mut entry) => {
88                        entry.get_mut().extend(output_names.cloned());
89                    }
90                }
91
92                drv.input_sources.insert(derivation);
93            }
94        }
95    }
96}
97
98#[builtins(state = "Rc<SnixStoreIO>")]
99pub(crate) mod derivation_builtins {
100    use std::collections::BTreeMap;
101    use std::sync::Arc;
102
103    use bstr::ByteSlice;
104
105    use nix_compat::derivation::{Output, Outputs};
106    use nix_compat::nixhash::{HashAlgo, NixHash};
107    use nix_compat::store_path::hash_placeholder;
108    use snix_build_glue::builder;
109    use snix_eval::generators::Gen;
110    use snix_eval::{NixContext, NixContextElement, NixString, try_cek_to_value};
111
112    use crate::builtins::utils::{select_string, strong_importing_coerce_to_string};
113    use crate::fetchurl::fetchurl_derivation_to_fetch;
114
115    use super::*;
116
117    #[builtin("placeholder")]
118    async fn builtin_placeholder(co: GenCo, input: Value) -> Result<Value, ErrorKind> {
119        if input.is_catchable() {
120            return Ok(input);
121        }
122
123        let nix_string = input
124            .to_str()
125            .context("looking at output name in builtins.placeholder")?;
126        let output_name = nix_string.to_str()?;
127
128        let placeholder = hash_placeholder(output_name);
129
130        Ok(placeholder.into())
131    }
132
133    /// Strictly construct a Nix derivation from the supplied arguments.
134    ///
135    /// This is considered an internal function, users usually want to
136    /// use the higher-level `builtins.derivation` instead.
137    #[builtin("derivationStrict")]
138    async fn builtin_derivation_strict(
139        state: Rc<SnixStoreIO>,
140        co: GenCo,
141        input: Value,
142    ) -> Result<Value, ErrorKind> {
143        if input.is_catchable() {
144            return Ok(input);
145        }
146
147        let input = input.to_attrs()?;
148        let name = generators::request_force(&co, input.select_required("name")?.clone()).await;
149
150        if name.is_catchable() {
151            return Ok(name);
152        }
153
154        let name = name.to_str().context("determining derivation name")?;
155        if name.is_empty() {
156            return Err(ErrorKind::Abort("derivation has empty name".to_string()));
157        }
158        let name = name.to_str()?;
159
160        let mut drv = Derivation::default();
161
162        let mut input_context = NixContext::new();
163
164        /// Inserts a key and value into the drv.environment BTreeMap, and fails if the
165        /// key did already exist before.
166        fn insert_env(
167            drv: &mut Derivation,
168            k: &str, /* TODO: non-utf8 env keys */
169            v: BString,
170        ) -> Result<(), DerivationError> {
171            if drv.environment.insert(k.into(), v).is_some() {
172                return Err(DerivationError::DuplicateEnvVar(k.into()));
173            }
174            Ok(())
175        }
176
177        // Check whether null attributes should be ignored or passed through.
178        let ignore_nulls = match input.select(IGNORE_NULLS) {
179            Some(b) => generators::request_force(&co, b.clone()).await.as_bool()?,
180            None => false,
181        };
182
183        // Peek at the STRUCTURED_ATTRS argument.
184        // If it's set and true, provide a BTreeMap that gets populated while looking at the arguments.
185        // We need it to be a BTreeMap, so iteration order of keys is reproducible.
186        let mut structured_attrs: Option<BTreeMap<&str, serde_json::Value>> =
187            match input.select(STRUCTURED_ATTRS_ENABLE_KEY) {
188                Some(b) => generators::request_force(&co, b.clone())
189                    .await
190                    .as_bool()?
191                    .then_some(Default::default()),
192                None => None,
193            };
194
195        // Look at the arguments passed to builtins.derivationStrict.
196        // Some set special fields in the Derivation struct, some change
197        // behaviour of other functionality.
198        for (arg_name, arg_value) in input.iter_sorted() {
199            let arg_name = arg_name.to_str()?;
200            // force the current value.
201            let value = generators::request_force(&co, arg_value.clone()).await;
202
203            // filter out nulls if ignore_nulls is set.
204            if ignore_nulls && matches!(value, Value::Null) {
205                continue;
206            }
207
208            match arg_name {
209                // Command line arguments to the builder.
210                // These are only set in drv.arguments.
211                "args" => {
212                    for arg in value.to_list()? {
213                        let s =
214                            try_cek_to_value!(strong_importing_coerce_to_string(&co, arg).await);
215                        input_context.mimic(&s);
216                        drv.arguments.push(s.to_str()?.to_owned())
217                    }
218                }
219
220                // If outputs is set, populate drv.outputs with them.
221                "outputs" => {
222                    let outputs = value
223                        .to_list()
224                        .context("looking at the `outputs` parameter of the derivation")?;
225
226                    let mut output_names = Vec::with_capacity(outputs.len());
227
228                    for output in outputs {
229                        let output_name = generators::request_force(&co, output)
230                            .await
231                            .to_str()
232                            .context("determining output name")?;
233
234                        input_context.mimic(&output_name);
235
236                        let output_name: OutputName = output_name
237                            .to_str()?
238                            .parse()
239                            .map_err(|err| ErrorKind::SnixError(Arc::new(err)))?;
240
241                        output_names.push(output_name);
242                    }
243                    drv.outputs = Outputs::try_from_iter(
244                        output_names
245                            .iter()
246                            .cloned()
247                            .map(|name| (name, Output::default())),
248                    )
249                    .map_err(nix_compat::derivation::DerivationError::from)
250                    .map_err(DerivationError::from)?;
251
252                    match structured_attrs.as_mut() {
253                        // add outputs to the json itself (as a list of strings)
254                        Some(structured_attrs) => {
255                            let names = output_names.iter().map(ToString::to_string).collect();
256                            structured_attrs.insert(arg_name, names);
257                        }
258                        // add drv.environment["outputs"] as a space-separated list
259                        None => {
260                            insert_env(&mut drv, arg_name, output_names.join(" ").into())?;
261                        }
262                    }
263                    // drv.environment[$output_name] is added after the loop,
264                    // with whatever is in drv.outputs[$output_name].
265                }
266
267                // handle builder and system.
268                "builder" | "system" => {
269                    let val_str =
270                        try_cek_to_value!(strong_importing_coerce_to_string(&co, value).await);
271                    input_context.mimic(&val_str);
272
273                    if arg_name == "builder" {
274                        val_str.to_str()?.clone_into(&mut drv.builder);
275                    } else {
276                        val_str.to_str()?.clone_into(&mut drv.system);
277                    }
278
279                    // Either populate drv.environment or structured_attrs.
280                    if let Some(ref mut structured_attrs) = structured_attrs {
281                        // No need to check for dups, we only iterate over every attribute name once
282                        structured_attrs.insert(arg_name, val_str.to_str()?.to_owned().into());
283                    } else {
284                        insert_env(&mut drv, arg_name, val_str.as_bytes().into())?;
285                    }
286                }
287
288                // Don't add `STRUCTURED_ATTRS_ENABLE_KEY`.
289                STRUCTURED_ATTRS_ENABLE_KEY if structured_attrs.is_some() => continue,
290
291                // IGNORE_NULLS is always skipped, even if it's not set to true.
292                IGNORE_NULLS => continue,
293
294                // all other args.
295                _ => {
296                    match structured_attrs {
297                        // In SA case, force and add to structured attrs.
298                        Some(ref mut structured_attrs) => {
299                            let val = generators::request_force(&co, value).await;
300                            if val.is_catchable() {
301                                return Ok(val);
302                            }
303
304                            let (val_json, context) = val.into_contextful_json(&co).await?;
305                            input_context.extend(context);
306
307                            // No need to check for dups, we only iterate over every attribute name once
308                            structured_attrs.insert(arg_name, val_json);
309                        }
310                        // In non-SA case, coerce to string and add to env.
311                        None => {
312                            if arg_name == builder::structured_attrs::JSON_KEY {
313                                return Err(DerivationError::StructuredAttrsJsonKeyPresent.into());
314                            }
315                            let val_str = try_cek_to_value!(
316                                strong_importing_coerce_to_string(&co, value).await
317                            );
318                            input_context.mimic(&val_str);
319
320                            insert_env(&mut drv, arg_name, val_str.as_bytes().into())?;
321                        }
322                    }
323                }
324            }
325        }
326        // end of per-argument loop
327
328        // Set the out output, dealing with FOD fields if required.
329        {
330            // Unset or set but empty string are treated the same.
331            let hash_str = try_cek_to_value!(
332                select_string(&co, &input, "outputHash")
333                    .await
334                    .context("evaluating the `outputHash` parameter")?
335            )
336            .filter(|s| !s.is_empty());
337
338            let hash_algo = try_cek_to_value!(
339                select_string(&co, &input, "outputHashAlgo")
340                    .await
341                    .context("evaluating the `outputHashAlgo` parameter")?
342            )
343            .filter(|s| !s.is_empty());
344
345            let hash_mode = try_cek_to_value!(
346                select_string(&co, &input, "outputHashMode")
347                    .await
348                    .context("evaluating the `outputHashMode` parameter")?
349            )
350            .filter(|s| !s.is_empty());
351
352            // FOD case.
353            if let Some(hash_str) = hash_str {
354                if !drv.outputs.is_single() {
355                    return Err(ErrorKind::SnixError(Arc::new(
356                        DerivationError::ConflictingOutputTypes,
357                    )));
358                }
359
360                // parse outputHashMode.
361                let mode = hash_mode
362                    .map(|s| s.parse().map_err(|err| ErrorKind::SnixError(Arc::new(err))))
363                    .transpose()?
364                    .unwrap_or_default();
365
366                // parse outputHashAlgo
367                let want_algo: Option<HashAlgo> = hash_algo
368                    .map(|s| s.parse())
369                    .transpose()
370                    .map_err(|err| ErrorKind::SnixError(Arc::new(err)))?;
371
372                // construct a NixHash
373                let hash = NixHash::from_str(&hash_str, want_algo)
374                    .map_err(|err| ErrorKind::SnixError(Arc::new(err)))?;
375
376                // Emit a warning if the hash was SRI, but with wrong padding.
377                if let Some(rest) = hash_str.strip_prefix(hash.algo().sri_prefix())
378                    && data_encoding::BASE64.encode_len(hash.algo().digest_length()) != rest.len()
379                {
380                    emit_warning_kind(&co, WarningKind::SRIHashWrongPadding).await;
381                }
382                drv.outputs = Outputs::from_fod_hash(OutputHash { mode, hash });
383            }
384        }
385
386        // Each output name needs to exist in the environment, at this
387        // point initialised as an empty string, as the ATerm serialization of that is later
388        // used for the output path calculation (which will also update output
389        // paths post-calculation, both in drv.environment and drv.outputs)
390        for output in drv.outputs.keys() {
391            if drv
392                .environment
393                .insert(output.to_string(), String::new().into())
394                .is_some()
395            {
396                emit_warning_kind(&co, WarningKind::ShadowedOutput(output.to_string())).await;
397            }
398        }
399
400        if let Some(structured_attrs) = structured_attrs {
401            // configure __json
402            drv.environment.insert(
403                builder::structured_attrs::JSON_KEY.to_string(),
404                BString::from(serde_json::to_string(&structured_attrs)?),
405            );
406        }
407
408        let mut known_paths = state.as_ref().build_state.known_paths.borrow_mut();
409        populate_inputs(&mut drv, input_context, &known_paths);
410
411        // At this point, derivation fields are fully populated from
412        // eval data structures.
413        drv.validate().map_err(DerivationError::InvalidDerivation)?;
414
415        // Calculate the hash_derivation_modulo for the current derivation..
416        debug_assert!(
417            drv.outputs.values().all(|output| { output.path.is_none() }),
418            "outputs should still be unset"
419        );
420
421        // Mutate the Derivation struct and set output paths
422        drv.calculate_output_paths(
423            name,
424            // This one is still intermediate (so not added to known_paths),
425            // as the outputs are still unset.
426            &drv.hash_derivation_modulo(|drv_path| {
427                *known_paths
428                    .get_hash_derivation_modulo(drv_path)
429                    .unwrap_or_else(|| panic!("{drv_path} not found"))
430            }),
431        )
432        .map_err(DerivationError::InvalidDerivation)?;
433
434        let drv_path = drv
435            .calculate_derivation_path(name)
436            .map_err(DerivationError::InvalidDerivation)?;
437
438        // Assemble the attrset to return from this builtin.
439        let out = Value::Attrs(NixAttrs::from_iter(
440            drv.outputs
441                .iter()
442                .map(|(name, output)| {
443                    (
444                        name.into(),
445                        NixString::new_context_from(
446                            NixContextElement::Single {
447                                name: name.into(),
448                                derivation: drv_path.to_absolute_path(),
449                            }
450                            .into(),
451                            output.path.as_ref().unwrap().to_absolute_path(),
452                        ),
453                    )
454                })
455                .chain(std::iter::once((
456                    "drvPath".to_owned(),
457                    NixString::new_context_from(
458                        NixContextElement::Derivation(drv_path.to_absolute_path()).into(),
459                        drv_path.to_absolute_path(),
460                    ),
461                ))),
462        ));
463
464        // If the derivation is a fake derivation (builtin:fetchurl),
465        // synthesize a [Fetch] and add it there, too.
466        if drv.builder == "builtin:fetchurl" {
467            let (name, fetch) = fetchurl_derivation_to_fetch(&drv)
468                .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?;
469
470            known_paths
471                .add_fetch(fetch, &name)
472                .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?;
473        }
474
475        // Register the Derivation in known_paths.
476        known_paths.add_derivation(drv_path, drv);
477
478        Ok(out)
479    }
480}