Skip to main content

snix_glue/builtins/
import.rs

1//! Implements builtins used to import paths in the store.
2
3use crate::snix_store_io::SnixStoreIO;
4use snix_castore::Node;
5use snix_castore::import::ingest_entries;
6use snix_eval::{
7    ErrorKind, EvalIO, Value,
8    builtin_macros::builtins,
9    generators::{self, GenCo},
10};
11use std::path::Path;
12
13use std::rc::Rc;
14
15async fn filtered_ingest(
16    state: Rc<SnixStoreIO>,
17    co: GenCo,
18    path: &Path,
19    filter: Option<&Value>,
20) -> Result<Node, ErrorKind> {
21    let mut entries: Vec<walkdir::DirEntry> = vec![];
22    let mut it = walkdir::WalkDir::new(path)
23        .follow_links(false)
24        .follow_root_links(false)
25        .contents_first(false)
26        .into_iter();
27
28    // Skip root node.
29    entries.push(
30        it.next()
31            .ok_or_else(|| ErrorKind::IO {
32                path: Some(path.to_path_buf()),
33                error: std::io::Error::new(std::io::ErrorKind::NotFound, "No root node emitted")
34                    .into(),
35            })?
36            .map_err(|err| ErrorKind::IO {
37                path: Some(path.to_path_buf()),
38                error: std::io::Error::from(err).into(),
39            })?,
40    );
41
42    while let Some(entry) = it.next() {
43        // Entry could be a NotFound, if the root path specified does not exist.
44        let entry = entry.map_err(|err| ErrorKind::IO {
45            path: err.path().map(|p| p.to_path_buf()),
46            error: std::io::Error::from(err).into(),
47        })?;
48
49        // As per Nix documentation `:doc builtins.filterSource`.
50        let file_type = if entry.file_type().is_dir() {
51            "directory"
52        } else if entry.file_type().is_file() {
53            "regular"
54        } else if entry.file_type().is_symlink() {
55            "symlink"
56        } else {
57            "unknown"
58        };
59
60        let should_keep: bool = if let Some(filter) = filter {
61            generators::request_force(
62                &co,
63                generators::request_call_with(
64                    &co,
65                    filter.clone(),
66                    [
67                        Value::String(entry.path().as_os_str().as_encoded_bytes().into()),
68                        Value::String(file_type.into()),
69                    ],
70                )
71                .await,
72            )
73            .await
74            .as_bool()?
75        } else {
76            true
77        };
78
79        if !should_keep {
80            if file_type == "directory" {
81                it.skip_current_dir();
82            }
83            continue;
84        }
85
86        entries.push(entry);
87    }
88
89    let dir_entries = entries.into_iter().rev().map(Ok);
90
91    state.tokio_handle.block_on(async {
92        let entries = snix_castore::import::fs::dir_entries_to_ingestion_stream::<'_, _, _, &[u8]>(
93            &state.build_state.blob_service,
94            dir_entries,
95            path,
96            None, // TODO re-scan
97        );
98        ingest_entries(&state.build_state.directory_service, entries)
99            .await
100            .map_err(|e| ErrorKind::IO {
101                path: Some(path.to_path_buf()),
102                error: Rc::new(std::io::Error::other(e)),
103            })
104    })
105}
106
107#[builtins(state = "Rc<SnixStoreIO>")]
108mod import_builtins {
109    use super::*;
110
111    use crate::builtins::ImportError;
112    use crate::snix_store_io::SnixStoreIO;
113    use bstr::ByteSlice;
114    use nix_compat::nixhash::{CAHash, HashAlgo, NixHash};
115    use nix_compat::store_path::{
116        StorePath, StorePathRef, build_ca_path, build_text_path_from_content_digest,
117    };
118    use sha2::Digest;
119    use snix_castore::blobservice::BlobService;
120    use snix_eval::builtins::coerce_value_to_path;
121    use snix_eval::generators::Gen;
122    use snix_eval::{AddContext, FileType, NixContext, NixContextElement, NixString};
123    use snix_eval::{ErrorKind, Value, generators::GenCo};
124    use snix_store::path_info::PathInfo;
125    use std::rc::Rc;
126    use std::sync::Arc;
127    use tokio::io::AsyncWriteExt;
128
129    /// Helper function dealing with uploading something from a std::io::Read to
130    /// the passed [BlobService], returning the B3Digest and size.
131    /// This function is sync (and uses the tokio handle to block).
132    /// A sync closure getting a copy of all bytes read can be passed in,
133    /// allowing to do other hashing where needed.
134    fn copy_to_blobservice<F>(
135        tokio_handle: tokio::runtime::Handle,
136        blob_service: impl BlobService,
137        mut r: impl std::io::Read,
138        mut inspect_f: F,
139    ) -> std::io::Result<(snix_castore::B3Digest, u64)>
140    where
141        F: FnMut(&[u8]),
142    {
143        let mut blob_size = 0;
144
145        let mut blob_writer = tokio_handle.block_on(async { blob_service.open_write().await });
146
147        // read piece by piece and write to blob_writer.
148        // This is a bit manual due to EvalIO being sync, while the blob writer being async.
149        {
150            let mut buf = [0u8; 4096];
151
152            loop {
153                // read bytes into buffer, break out if EOF
154                let len = r.read(&mut buf)?;
155                if len == 0 {
156                    break;
157                }
158                blob_size += len as u64;
159
160                let data = &buf[0..len];
161
162                // write to blobwriter
163                tokio_handle.block_on(async { blob_writer.write_all(data).await })?;
164
165                // Call inspect_f
166                inspect_f(data);
167            }
168
169            let blob_digest = tokio_handle.block_on(async { blob_writer.close().await })?;
170
171            Ok((blob_digest, blob_size))
172        }
173    }
174
175    // This is a helper used by both builtins.path and builtins.filterSource.
176    async fn import_helper(
177        state: Rc<SnixStoreIO>,
178        co: GenCo,
179        path: std::path::PathBuf,
180        name: Option<&Value>,
181        filter: Option<&Value>,
182        recursive_ingestion: bool,
183        expected_sha256: Option<[u8; 32]>,
184    ) -> Result<Value, ErrorKind> {
185        // Determine the name, either chosen by the user or derived from the path.
186        let name: String = match name {
187            Some(name) => {
188                let nix_str = generators::request_force(&co, name.clone())
189                    .await
190                    .to_str()?;
191
192                nix_compat::store_path::validate_name(&nix_str)
193                    .map_err(|err| {
194                        ErrorKind::SnixError(Arc::new(
195                            nix_compat::store_path::ParseStorePathError::from(err),
196                        ))
197                    })?
198                    .to_owned()
199            }
200            None => {
201                let file_name = path.file_name().ok_or_else(|| {
202                    std::io::Error::new(
203                        std::io::ErrorKind::InvalidFilename,
204                        "path without basename encountered",
205                    )
206                })?;
207                nix_compat::store_path::validate_name_from_os_str(file_name)
208                    .map_err(|err| ErrorKind::SnixError(Arc::new(err)))?
209                    .to_owned()
210            }
211        };
212        // As a first step, we ingest the contents, and get back a root node,
213        // and optionally the sha256 a flat file.
214        let (root_node, ca) = match std::fs::metadata(&path)?.file_type().into() {
215            // Check if the path points to a regular file.
216            // If it does, the filter function is never executed, and we copy to the blobservice directly.
217            // If recursive is false, we need to calculate the sha256 digest of the raw contents,
218            // as that affects the output path calculation.
219            FileType::Regular => {
220                let mut file = state.open(&path)?;
221                let mut h = (!recursive_ingestion).then(sha2::Sha256::new);
222
223                let (blob_digest, blob_size) = copy_to_blobservice(
224                    state.tokio_handle.clone(),
225                    &state.build_state.blob_service,
226                    &mut file,
227                    |data| {
228                        // update blob_sha256 if needed.
229                        if let Some(h) = h.as_mut() {
230                            h.update(data)
231                        }
232                    },
233                )?;
234
235                (
236                    Node::File {
237                        digest: blob_digest,
238                        size: blob_size,
239                        executable: false,
240                    },
241                    h.map(|h| {
242                        // If non-recursive ingestion was requested, we return that one.
243                        let actual_sha256 = h.finalize().into();
244
245                        // If an expected hash was provided upfront, compare and bail out.
246                        if let Some(expected_sha256) = expected_sha256
247                            && actual_sha256 != expected_sha256
248                        {
249                            return Err(ImportError::HashMismatch(
250                                path.clone(),
251                                NixHash::Sha256(expected_sha256),
252                                NixHash::Sha256(actual_sha256),
253                            ));
254                        }
255                        Ok(CAHash::Flat(NixHash::Sha256(actual_sha256)))
256                    })
257                    .transpose()?,
258                )
259            }
260
261            FileType::Directory if !recursive_ingestion => {
262                return Err(ImportError::FlatImportOfNonFile(path))?;
263            }
264
265            // do the filtered ingest
266            FileType::Directory => (
267                filtered_ingest(state.clone(), co, path.as_ref(), filter).await?,
268                None,
269            ),
270            FileType::Symlink => {
271                // FUTUREWORK: Nix follows a symlink if it's at the root,
272                // except if it's not resolve-able (NixOS/nix#7761).
273                return Err(snix_eval::ErrorKind::IO {
274                    path: Some(path),
275                    error: Rc::new(std::io::Error::new(
276                        std::io::ErrorKind::Unsupported,
277                        "builtins.path pointing to a symlink is ill-defined.",
278                    )),
279                });
280            }
281            FileType::Unknown => {
282                return Err(snix_eval::ErrorKind::IO {
283                    path: Some(path),
284                    error: Rc::new(std::io::Error::new(
285                        std::io::ErrorKind::Unsupported,
286                        "unsupported file type",
287                    )),
288                });
289            }
290        };
291
292        // Calculate the NAR sha256.
293        let (nar_size, nar_sha256) = state
294            .tokio_handle
295            .block_on(async {
296                state
297                    .build_state
298                    .nar_calculation_service
299                    .as_ref()
300                    .calculate_nar(&root_node)
301                    .await
302            })
303            .map_err(|e| snix_eval::ErrorKind::SnixError(Arc::from(e)))?;
304
305        // Calculate the CA hash for the recursive cases, this is only already
306        // `Some(_)` for flat ingestion.
307        let ca = match ca {
308            None => {
309                // If an upfront-expected NAR hash was specified, compare.
310                if let Some(expected_nar_sha256) = expected_sha256
311                    && expected_nar_sha256 != nar_sha256
312                {
313                    return Err(ImportError::HashMismatch(
314                        path,
315                        NixHash::Sha256(expected_nar_sha256),
316                        NixHash::Sha256(nar_sha256),
317                    )
318                    .into());
319                }
320                CAHash::Nar(NixHash::Sha256(nar_sha256))
321            }
322            Some(ca) => ca,
323        };
324
325        let store_path = build_ca_path(&name, recursive_ingestion, &ca.hash(), [], false)
326            .map_err(|e| snix_eval::ErrorKind::SnixError(Arc::from(e)))?
327            .to_owned();
328
329        let path_info = state
330            .tokio_handle
331            .block_on(async {
332                state
333                    .build_state
334                    .path_info_service
335                    .as_ref()
336                    .put(PathInfo {
337                        store_path,
338                        node: root_node,
339                        // There's no reference scanning on path contents ingested like this.
340                        references: vec![],
341                        nar_size,
342                        nar_sha256,
343                        signatures: vec![],
344                        deriver: None,
345                        ca: Some(ca),
346                    })
347                    .await
348            })
349            .map_err(|e| snix_eval::ErrorKind::IO {
350                path: Some(path),
351                error: Rc::new(std::io::Error::other(e)),
352            })?;
353
354        // We need to attach context to the final output path.
355        let outpath = path_info.store_path.to_absolute_path();
356
357        Ok(
358            NixString::new_context_from(NixContextElement::Plain(outpath.clone()).into(), outpath)
359                .into(),
360        )
361    }
362
363    #[builtin("path")]
364    async fn builtin_path(
365        state: Rc<SnixStoreIO>,
366        co: GenCo,
367        args: Value,
368    ) -> Result<Value, ErrorKind> {
369        let args = args.to_attrs()?;
370
371        let path = match coerce_value_to_path(
372            &co,
373            generators::request_force(&co, args.select_required("path")?.clone()).await,
374        )
375        .await?
376        {
377            Ok(path) => path,
378            Err(cek) => return Ok(cek.into()),
379        };
380
381        let filter = args.select("filter");
382
383        // Construct a sha256 hasher, which is needed for flat ingestion.
384        let recursive_ingestion = args
385            .select("recursive")
386            .map(|r| r.as_bool())
387            .transpose()?
388            .unwrap_or(true); // Yes, yes, Nix, by default, puts `recursive = true;`.
389
390        let expected_sha256 = args
391            .select("sha256")
392            .map(|h| {
393                h.to_str().and_then(|expected| {
394                    match NixHash::from_str(expected.to_str()?, Some(HashAlgo::Sha256)) {
395                        Ok(NixHash::Sha256(digest)) => Ok(digest),
396                        Ok(_) => unreachable!(),
397                        Err(e) => Err(ErrorKind::InvalidHash(e.to_string())),
398                    }
399                })
400            })
401            .transpose()?;
402
403        import_helper(
404            state,
405            co,
406            path,
407            args.select("name"),
408            filter,
409            recursive_ingestion,
410            expected_sha256,
411        )
412        .await
413    }
414
415    #[builtin("filterSource")]
416    async fn builtin_filter_source(
417        state: Rc<SnixStoreIO>,
418        co: GenCo,
419        #[lazy] filter: Value,
420        path: Value,
421    ) -> Result<Value, ErrorKind> {
422        let path =
423            match coerce_value_to_path(&co, generators::request_force(&co, path).await).await? {
424                Ok(path) => path,
425                Err(cek) => return Ok(cek.into()),
426            };
427
428        import_helper(state, co, path, None, Some(&filter), true, None).await
429    }
430
431    #[builtin("storePath")]
432    async fn builtin_store_path(
433        state: Rc<SnixStoreIO>,
434        co: GenCo,
435        path: Value,
436    ) -> Result<Value, ErrorKind> {
437        let p = match &path {
438            Value::String(s) => Path::new(s.as_bytes().to_os_str()?),
439            Value::Path(p) => p.as_path(),
440            _ => {
441                return Err(ErrorKind::TypeError {
442                    expected: "string or path",
443                    actual: path.type_of(),
444                });
445            }
446        };
447
448        // For this builtin, the path needs to start with an absolute store path.
449        let (store_path, _sub_path) = StorePathRef::from_absolute_path_full(p)
450            .map_err(|_e| ImportError::PathNotAbsoluteOrInvalid(p.to_path_buf()))?;
451
452        if state.path_exists(p)? {
453            Ok(Value::String(NixString::new_context_from(
454                [NixContextElement::Plain(store_path.to_absolute_path())].into(),
455                p.as_os_str().as_encoded_bytes(),
456            )))
457        } else {
458            Err(ErrorKind::IO {
459                path: Some(p.to_path_buf()),
460                error: Rc::new(std::io::ErrorKind::NotFound.into()),
461            })
462        }
463    }
464
465    #[builtin("toFile")]
466    async fn builtin_to_file(
467        state: Rc<SnixStoreIO>,
468        co: GenCo,
469        name: Value,
470        content: Value,
471    ) -> Result<Value, ErrorKind> {
472        if name.is_catchable() {
473            return Ok(name);
474        }
475
476        if content.is_catchable() {
477            return Ok(content);
478        }
479
480        let name = name
481            .to_str()
482            .context("evaluating the `name` parameter of builtins.toFile")?;
483        let content = content
484            .to_contextful_str()
485            .context("evaluating the `content` parameter of builtins.toFile")?;
486
487        if content.iter_ctx_derivation().count() > 0
488            || content.iter_ctx_single_outputs().count() > 0
489        {
490            return Err(ErrorKind::UnexpectedContext);
491        }
492
493        // upload contents to the blobservice and create a root node
494        let mut h = sha2::Sha256::new();
495        let (blob_digest, blob_size) = copy_to_blobservice(
496            state.tokio_handle.clone(),
497            &state.build_state.blob_service,
498            std::io::Cursor::new(&content),
499            |data| h.update(data),
500        )?;
501
502        let root_node = Node::File {
503            digest: blob_digest,
504            size: blob_size,
505            executable: false,
506        };
507
508        // calculate the nar hash
509        let (nar_size, nar_sha256) = state
510            .tokio_handle
511            .block_on(
512                state
513                    .build_state
514                    .nar_calculation_service
515                    .calculate_nar(&root_node),
516            )
517            .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?;
518
519        let content_digest: [u8; 32] = h.finalize().into();
520        let references = content.iter_ctx_plain().map(|sp| {
521            StorePathRef::from_absolute_path(sp.as_bytes())
522                .expect("Snix bug: must parse as store path")
523        });
524
525        // persist via pathinfo service.
526        let store_path = state
527            .tokio_handle
528            .block_on(
529                state.build_state.path_info_service.put(PathInfo {
530                    store_path: build_text_path_from_content_digest(
531                        name.to_str()?,
532                        content_digest,
533                        references,
534                    )
535                    .map_err(|_e| {
536                        nix_compat::derivation::DerivationError::InvalidOutputs(
537                            nix_compat::derivation::outputs::OutputsError::InvalidOutputName(
538                                name.to_str_lossy().into_owned(),
539                            ),
540                        )
541                    })
542                    .map_err(crate::builtins::DerivationError::InvalidDerivation)?
543                    .to_owned(),
544                    node: root_node,
545                    // assemble references from plain context.
546                    references: content
547                        .iter_ctx_plain()
548                        .map(|elem| StorePath::from_absolute_path(elem.as_bytes()))
549                        .collect::<Result<_, _>>()
550                        .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?,
551                    nar_size,
552                    nar_sha256,
553                    signatures: vec![],
554                    deriver: None,
555                    ca: Some(CAHash::Text(content_digest)),
556                }),
557            )
558            .map_err(|e| ErrorKind::SnixError(Arc::from(e)))
559            .map(|path_info| path_info.store_path)?;
560
561        let abs_path = store_path.to_absolute_path();
562        let context: NixContext = NixContextElement::Plain(abs_path.clone()).into();
563
564        Ok(Value::from(NixString::new_context_from(context, abs_path)))
565    }
566}
567
568pub use import_builtins::builtins as import_builtins;