about summary refs log blame commit diff
path: root/tvix/glue/src/builtins/import.rs
blob: d2e2e6c5bd7da2828fe23bec8519f57eb4b43b26 (plain) (tree)
1
2
3
4
5
6
7
8
9

                                                          
                                      
                    
                                         
                       


                              
                             








                           
                              
                                                     



                                            


                      
                 




































                                                                                                

                                                                                          
















                                         
                            

     
                                                        
 
                                       
                                                                                                   


                                
                                 

                                                         
                  
                                        
                                               
                                                                                  

              



                                      

                 
                                     
                                          
                        
                                               
                                             
                     
                    
                                 
                                                  

                                                         
                                                            
 







                                                 






                                                                                               




































                                                                                                  













                                                                               
 










                                                               
                         






                                                                                               
                 





                                                                    




                                            





















                                                                                   

                                                                
                        
                 
 




                                                                                                 
                                 
































                                                                               

          
                                                        
                                                   


                                                       
                                                



                    
                             
                         




                                                                                                    

                                                   
                                  
                
 
                                                              
                                                              




                                                                                                  

     







                                   
                                                                                         

                                                         
                             

                             
                                           
                                            



                                              
                     
                                                   

                             
                                                                 

                                  

                          


                                            



                                                              




                                                                                                  
     






                                   


                                                                     





                                                 
          
 


                                                                                   
 









                                                                                 
         
     


                                                     
//! Implements builtins used to import paths in the store.

use crate::tvix_store_io::TvixStoreIO;
use std::path::Path;
use tvix_castore::import::ingest_entries;
use tvix_castore::Node;
use tvix_eval::{
    builtin_macros::builtins,
    generators::{self, GenCo},
    ErrorKind, EvalIO, Value,
};

use std::rc::Rc;

async fn filtered_ingest(
    state: Rc<TvixStoreIO>,
    co: GenCo,
    path: &Path,
    filter: Option<&Value>,
) -> Result<Node, ErrorKind> {
    let mut entries: Vec<walkdir::DirEntry> = vec![];
    let mut it = walkdir::WalkDir::new(path)
        .follow_links(false)
        .follow_root_links(false)
        .contents_first(false)
        .into_iter();

    // Skip root node.
    entries.push(
        it.next()
            .ok_or_else(|| ErrorKind::IO {
                path: Some(path.to_path_buf()),
                error: std::io::Error::new(std::io::ErrorKind::NotFound, "No root node emitted")
                    .into(),
            })?
            .map_err(|err| ErrorKind::IO {
                path: Some(path.to_path_buf()),
                error: std::io::Error::from(err).into(),
            })?,
    );

    while let Some(entry) = it.next() {
        // Entry could be a NotFound, if the root path specified does not exist.
        let entry = entry.map_err(|err| ErrorKind::IO {
            path: err.path().map(|p| p.to_path_buf()),
            error: std::io::Error::from(err).into(),
        })?;

        // As per Nix documentation `:doc builtins.filterSource`.
        let file_type = if entry.file_type().is_dir() {
            "directory"
        } else if entry.file_type().is_file() {
            "regular"
        } else if entry.file_type().is_symlink() {
            "symlink"
        } else {
            "unknown"
        };

        let should_keep: bool = if let Some(filter) = filter {
            generators::request_force(
                &co,
                generators::request_call_with(
                    &co,
                    filter.clone(),
                    [
                        Value::String(entry.path().as_os_str().as_encoded_bytes().into()),
                        Value::String(file_type.into()),
                    ],
                )
                .await,
            )
            .await
            .as_bool()?
        } else {
            true
        };

        if !should_keep {
            if file_type == "directory" {
                it.skip_current_dir();
            }
            continue;
        }

        entries.push(entry);
    }

    let dir_entries = entries.into_iter().rev().map(Ok);

    state.tokio_handle.block_on(async {
        let entries = tvix_castore::import::fs::dir_entries_to_ingestion_stream::<'_, _, _, &[u8]>(
            &state.blob_service,
            dir_entries,
            path,
            None, // TODO re-scan
        );
        ingest_entries(&state.directory_service, entries)
            .await
            .map_err(|e| ErrorKind::IO {
                path: Some(path.to_path_buf()),
                error: Rc::new(std::io::Error::new(std::io::ErrorKind::Other, e)),
            })
    })
}

#[builtins(state = "Rc<TvixStoreIO>")]
mod import_builtins {
    use super::*;

    use crate::builtins::ImportError;
    use crate::tvix_store_io::TvixStoreIO;
    use bstr::ByteSlice;
    use nix_compat::nixhash::{CAHash, NixHash};
    use nix_compat::store_path::StorePathRef;
    use sha2::Digest;
    use std::rc::Rc;
    use tokio::io::AsyncWriteExt;
    use tvix_eval::builtins::coerce_value_to_path;
    use tvix_eval::generators::Gen;
    use tvix_eval::{generators::GenCo, ErrorKind, Value};
    use tvix_eval::{FileType, NixContextElement, NixString};

    #[builtin("path")]
    async fn builtin_path(
        state: Rc<TvixStoreIO>,
        co: GenCo,
        args: Value,
    ) -> Result<Value, ErrorKind> {
        let args = args.to_attrs()?;
        let path = args.select_required("path")?;
        let path =
            match coerce_value_to_path(&co, generators::request_force(&co, path.clone()).await)
                .await?
            {
                Ok(path) => path,
                Err(cek) => return Ok(cek.into()),
            };
        let name: String = if let Some(name) = args.select("name") {
            generators::request_force(&co, name.clone())
                .await
                .to_str()?
                .as_bstr()
                .to_string()
        } else {
            tvix_store::import::path_to_name(&path)
                .expect("Failed to derive the default name out of the path")
                .to_string()
        };
        let filter = args.select("filter");
        let recursive_ingestion = args
            .select("recursive")
            .map(|r| r.as_bool())
            .transpose()?
            .unwrap_or(true); // Yes, yes, Nix, by default, puts `recursive = true;`.
        let expected_sha256 = args
            .select("sha256")
            .map(|h| {
                h.to_str().and_then(|expected| {
                    let expected = expected.into_bstring().to_string();
                    // TODO: ensure that we fail if this is not a valid str.
                    nix_compat::nixhash::from_str(&expected, None).map_err(|_err| {
                        // TODO: a better error would be nice, we use
                        // DerivationError::InvalidOutputHash usually for derivation construction.
                        // This is not a derivation construction, should we move it outside and
                        // generalize?
                        ErrorKind::TypeError {
                            expected: "sha256",
                            actual: "not a sha256",
                        }
                    })
                })
            })
            .transpose()?;

        // Check if the path points to a regular file.
        // If it does, the filter function is never executed.
        // TODO: follow symlinks and check their type instead
        let (root_node, ca_hash) = match state.file_type(path.as_ref())? {
            FileType::Regular => {
                let mut file = state.open(path.as_ref())?;
                // This is a single file, copy it to the blobservice directly.
                let mut hash = sha2::Sha256::new();
                let mut blob_size = 0;
                let mut blob_writer = state
                    .tokio_handle
                    .block_on(async { state.blob_service.open_write().await });

                let mut buf = [0u8; 4096];

                loop {
                    // read bytes into buffer, break out if EOF
                    let len = file.read(&mut buf)?;
                    if len == 0 {
                        break;
                    }
                    blob_size += len as u64;

                    let data = &buf[0..len];

                    // add to blobwriter
                    state
                        .tokio_handle
                        .block_on(async { blob_writer.write_all(data).await })?;

                    // update the sha256 hash function. We can skip that if we're not using it.
                    if !recursive_ingestion {
                        hash.update(data);
                    }
                }

                // close the blob writer, get back the b3 digest.
                let blob_digest = state
                    .tokio_handle
                    .block_on(async { blob_writer.close().await })?;

                let root_node = Node::File {
                    digest: blob_digest,
                    size: blob_size,
                    executable: false,
                };

                let ca_hash = if recursive_ingestion {
                    let (_nar_size, nar_sha256) = state
                        .tokio_handle
                        .block_on(async {
                            state
                                .nar_calculation_service
                                .as_ref()
                                .calculate_nar(&root_node)
                                .await
                        })
                        .map_err(|e| tvix_eval::ErrorKind::TvixError(Rc::new(e)))?;
                    CAHash::Nar(NixHash::Sha256(nar_sha256))
                } else {
                    CAHash::Flat(NixHash::Sha256(hash.finalize().into()))
                };

                (root_node, ca_hash)
            }

            FileType::Directory => {
                if !recursive_ingestion {
                    return Err(ImportError::FlatImportOfNonFile(
                        path.to_string_lossy().to_string(),
                    ))?;
                }

                // do the filtered ingest
                let root_node = filtered_ingest(state.clone(), co, path.as_ref(), filter).await?;

                // calculate the NAR sha256
                let (_nar_size, nar_sha256) = state
                    .tokio_handle
                    .block_on(async {
                        state
                            .nar_calculation_service
                            .as_ref()
                            .calculate_nar(&root_node)
                            .await
                    })
                    .map_err(|e| tvix_eval::ErrorKind::TvixError(Rc::new(e)))?;

                let ca_hash = CAHash::Nar(NixHash::Sha256(nar_sha256));

                (root_node, ca_hash)
            }
            FileType::Symlink => {
                // FUTUREWORK: Nix follows a symlink if it's at the root,
                // except if it's not resolve-able (NixOS/nix#7761).i
                return Err(tvix_eval::ErrorKind::IO {
                    path: Some(path.to_path_buf()),
                    error: Rc::new(std::io::Error::new(
                        std::io::ErrorKind::Unsupported,
                        "builtins.path pointing to a symlink is ill-defined.",
                    )),
                });
            }
            FileType::Unknown => {
                return Err(tvix_eval::ErrorKind::IO {
                    path: Some(path.to_path_buf()),
                    error: Rc::new(std::io::Error::new(
                        std::io::ErrorKind::Unsupported,
                        "unsupported file type",
                    )),
                })
            }
        };

        if let Some(expected_sha256) = expected_sha256 {
            if *ca_hash.hash() != expected_sha256 {
                Err(ImportError::HashMismatch(
                    path.to_string_lossy().to_string(),
                    expected_sha256,
                    ca_hash.hash().into_owned(),
                ))?;
            }
        }

        let path_info = state
            .tokio_handle
            .block_on(async {
                state
                    .register_in_path_info_service(name.as_ref(), path.as_ref(), ca_hash, root_node)
                    .await
            })
            .map_err(|e| tvix_eval::ErrorKind::IO {
                path: Some(path.to_path_buf()),
                error: Rc::new(e),
            })?;

        // We need to attach context to the final output path.
        let outpath = path_info.store_path.to_absolute_path();

        Ok(
            NixString::new_context_from(NixContextElement::Plain(outpath.clone()).into(), outpath)
                .into(),
        )
    }

    #[builtin("filterSource")]
    async fn builtin_filter_source(
        state: Rc<TvixStoreIO>,
        co: GenCo,
        #[lazy] filter: Value,
        path: Value,
    ) -> Result<Value, ErrorKind> {
        let p = path.to_path()?;
        let root_node = filtered_ingest(Rc::clone(&state), co, &p, Some(&filter)).await?;
        let name = tvix_store::import::path_to_name(&p)?;

        let path_info = state
            .tokio_handle
            .block_on(async {
                let (_, nar_sha256) = state
                    .nar_calculation_service
                    .as_ref()
                    .calculate_nar(&root_node)
                    .await?;

                state
                    .register_in_path_info_service(
                        name,
                        &p,
                        CAHash::Nar(NixHash::Sha256(nar_sha256)),
                        root_node,
                    )
                    .await
            })
            .map_err(|err| ErrorKind::IO {
                path: Some(p.to_path_buf()),
                error: err.into(),
            })?;

        // We need to attach context to the final output path.
        let outpath = path_info.store_path.to_absolute_path();

        Ok(
            NixString::new_context_from(NixContextElement::Plain(outpath.clone()).into(), outpath)
                .into(),
        )
    }

    #[builtin("storePath")]
    async fn builtin_store_path(
        state: Rc<TvixStoreIO>,
        co: GenCo,
        path: Value,
    ) -> Result<Value, ErrorKind> {
        let p = match &path {
            Value::String(s) => Path::new(s.as_bytes().to_os_str()?),
            Value::Path(p) => p.as_path(),
            _ => {
                return Err(ErrorKind::TypeError {
                    expected: "string or path",
                    actual: path.type_of(),
                })
            }
        };

        // For this builtin, the path needs to start with an absolute store path.
        let (store_path, _sub_path) = StorePathRef::from_absolute_path_full(p)
            .map_err(|_e| ImportError::PathNotAbsoluteOrInvalid(p.to_path_buf()))?;

        if state.path_exists(p)? {
            Ok(Value::String(NixString::new_context_from(
                [NixContextElement::Plain(store_path.to_absolute_path())].into(),
                p.as_os_str().as_encoded_bytes(),
            )))
        } else {
            Err(ErrorKind::IO {
                path: Some(p.to_path_buf()),
                error: Rc::new(std::io::ErrorKind::NotFound.into()),
            })
        }
    }
}

pub use import_builtins::builtins as import_builtins;