snix_glue/builtins/
fetchers.rs

1//! Contains builtins that fetch paths from the Internet, or local filesystem.
2
3use super::utils::select_string;
4use crate::{
5    fetchers::{Fetch, url_basename},
6    snix_store_io::SnixStoreIO,
7};
8use nix_compat::nixhash::{HashAlgo, NixHash};
9use snix_eval::builtin_macros::builtins;
10use snix_eval::generators::Gen;
11use snix_eval::generators::GenCo;
12use snix_eval::{CatchableErrorKind, ErrorKind, Value};
13use std::rc::Rc;
14use url::Url;
15
16// Used as a return type for extract_fetch_args, which is sharing some
17// parsing code between the fetchurl and fetchTarball builtins.
18struct NixFetchArgs {
19    url: Url,
20    name: Option<String>,
21    sha256: Option<[u8; 32]>,
22}
23
24// `fetchurl` and `fetchTarball` accept a single argument, which can either be the URL (as string),
25// or an attrset, where `url`, `sha256` and `name` keys are allowed.
26async fn extract_fetch_args(
27    co: &GenCo,
28    args: Value,
29) -> Result<Result<NixFetchArgs, CatchableErrorKind>, ErrorKind> {
30    if let Ok(url_str) = args.to_str() {
31        // Get the raw bytes, not the ToString repr.
32        let url_str =
33            String::from_utf8(url_str.as_bytes().to_vec()).map_err(|_| ErrorKind::Utf8)?;
34
35        // Parse the URL.
36        let url = Url::parse(&url_str).map_err(|e| ErrorKind::SnixError(Rc::new(e)))?;
37
38        return Ok(Ok(NixFetchArgs {
39            url,
40            name: None,
41            sha256: None,
42        }));
43    }
44
45    let attrs = args.to_attrs().map_err(|_| ErrorKind::TypeError {
46        expected: "attribute set or contextless string",
47        actual: args.type_of(),
48    })?;
49
50    // Reject disallowed attrset keys, to match Nix' behaviour.
51    // We complain about the first unexpected key we find in the list.
52    const VALID_KEYS: [&[u8]; 3] = [b"url", b"name", b"sha256"];
53    if let Some(first_invalid_key) = attrs.keys().find(|k| !&VALID_KEYS.contains(&k.as_bytes())) {
54        return Err(ErrorKind::UnexpectedArgumentBuiltin(
55            first_invalid_key.clone(),
56        ));
57    }
58
59    let url_str = match select_string(co, &attrs, "url").await? {
60        Ok(s) => s.ok_or_else(|| ErrorKind::AttributeNotFound { name: "url".into() })?,
61        Err(cek) => return Ok(Err(cek)),
62    };
63    let name = match select_string(co, &attrs, "name").await? {
64        Ok(s) => s,
65        Err(cek) => return Ok(Err(cek)),
66    };
67    let sha256_str = match select_string(co, &attrs, "sha256").await? {
68        Ok(s) => s,
69        Err(cek) => return Ok(Err(cek)),
70    };
71
72    Ok(Ok(NixFetchArgs {
73        url: Url::parse(&url_str).map_err(|e| ErrorKind::SnixError(Rc::new(e)))?,
74        name,
75        // parse the sha256 string into a digest, and bail out if it's not sha256.
76        sha256: sha256_str
77            .map(
78                |sha256_str| match NixHash::from_str(&sha256_str, Some(HashAlgo::Sha256)) {
79                    Ok(NixHash::Sha256(digest)) => Ok(digest),
80                    _ => Err(ErrorKind::InvalidHash(sha256_str)),
81                },
82            )
83            .transpose()?,
84    }))
85}
86
87#[allow(unused_variables)] // for the `state` arg, for now
88#[builtins(state = "Rc<SnixStoreIO>")]
89pub(crate) mod fetcher_builtins {
90    use bstr::ByteSlice;
91    use nix_compat::{flakeref, nixhash::NixHash};
92    use std::collections::BTreeMap;
93
94    use super::*;
95
96    /// Consumes a fetch.
97    /// If there is enough info to calculate the store path without fetching,
98    /// queue the fetch to be fetched lazily, and return the store path.
99    /// If there's not enough info to calculate it, do the fetch now, and then
100    /// return the store path.
101    fn fetch_lazy(state: Rc<SnixStoreIO>, name: String, fetch: Fetch) -> Result<Value, ErrorKind> {
102        match fetch
103            .store_path(&name)
104            .map_err(|e| ErrorKind::SnixError(Rc::new(e)))?
105        {
106            Some(store_path) => {
107                // Move the fetch to KnownPaths, so it can be actually fetched later.
108                let sp = state
109                    .known_paths
110                    .borrow_mut()
111                    .add_fetch(fetch, &name)
112                    .expect("Snix bug: should only fail if the store path cannot be calculated");
113
114                debug_assert_eq!(
115                    sp, store_path,
116                    "calculated store path by KnownPaths should match"
117                );
118
119                // Emit the calculated Store Path.
120                Ok(Value::Path(Box::new(store_path.to_absolute_path().into())))
121            }
122            None => {
123                // If we don't have enough info, do the fetch now.
124                let (store_path, _path_info) = state
125                    .tokio_handle
126                    .block_on(async { state.fetcher.ingest_and_persist(&name, fetch).await })
127                    .map_err(|e| ErrorKind::SnixError(Rc::new(e)))?;
128
129                Ok(Value::Path(Box::new(store_path.to_absolute_path().into())))
130            }
131        }
132    }
133
134    #[builtin("fetchurl")]
135    async fn builtin_fetchurl(
136        state: Rc<SnixStoreIO>,
137        co: GenCo,
138        args: Value,
139    ) -> Result<Value, ErrorKind> {
140        let args = match extract_fetch_args(&co, args).await? {
141            Ok(args) => args,
142            Err(cek) => return Ok(Value::from(cek)),
143        };
144
145        // Derive the name from the URL basename if not set explicitly.
146        let name = args
147            .name
148            .unwrap_or_else(|| url_basename(&args.url).to_owned());
149
150        fetch_lazy(
151            state,
152            name,
153            Fetch::URL {
154                url: args.url,
155                exp_hash: args.sha256.map(NixHash::Sha256),
156            },
157        )
158    }
159
160    #[builtin("fetchTarball")]
161    async fn builtin_fetch_tarball(
162        state: Rc<SnixStoreIO>,
163        co: GenCo,
164        args: Value,
165    ) -> Result<Value, ErrorKind> {
166        let args = match extract_fetch_args(&co, args).await? {
167            Ok(args) => args,
168            Err(cek) => return Ok(Value::from(cek)),
169        };
170
171        // Name defaults to "source" if not set explicitly.
172        const DEFAULT_NAME_FETCH_TARBALL: &str = "source";
173        let name = args
174            .name
175            .unwrap_or_else(|| DEFAULT_NAME_FETCH_TARBALL.to_owned());
176
177        fetch_lazy(
178            state,
179            name,
180            Fetch::Tarball {
181                url: args.url,
182                exp_nar_sha256: args.sha256,
183            },
184        )
185    }
186
187    #[builtin("fetchGit")]
188    async fn builtin_fetch_git(
189        state: Rc<SnixStoreIO>,
190        co: GenCo,
191        args: Value,
192    ) -> Result<Value, ErrorKind> {
193        Err(ErrorKind::NotImplemented("fetchGit"))
194    }
195
196    // FUTUREWORK: make it a feature flag once #64 is implemented
197    #[builtin("parseFlakeRef")]
198    async fn builtin_parse_flake_ref(
199        state: Rc<SnixStoreIO>,
200        co: GenCo,
201        value: Value,
202    ) -> Result<Value, ErrorKind> {
203        let flake_ref = value.to_str()?;
204        let flake_ref_str = flake_ref.to_str()?;
205
206        let fetch_args = flake_ref_str
207            .parse()
208            .map_err(|err| ErrorKind::SnixError(Rc::new(err)))?;
209
210        // Convert the FlakeRef to our Value format
211        let mut attrs = BTreeMap::new();
212
213        // Extract type and url based on the variant
214        match fetch_args {
215            flakeref::FlakeRef::Git { url, .. } => {
216                attrs.insert("type".into(), Value::from("git"));
217                attrs.insert("url".into(), Value::from(url.to_string()));
218            }
219            flakeref::FlakeRef::GitHub {
220                owner, repo, r#ref, ..
221            } => {
222                attrs.insert("type".into(), Value::from("github"));
223                attrs.insert("owner".into(), Value::from(owner));
224                attrs.insert("repo".into(), Value::from(repo));
225                if let Some(ref_name) = r#ref {
226                    attrs.insert("ref".into(), Value::from(ref_name));
227                }
228            }
229            flakeref::FlakeRef::GitLab { owner, repo, .. } => {
230                attrs.insert("type".into(), Value::from("gitlab"));
231                attrs.insert("owner".into(), Value::from(owner));
232                attrs.insert("repo".into(), Value::from(repo));
233            }
234            flakeref::FlakeRef::File { url, .. } => {
235                attrs.insert("type".into(), Value::from("file"));
236                attrs.insert("url".into(), Value::from(url.to_string()));
237            }
238            flakeref::FlakeRef::Tarball { url, .. } => {
239                attrs.insert("type".into(), Value::from("tarball"));
240                attrs.insert("url".into(), Value::from(url.to_string()));
241            }
242            flakeref::FlakeRef::Path { path, .. } => {
243                attrs.insert("type".into(), Value::from("path"));
244                attrs.insert(
245                    "path".into(),
246                    Value::from(path.to_string_lossy().into_owned()),
247                );
248            }
249            _ => {
250                // For all other ref types, return a simple type/url attributes
251                attrs.insert("type".into(), Value::from("indirect"));
252                attrs.insert("url".into(), Value::from(flake_ref_str));
253            }
254        }
255
256        Ok(Value::Attrs(Box::new(attrs.into())))
257    }
258}