1use super::utils::select_string;
4use crate::snix_store_io::SnixStoreIO;
5use nix_compat::nixhash::{HashAlgo, NixHash};
6use snix_eval::builtin_macros::builtins;
7use snix_eval::generators::Gen;
8use snix_eval::generators::GenCo;
9use snix_eval::{CatchableErrorKind, ErrorKind, Value, try_cek};
10use std::{rc::Rc, sync::Arc};
11use url::Url;
12
13struct NixFetchArgs {
16 url: Url,
17 name: Option<String>,
18 sha256: Option<[u8; 32]>,
19}
20
21async fn extract_fetch_args(
24 co: &GenCo,
25 args: Value,
26) -> Result<Result<NixFetchArgs, CatchableErrorKind>, ErrorKind> {
27 if let Ok(url_str) = args.to_str() {
28 let url_str =
30 String::from_utf8(url_str.as_bytes().to_vec()).map_err(|_| ErrorKind::Utf8)?;
31
32 let url = Url::parse(&url_str).map_err(|e| ErrorKind::SnixError(Arc::from(e)))?;
34
35 return Ok(Ok(NixFetchArgs {
36 url,
37 name: None,
38 sha256: None,
39 }));
40 }
41
42 let attrs = args.to_attrs().map_err(|_| ErrorKind::TypeError {
43 expected: "attribute set or contextless string",
44 actual: args.type_of(),
45 })?;
46
47 const VALID_KEYS: [&[u8]; 3] = [b"url", b"name", b"sha256"];
50 if let Some(first_invalid_key) = attrs.keys().find(|k| !&VALID_KEYS.contains(&k.as_bytes())) {
51 return Err(ErrorKind::UnexpectedArgumentBuiltin(
52 first_invalid_key.clone(),
53 ));
54 }
55
56 let url_str = try_cek!(select_string(co, &attrs, "url").await?)
57 .ok_or_else(|| ErrorKind::AttributeNotFound { name: "url".into() })?;
58 let name = try_cek!(select_string(co, &attrs, "name").await?);
59 let sha256_str = try_cek!(select_string(co, &attrs, "sha256").await?);
60
61 Ok(Ok(NixFetchArgs {
62 url: Url::parse(&url_str).map_err(|e| ErrorKind::SnixError(Arc::from(e)))?,
63 name,
64 sha256: sha256_str
66 .map(
67 |sha256_str| match NixHash::from_str(&sha256_str, Some(HashAlgo::Sha256)) {
68 Ok(NixHash::Sha256(digest)) => Ok(digest),
69 _ => Err(ErrorKind::InvalidHash(sha256_str)),
70 },
71 )
72 .transpose()?,
73 }))
74}
75
76#[allow(unused_variables)] #[builtins(state = "Rc<SnixStoreIO>")]
78pub(crate) mod fetcher_builtins {
79 use bstr::ByteSlice;
80 use nix_compat::{flakeref, nixhash::NixHash};
81 use snix_build_glue::fetchers::Fetch;
82 use snix_eval::{NixContext, NixString, try_cek_to_value};
83 use std::collections::BTreeMap;
84
85 use super::*;
86
87 fn url_basename(url: &Url) -> &str {
89 let s = url.path().trim_end_matches('/');
90
91 match s.rsplit_once('/') {
92 None => url.host_str().unwrap_or_default(),
93 Some((_, basename)) => basename,
94 }
95 }
96
97 fn fetch_lazy(state: Rc<SnixStoreIO>, name: String, fetch: Fetch) -> Result<Value, ErrorKind> {
105 let store_path = match fetch
106 .store_path(&name)
107 .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?
108 {
109 Some(store_path) => {
110 let sp = state
112 .build_state
113 .known_paths
114 .borrow_mut()
115 .add_fetch(fetch, &name)
116 .expect("Snix bug: should only fail if the store path cannot be calculated");
117
118 debug_assert_eq!(
119 sp, store_path,
120 "calculated store path by KnownPaths should match"
121 );
122 sp
123 }
124 None => {
125 let (store_path, _path_info) = state
127 .tokio_handle
128 .block_on(async {
129 state
130 .build_state
131 .fetcher
132 .ingest_and_persist(&name, fetch)
133 .await
134 })
135 .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?;
136
137 store_path
138 }
139 };
140
141 let s = store_path.to_absolute_path();
142
143 let context = NixContext::new().append(snix_eval::NixContextElement::Plain(s.clone()));
145 Ok(Value::String(NixString::new_context_from(context, s)))
146 }
147
148 #[builtin("fetchurl")]
149 async fn builtin_fetchurl(
150 state: Rc<SnixStoreIO>,
151 co: GenCo,
152 args: Value,
153 ) -> Result<Value, ErrorKind> {
154 let args = try_cek_to_value!(extract_fetch_args(&co, args).await?);
155
156 let name = args
158 .name
159 .unwrap_or_else(|| url_basename(&args.url).to_owned());
160
161 fetch_lazy(
162 state,
163 name,
164 Fetch::URL {
165 url: args.url,
166 exp_hash: args.sha256.map(NixHash::Sha256),
167 },
168 )
169 }
170
171 #[builtin("fetchTarball")]
172 async fn builtin_fetch_tarball(
173 state: Rc<SnixStoreIO>,
174 co: GenCo,
175 args: Value,
176 ) -> Result<Value, ErrorKind> {
177 let args = try_cek_to_value!(extract_fetch_args(&co, args).await?);
178
179 const DEFAULT_NAME_FETCH_TARBALL: &str = "source";
181 let name = args
182 .name
183 .unwrap_or_else(|| DEFAULT_NAME_FETCH_TARBALL.to_owned());
184
185 fetch_lazy(
186 state,
187 name,
188 Fetch::Tarball {
189 url: args.url,
190 exp_nar_sha256: args.sha256,
191 },
192 )
193 }
194
195 #[builtin("fetchGit")]
196 async fn builtin_fetch_git(
197 state: Rc<SnixStoreIO>,
198 co: GenCo,
199 args: Value,
200 ) -> Result<Value, ErrorKind> {
201 Err(ErrorKind::NotImplemented("fetchGit"))
202 }
203
204 #[builtin("parseFlakeRef")]
206 async fn builtin_parse_flake_ref(
207 state: Rc<SnixStoreIO>,
208 co: GenCo,
209 value: Value,
210 ) -> Result<Value, ErrorKind> {
211 let flake_ref = value.to_str()?;
212 let flake_ref_str = flake_ref.to_str()?;
213
214 let fetch_args = flake_ref_str
215 .parse()
216 .map_err(|err| ErrorKind::SnixError(Arc::new(err)))?;
217
218 let mut attrs = BTreeMap::new();
220
221 match fetch_args {
223 flakeref::FlakeRef::Git { url, .. } => {
224 attrs.insert("type".into(), Value::from("git"));
225 attrs.insert("url".into(), Value::from(url.to_string()));
226 }
227 flakeref::FlakeRef::GitHub {
228 owner, repo, r#ref, ..
229 } => {
230 attrs.insert("type".into(), Value::from("github"));
231 attrs.insert("owner".into(), Value::from(owner));
232 attrs.insert("repo".into(), Value::from(repo));
233 if let Some(ref_name) = r#ref {
234 attrs.insert("ref".into(), Value::from(ref_name));
235 }
236 }
237 flakeref::FlakeRef::GitLab { owner, repo, .. } => {
238 attrs.insert("type".into(), Value::from("gitlab"));
239 attrs.insert("owner".into(), Value::from(owner));
240 attrs.insert("repo".into(), Value::from(repo));
241 }
242 flakeref::FlakeRef::File { url, .. } => {
243 attrs.insert("type".into(), Value::from("file"));
244 attrs.insert("url".into(), Value::from(url.to_string()));
245 }
246 flakeref::FlakeRef::Tarball { url, .. } => {
247 attrs.insert("type".into(), Value::from("tarball"));
248 attrs.insert("url".into(), Value::from(url.to_string()));
249 }
250 flakeref::FlakeRef::Path { path, .. } => {
251 attrs.insert("type".into(), Value::from("path"));
252 attrs.insert(
253 "path".into(),
254 Value::from(path.to_string_lossy().into_owned()),
255 );
256 }
257 _ => {
258 attrs.insert("type".into(), Value::from("indirect"));
260 attrs.insert("url".into(), Value::from(flake_ref_str));
261 }
262 }
263
264 Ok(Value::Attrs(attrs.into()))
265 }
266
267 #[cfg(test)]
268 mod tests {
269 mod url_basename {
270 use super::super::*;
271 use rstest::rstest;
272
273 #[rstest]
274 #[case::empty_path("", "localhost")]
275 #[case::path_on_root("/dir", "dir")]
276 #[case::relative_path("dir/foo", "foo")]
277 #[case::root_with_trailing_slash("/", "localhost")]
278 #[case::root_with_many_trailing_slashes("///", "localhost")]
279 #[case::trailing_slash("/dir/", "dir")]
280 #[case::many_trailing_slashes("/dir//", "dir")]
281 fn test_url_basename(#[case] url_path: &str, #[case] exp_basename: &str) {
282 let mut url = Url::parse("http://localhost").expect("invalid url");
283 url.set_path(url_path);
284 assert_eq!(url_basename(&url), exp_basename);
285 }
286 }
287 }
288}