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