//! Contains builtins that fetch paths from the Internet, or local filesystem. use super::utils::select_string; use crate::{ fetchers::{url_basename, Fetch}, tvix_store_io::TvixStoreIO, }; use nix_compat::nixhash; use std::rc::Rc; use tvix_eval::builtin_macros::builtins; use tvix_eval::generators::Gen; use tvix_eval::generators::GenCo; use tvix_eval::{CatchableErrorKind, ErrorKind, Value}; use url::Url; // Used as a return type for extract_fetch_args, which is sharing some // parsing code between the fetchurl and fetchTarball builtins. struct NixFetchArgs { url: Url, name: Option, sha256: Option<[u8; 32]>, } // `fetchurl` and `fetchTarball` accept a single argument, which can either be the URL (as string), // or an attrset, where `url`, `sha256` and `name` keys are allowed. async fn extract_fetch_args( co: &GenCo, args: Value, ) -> Result, ErrorKind> { if let Ok(url_str) = args.to_str() { // Get the raw bytes, not the ToString repr. let url_str = String::from_utf8(url_str.as_bytes().to_vec()).map_err(|_| ErrorKind::Utf8)?; // Parse the URL. let url = Url::parse(&url_str).map_err(|e| ErrorKind::TvixError(Rc::new(e)))?; return Ok(Ok(NixFetchArgs { url, name: None, sha256: None, })); } let attrs = args.to_attrs().map_err(|_| ErrorKind::TypeError { expected: "attribute set or contextless string", actual: args.type_of(), })?; let url_str = match select_string(co, &attrs, "url").await? { Ok(s) => s.ok_or_else(|| ErrorKind::AttributeNotFound { name: "url".into() })?, Err(cek) => return Ok(Err(cek)), }; let name = match select_string(co, &attrs, "name").await? { Ok(s) => s, Err(cek) => return Ok(Err(cek)), }; let sha256_str = match select_string(co, &attrs, "sha256").await? { Ok(s) => s, Err(cek) => return Ok(Err(cek)), }; // Disallow other attrset keys, to match Nix' behaviour. // We complain about the first unexpected key we find in the list. const VALID_KEYS: [&[u8]; 3] = [b"url", b"name", b"sha256"]; if let Some(first_invalid_key) = attrs.keys().find(|k| !&VALID_KEYS.contains(&k.as_bytes())) { return Err(ErrorKind::UnexpectedArgumentBuiltin( first_invalid_key.clone(), )); } // parse the sha256 string into a digest. let sha256 = match sha256_str { Some(sha256_str) => { let nixhash = nixhash::from_str(&sha256_str, Some("sha256")) // TODO: DerivationError::InvalidOutputHash should be moved to ErrorKind::InvalidHash and used here instead .map_err(|e| ErrorKind::TvixError(Rc::new(e)))?; Some(nixhash.digest_as_bytes().try_into().expect("is sha256")) } None => None, }; // Parse the URL. let url = Url::parse(&url_str).map_err(|e| ErrorKind::TvixError(Rc::new(e)))?; Ok(Ok(NixFetchArgs { url, name, sha256 })) } #[allow(unused_variables)] // for the `state` arg, for now #[builtins(state = "Rc")] pub(crate) mod fetcher_builtins { use nix_compat::nixhash::NixHash; use super::*; /// Consumes a fetch. /// If there is enough info to calculate the store path without fetching, /// queue the fetch to be fetched lazily, and return the store path. /// If there's not enough info to calculate it, do the fetch now, and then /// return the store path. fn fetch_lazy(state: Rc, name: String, fetch: Fetch) -> Result { match fetch .store_path(&name) .map_err(|e| ErrorKind::TvixError(Rc::new(e)))? { Some(store_path) => { // Move the fetch to KnownPaths, so it can be actually fetched later. let sp = state .known_paths .borrow_mut() .add_fetch(fetch, &name) .expect("Tvix bug: should only fail if the store path cannot be calculated"); debug_assert_eq!( sp, store_path, "calculated store path by KnownPaths should match" ); // Emit the calculated Store Path. Ok(Value::Path(Box::new(store_path.to_absolute_path().into()))) } None => { // If we don't have enough info, do the fetch now. let (store_path, _root_node) = state .tokio_handle .block_on(async { state.fetcher.ingest_and_persist(&name, fetch).await }) .map_err(|e| ErrorKind::TvixError(Rc::new(e)))?; Ok(Value::Path(Box::new(store_path.to_absolute_path().into()))) } } } #[builtin("fetchurl")] async fn builtin_fetchurl( state: Rc, co: GenCo, args: Value, ) -> Result { let args = match extract_fetch_args(&co, args).await? { Ok(args) => args, Err(cek) => return Ok(Value::from(cek)), }; // Derive the name from the URL basename if not set explicitly. let name = args .name .unwrap_or_else(|| url_basename(&args.url).to_owned()); fetch_lazy( state, name, Fetch::URL { url: args.url, exp_hash: args.sha256.map(NixHash::Sha256), }, ) } #[builtin("fetchTarball")] async fn builtin_fetch_tarball( state: Rc, co: GenCo, args: Value, ) -> Result { let args = match extract_fetch_args(&co, args).await? { Ok(args) => args, Err(cek) => return Ok(Value::from(cek)), }; // Name defaults to "source" if not set explicitly. const DEFAULT_NAME_FETCH_TARBALL: &str = "source"; let name = args .name .unwrap_or_else(|| DEFAULT_NAME_FETCH_TARBALL.to_owned()); fetch_lazy( state, name, Fetch::Tarball { url: args.url, exp_nar_sha256: args.sha256, }, ) } #[builtin("fetchGit")] async fn builtin_fetch_git( state: Rc, co: GenCo, args: Value, ) -> Result { Err(ErrorKind::NotImplemented("fetchGit")) } }