snix_glue/builtins/
derivation.rs

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