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 STRUCTURED_ATTRS: &str = "__structuredAttrs";
19const IGNORE_NULLS: &str = "__ignoreNulls";
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            if data_encoding::BASE64.encode_len(algo.digest_length()) != rest.len() {
164                return Ok(Some(WarningKind::SRIHashWrongPadding));
165            }
166        }
167    }
168    Ok(None)
169}
170
171#[builtins(state = "Rc<SnixStoreIO>")]
172pub(crate) mod derivation_builtins {
173    use std::collections::BTreeMap;
174
175    use bstr::ByteSlice;
176
177    use nix_compat::derivation::validate_output_name;
178    use nix_compat::store_path::hash_placeholder;
179    use snix_eval::generators::Gen;
180    use snix_eval::{NixContext, NixContextElement, NixString, try_cek_to_value};
181
182    use crate::builtins::utils::{select_string, strong_importing_coerce_to_string};
183    use crate::fetchurl::fetchurl_derivation_to_fetch;
184
185    use super::*;
186
187    #[builtin("placeholder")]
188    async fn builtin_placeholder(co: GenCo, input: Value) -> Result<Value, ErrorKind> {
189        if input.is_catchable() {
190            return Ok(input);
191        }
192
193        let nix_string = input
194            .to_str()
195            .context("looking at output name in builtins.placeholder")?;
196        let output_name = nix_string.to_str()?;
197
198        // Validate the output name using the same rules as derivations
199        validate_output_name(output_name).map_err(|e| {
200            ErrorKind::Abort(format!("invalid output name in builtins.placeholder: {e}"))
201        })?;
202
203        let placeholder = hash_placeholder(output_name);
204
205        Ok(placeholder.into())
206    }
207
208    /// Strictly construct a Nix derivation from the supplied arguments.
209    ///
210    /// This is considered an internal function, users usually want to
211    /// use the higher-level `builtins.derivation` instead.
212    #[builtin("derivationStrict")]
213    async fn builtin_derivation_strict(
214        state: Rc<SnixStoreIO>,
215        co: GenCo,
216        input: Value,
217    ) -> Result<Value, ErrorKind> {
218        if input.is_catchable() {
219            return Ok(input);
220        }
221
222        let input = input.to_attrs()?;
223        let name = generators::request_force(&co, input.select_required("name")?.clone()).await;
224
225        if name.is_catchable() {
226            return Ok(name);
227        }
228
229        let name = name.to_str().context("determining derivation name")?;
230        if name.is_empty() {
231            return Err(ErrorKind::Abort("derivation has empty name".to_string()));
232        }
233        let name = name.to_str()?;
234
235        let mut drv = Derivation::default();
236        drv.outputs.insert("out".to_string(), Default::default());
237        let mut input_context = NixContext::new();
238
239        /// Inserts a key and value into the drv.environment BTreeMap, and fails if the
240        /// key did already exist before.
241        fn insert_env(
242            drv: &mut Derivation,
243            k: &str, /* TODO: non-utf8 env keys */
244            v: BString,
245        ) -> Result<(), DerivationError> {
246            if drv.environment.insert(k.into(), v).is_some() {
247                return Err(DerivationError::DuplicateEnvVar(k.into()));
248            }
249            Ok(())
250        }
251
252        // Check whether null attributes should be ignored or passed through.
253        let ignore_nulls = match input.select(IGNORE_NULLS) {
254            Some(b) => generators::request_force(&co, b.clone()).await.as_bool()?,
255            None => false,
256        };
257
258        // peek at the STRUCTURED_ATTRS argument.
259        // If it's set and true, provide a BTreeMap that gets populated while looking at the arguments.
260        // We need it to be a BTreeMap, so iteration order of keys is reproducible.
261        let mut structured_attrs: Option<BTreeMap<String, serde_json::Value>> =
262            match input.select(STRUCTURED_ATTRS) {
263                Some(b) => generators::request_force(&co, b.clone())
264                    .await
265                    .as_bool()?
266                    .then_some(Default::default()),
267                None => None,
268            };
269
270        // Look at the arguments passed to builtins.derivationStrict.
271        // Some set special fields in the Derivation struct, some change
272        // behaviour of other functionality.
273        for (arg_name, arg_value) in input.clone().into_iter_sorted() {
274            let arg_name = arg_name.to_str()?;
275            // force the current value.
276            let value = generators::request_force(&co, arg_value).await;
277
278            // filter out nulls if ignore_nulls is set.
279            if ignore_nulls && matches!(value, Value::Null) {
280                continue;
281            }
282
283            match arg_name {
284                // Command line arguments to the builder.
285                // These are only set in drv.arguments.
286                "args" => {
287                    for arg in value.to_list()? {
288                        let s =
289                            try_cek_to_value!(strong_importing_coerce_to_string(&co, arg).await);
290                        input_context.mimic(&s);
291                        drv.arguments.push(s.to_str()?.to_owned())
292                    }
293                }
294
295                // If outputs is set, remove the original default `out` output,
296                // and replace it with the list of outputs.
297                "outputs" => {
298                    let outputs = value
299                        .to_list()
300                        .context("looking at the `outputs` parameter of the derivation")?;
301
302                    // Remove the original default `out` output.
303                    drv.outputs.clear();
304
305                    let mut output_names = Vec::with_capacity(outputs.len());
306
307                    for output in outputs {
308                        let output_name = generators::request_force(&co, output)
309                            .await
310                            .to_str()
311                            .context("determining output name")?;
312
313                        input_context.mimic(&output_name);
314
315                        // Populate drv.outputs
316                        if drv
317                            .outputs
318                            .insert(output_name.to_str()?.to_owned(), Default::default())
319                            .is_some()
320                        {
321                            Err(DerivationError::DuplicateOutput(
322                                output_name.to_str_lossy().into_owned(),
323                            ))?
324                        }
325                        output_names.push(output_name.to_str()?.to_owned());
326                    }
327
328                    match structured_attrs.as_mut() {
329                        // add outputs to the json itself (as a list of strings)
330                        Some(structured_attrs) => {
331                            structured_attrs.insert(arg_name.into(), output_names.into());
332                        }
333                        // add drv.environment["outputs"] as a space-separated list
334                        None => {
335                            insert_env(&mut drv, arg_name, output_names.join(" ").into())?;
336                        }
337                    }
338                    // drv.environment[$output_name] is added after the loop,
339                    // with whatever is in drv.outputs[$output_name].
340                }
341
342                // handle builder and system.
343                "builder" | "system" => {
344                    let val_str =
345                        try_cek_to_value!(strong_importing_coerce_to_string(&co, value).await);
346                    input_context.mimic(&val_str);
347
348                    if arg_name == "builder" {
349                        val_str.to_str()?.clone_into(&mut drv.builder);
350                    } else {
351                        val_str.to_str()?.clone_into(&mut drv.system);
352                    }
353
354                    // Either populate drv.environment or structured_attrs.
355                    if let Some(ref mut structured_attrs) = structured_attrs {
356                        // No need to check for dups, we only iterate over every attribute name once
357                        structured_attrs
358                            .insert(arg_name.to_owned(), val_str.to_str()?.to_owned().into());
359                    } else {
360                        insert_env(&mut drv, arg_name, val_str.as_bytes().into())?;
361                    }
362                }
363
364                // Don't add STRUCTURED_ATTRS if enabled.
365                STRUCTURED_ATTRS if structured_attrs.is_some() => continue,
366                // IGNORE_NULLS is always skipped, even if it's not set to true.
367                IGNORE_NULLS => continue,
368
369                // all other args.
370                _ => {
371                    // In SA case, force and add to structured attrs.
372                    // In non-SA case, coerce to string and add to env.
373                    if let Some(ref mut structured_attrs) = structured_attrs {
374                        let val = generators::request_force(&co, value).await;
375                        if val.is_catchable() {
376                            return Ok(val);
377                        }
378
379                        let (val_json, context) = val.into_contextful_json(&co).await?;
380                        input_context.extend(context.into_iter());
381
382                        // No need to check for dups, we only iterate over every attribute name once
383                        structured_attrs.insert(arg_name.to_owned(), val_json);
384                    } else {
385                        let val_str =
386                            try_cek_to_value!(strong_importing_coerce_to_string(&co, value).await);
387                        input_context.mimic(&val_str);
388
389                        insert_env(&mut drv, arg_name, val_str.as_bytes().into())?;
390                    }
391                }
392            }
393        }
394        // end of per-argument loop
395
396        // Configure fixed-output derivations if required.
397        {
398            let output_hash = try_cek_to_value!(
399                select_string(&co, &input, "outputHash")
400                    .await
401                    .context("evaluating the `outputHash` parameter")?
402            );
403            let output_hash_algo = try_cek_to_value!(
404                select_string(&co, &input, "outputHashAlgo")
405                    .await
406                    .context("evaluating the `outputHashAlgo` parameter")?
407            );
408            let output_hash_mode = try_cek_to_value!(
409                select_string(&co, &input, "outputHashMode")
410                    .await
411                    .context("evaluating the `outputHashMode` parameter")?
412            );
413
414            if let Some(warning) =
415                handle_fixed_output(&mut drv, output_hash, output_hash_algo, output_hash_mode)?
416            {
417                emit_warning_kind(&co, warning).await;
418            }
419        }
420
421        // Each output name needs to exist in the environment, at this
422        // point initialised as an empty string, as the ATerm serialization of that is later
423        // used for the output path calculation (which will also update output
424        // paths post-calculation, both in drv.environment and drv.outputs)
425        for output in drv.outputs.keys() {
426            if drv
427                .environment
428                .insert(output.to_string(), String::new().into())
429                .is_some()
430            {
431                emit_warning_kind(&co, WarningKind::ShadowedOutput(output.to_string())).await;
432            }
433        }
434
435        if let Some(structured_attrs) = structured_attrs {
436            // configure __json
437            drv.environment.insert(
438                "__json".to_string(),
439                BString::from(serde_json::to_string(&structured_attrs)?),
440            );
441        }
442
443        let mut known_paths = state.as_ref().known_paths.borrow_mut();
444        populate_inputs(&mut drv, input_context, &known_paths);
445
446        // At this point, derivation fields are fully populated from
447        // eval data structures.
448        drv.validate(false)
449            .map_err(DerivationError::InvalidDerivation)?;
450
451        // Calculate the hash_derivation_modulo for the current derivation..
452        debug_assert!(
453            drv.outputs.values().all(|output| { output.path.is_none() }),
454            "outputs should still be unset"
455        );
456
457        // Mutate the Derivation struct and set output paths
458        drv.calculate_output_paths(
459            name,
460            // This one is still intermediate (so not added to known_paths),
461            // as the outputs are still unset.
462            &drv.hash_derivation_modulo(|drv_path| {
463                *known_paths
464                    .get_hash_derivation_modulo(&drv_path.to_owned())
465                    .unwrap_or_else(|| panic!("{drv_path} not found"))
466            }),
467        )
468        .map_err(DerivationError::InvalidDerivation)?;
469
470        let drv_path = drv
471            .calculate_derivation_path(name)
472            .map_err(DerivationError::InvalidDerivation)?;
473
474        // Assemble the attrset to return from this builtin.
475        let out = Value::Attrs(Box::new(NixAttrs::from_iter(
476            drv.outputs
477                .iter()
478                .map(|(name, output)| {
479                    (
480                        name.clone(),
481                        NixString::new_context_from(
482                            NixContextElement::Single {
483                                name: name.clone(),
484                                derivation: drv_path.to_absolute_path(),
485                            }
486                            .into(),
487                            output.path.as_ref().unwrap().to_absolute_path(),
488                        ),
489                    )
490                })
491                .chain(std::iter::once((
492                    "drvPath".to_owned(),
493                    NixString::new_context_from(
494                        NixContextElement::Derivation(drv_path.to_absolute_path()).into(),
495                        drv_path.to_absolute_path(),
496                    ),
497                ))),
498        )));
499
500        // If the derivation is a fake derivation (builtin:fetchurl),
501        // synthesize a [Fetch] and add it there, too.
502        if drv.builder == "builtin:fetchurl" {
503            let (name, fetch) =
504                fetchurl_derivation_to_fetch(&drv).map_err(|e| ErrorKind::SnixError(Rc::new(e)))?;
505
506            known_paths
507                .add_fetch(fetch, &name)
508                .map_err(|e| ErrorKind::SnixError(Rc::new(e)))?;
509        }
510
511        // Register the Derivation in known_paths.
512        known_paths.add_derivation(drv_path, drv);
513
514        Ok(out)
515    }
516}