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