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