1use futures::TryStreamExt;
3use nix_compat::{nixhash::CAHash, store_path::StorePath};
4use snix_build::buildservice::BuildService;
5use snix_eval::{EvalIO, FileType, StdIO};
6use snix_store::nar::NarCalculationService;
7use std::{
8 cell::RefCell,
9 env,
10 ffi::{OsStr, OsString},
11 io,
12 os::unix::ffi::OsStrExt,
13 path::{Path, PathBuf},
14 sync::Arc,
15};
16use tokio_util::io::SyncIoBridge;
17use tracing::{Level, Span, error, instrument, warn};
18use tracing_indicatif::span_ext::IndicatifSpanExt;
19use url::Url;
20
21use snix_castore::{
22 Node,
23 blobservice::BlobService,
24 directoryservice::{DirectoryService, traversal::descend_to},
25};
26use snix_store::pathinfoservice::{PathInfo, PathInfoService};
27
28use crate::builder;
29use crate::fetchers::Fetcher;
30use crate::known_paths::KnownPaths;
31
32pub struct SnixStoreIO {
48 pub(crate) blob_service: Arc<dyn BlobService>,
50 pub(crate) directory_service: Arc<dyn DirectoryService>,
51 pub(crate) path_info_service: Arc<dyn PathInfoService>,
52 pub(crate) nar_calculation_service: Arc<dyn NarCalculationService>,
53
54 std_io: StdIO,
55 #[allow(dead_code)]
56 build_service: Arc<dyn BuildService>,
57 pub(crate) tokio_handle: tokio::runtime::Handle,
58
59 #[allow(clippy::type_complexity)]
60 pub(crate) fetcher: Fetcher<
61 Arc<dyn BlobService>,
62 Arc<dyn DirectoryService>,
63 Arc<dyn PathInfoService>,
64 Arc<dyn NarCalculationService>,
65 >,
66
67 pub known_paths: RefCell<KnownPaths>,
69}
70
71impl SnixStoreIO {
72 pub fn new(
73 blob_service: Arc<dyn BlobService>,
74 directory_service: Arc<dyn DirectoryService>,
75 path_info_service: Arc<dyn PathInfoService>,
76 nar_calculation_service: Arc<dyn NarCalculationService>,
77 build_service: Arc<dyn BuildService>,
78 tokio_handle: tokio::runtime::Handle,
79 hashed_mirrors: Vec<Url>,
80 ) -> Self {
81 Self {
82 blob_service: blob_service.clone(),
83 directory_service: directory_service.clone(),
84 path_info_service: path_info_service.clone(),
85 nar_calculation_service: nar_calculation_service.clone(),
86 std_io: StdIO {},
87 build_service,
88 tokio_handle,
89 fetcher: Fetcher::new(
90 blob_service,
91 directory_service,
92 path_info_service,
93 nar_calculation_service,
94 hashed_mirrors,
95 ),
96 known_paths: Default::default(),
97 }
98 }
99
100 #[instrument(skip(self, store_path), fields(store_path=%store_path, indicatif.pb_show=tracing::field::Empty), ret(level = Level::TRACE), err(level = Level::TRACE))]
108 async fn store_path_to_path_info(
109 &self,
110 store_path: &StorePath<String>,
111 sub_path: &Path,
112 ) -> io::Result<Option<PathInfo>> {
113 let mut path_info = match self
119 .path_info_service
120 .as_ref()
121 .get(*store_path.digest())
122 .await
123 .map_err(std::io::Error::other)?
124 {
125 Some(path_info) => path_info,
126 None => {
136 let maybe_fetch = self
142 .known_paths
143 .borrow()
144 .get_fetch_for_output_path(store_path);
145
146 match maybe_fetch {
147 Some((name, fetch)) => {
148 let (sp, path_info) = self
149 .fetcher
150 .ingest_and_persist(&name, fetch)
151 .await
152 .map_err(|e| {
153 std::io::Error::new(std::io::ErrorKind::InvalidData, e)
154 })?;
155
156 debug_assert_eq!(
157 sp.to_absolute_path(),
158 store_path.as_ref().to_absolute_path(),
159 "store path returned from fetcher must match store path we have in fetchers"
160 );
161
162 path_info
163 }
164 None => {
165 let (drv_path, drv) = {
167 let known_paths = self.known_paths.borrow();
168 match known_paths.get_drv_path_for_output_path(store_path) {
169 Some(drv_path) => (
170 drv_path.to_owned(),
171 known_paths.get_drv_by_drvpath(drv_path).unwrap().to_owned(),
172 ),
173 None => {
174 warn!(store_path=%store_path, "no drv found");
175 return Ok(None);
177 }
178 }
179 };
180 let span = Span::current();
181 span.pb_start();
182 span.pb_set_style(&snix_tracing::PB_SPINNER_STYLE);
183 span.pb_set_message(&format!("⏳Waiting for inputs {}", &store_path));
184
185 let resolved_inputs = {
189 let known_paths = &self.known_paths.borrow();
190 builder::get_all_inputs(&drv, known_paths, |path| {
191 Box::pin(async move {
192 self.store_path_to_path_info(&path, Path::new("")).await
193 })
194 })
195 }
196 .try_collect()
197 .await?;
198
199 let mut ca = drv.fod_digest().map(|fod_digest| {
201 CAHash::Nar(nix_compat::nixhash::NixHash::Sha256(fod_digest))
202 });
203
204 let build_request =
206 builder::derivation_into_build_request(drv, &resolved_inputs)?;
207
208 let mut output_paths: Vec<StorePath<String>> =
212 Vec::with_capacity(build_request.outputs.len());
213 let all_possible_refs: Vec<StorePath<String>> = build_request
214 .outputs
215 .iter()
216 .map(|p| {
217 let sp = StorePath::<String>::from_bytes(
218 p.strip_prefix(&nix_compat::store_path::STORE_DIR[1..])
219 .expect("output doesn't have expected store_dir prefix")
220 .as_os_str()
221 .as_bytes(),
222 )
223 .expect("Snix bug: cannot parse output as StorePath");
224 output_paths.push(sp.clone());
225
226 sp
227 })
228 .chain(resolved_inputs.keys().cloned())
229 .collect();
230
231 span.pb_set_message(&format!("🔨Building {}", &store_path));
232
233 let build_result = self
235 .build_service
236 .as_ref()
237 .do_build(build_request)
238 .await
239 .map_err(std::io::Error::other)?;
240
241 let mut out_path_info: Option<PathInfo> = None;
242
243 for (output, output_path) in
245 build_result.outputs.into_iter().zip(output_paths)
246 {
247 let (nar_size, nar_sha256) = self
249 .nar_calculation_service
250 .calculate_nar(&output.node)
251 .await
252 .map_err(std::io::Error::other)?;
253
254 let path_info = PathInfo {
256 store_path: output_path.clone(),
257 node: output.node,
258 references: {
259 let mut references =
260 Vec::with_capacity(output.output_needles.len());
261
262 for needle_idx in output.output_needles {
264 let output = all_possible_refs
265 .get(needle_idx as usize)
266 .ok_or(std::io::Error::other("invalid needle_idx"))?
267 .clone();
268 references.push(output);
269 }
270
271 references.sort();
273 references
274 },
275 nar_size,
276 nar_sha256,
277 signatures: vec![],
278 deriver: Some(
279 StorePath::from_name_and_digest_fixed(
280 drv_path
281 .name()
282 .strip_suffix(".drv")
283 .expect("missing .drv suffix"),
284 *drv_path.digest(),
285 )
286 .expect(
287 "Snix bug: StorePath without .drv suffix must be valid",
288 ),
289 ),
290 ca: ca.take(),
292 };
293
294 self.path_info_service
295 .put(path_info.clone())
296 .await
297 .map_err(std::io::Error::other)?;
298
299 if store_path == &output_path {
300 out_path_info = Some(path_info);
301 }
302 }
303
304 out_path_info.ok_or(io::Error::other("build didn't produce store path"))?
305 }
306 }
307 }
308 };
309
310 let sub_path = snix_castore::PathBuf::from_host_path(sub_path, true)?;
313
314 Ok(
315 descend_to(&self.directory_service, path_info.node.clone(), sub_path)
316 .await
317 .map_err(std::io::Error::other)?
318 .map(|node| {
319 path_info.node = node;
320 path_info
321 }),
322 )
323 }
324}
325
326fn node_get_type(node: &Node) -> FileType {
328 match node {
329 Node::Directory { .. } => FileType::Directory,
330 Node::File { .. } => FileType::Regular,
331 Node::Symlink { .. } => FileType::Symlink,
332 }
333}
334
335impl EvalIO for SnixStoreIO {
336 #[instrument(skip(self), ret(level = Level::TRACE), err)]
337 fn path_exists(&self, path: &Path) -> io::Result<bool> {
338 if let Ok((store_path, sub_path)) = StorePath::from_absolute_path_full(path) {
339 if self
340 .tokio_handle
341 .block_on(self.store_path_to_path_info(&store_path, sub_path))?
342 .is_some()
343 {
344 Ok(true)
345 } else {
346 self.std_io.path_exists(path)
349 }
350 } else {
351 self.std_io.path_exists(path)
353 }
354 }
355
356 #[instrument(skip(self), err)]
357 fn open(&self, path: &Path) -> io::Result<Box<dyn io::Read>> {
358 if let Ok((store_path, sub_path)) = StorePath::from_absolute_path_full(path) {
359 if let Some(path_info) = self
360 .tokio_handle
361 .block_on(async { self.store_path_to_path_info(&store_path, sub_path).await })?
362 {
363 match path_info.node {
365 Node::Directory { .. } => {
366 Err(io::Error::new(
368 io::ErrorKind::Unsupported,
369 format!("tried to open directory at {path:?}"),
370 ))
371 }
372 Node::File { digest, .. } => {
373 self.tokio_handle.block_on(async {
374 let resp = self.blob_service.as_ref().open_read(&digest).await?;
375 match resp {
376 Some(blob_reader) => {
377 Ok(Box::new(SyncIoBridge::new(blob_reader))
379 as Box<dyn io::Read>)
380 }
381 None => {
382 error!(
383 blob.digest = %digest,
384 "blob not found",
385 );
386 Err(io::Error::new(
387 io::ErrorKind::NotFound,
388 format!("blob {} not found", &digest),
389 ))
390 }
391 }
392 })
393 }
394 Node::Symlink { .. } => Err(io::Error::new(
395 io::ErrorKind::Unsupported,
396 "open for symlinks is unsupported",
397 ))?,
398 }
399 } else {
400 self.std_io.open(path)
403 }
404 } else {
405 self.std_io.open(path)
407 }
408 }
409
410 #[instrument(skip(self), ret(level = Level::TRACE), err)]
411 fn file_type(&self, path: &Path) -> io::Result<FileType> {
412 if let Ok((store_path, sub_path)) = StorePath::from_absolute_path_full(path) {
413 if let Some(path_info) = self
414 .tokio_handle
415 .block_on(async { self.store_path_to_path_info(&store_path, sub_path).await })?
416 {
417 Ok(node_get_type(&path_info.node))
418 } else {
419 self.std_io.file_type(path)
420 }
421 } else {
422 self.std_io.file_type(path)
423 }
424 }
425
426 #[instrument(skip(self), ret(level = Level::TRACE), err)]
427 fn read_dir(&self, path: &Path) -> io::Result<Vec<(bytes::Bytes, FileType)>> {
428 if let Ok((store_path, sub_path)) = StorePath::from_absolute_path_full(path) {
429 if let Some(path_info) = self
430 .tokio_handle
431 .block_on(async { self.store_path_to_path_info(&store_path, sub_path).await })?
432 {
433 match path_info.node {
434 Node::Directory { digest, .. } => {
435 let directory = self
437 .tokio_handle
438 .block_on(async { self.directory_service.as_ref().get(&digest).await })
439 .map_err(std::io::Error::other)?
440 .ok_or_else(|| {
441 error!(
443 directory.digest = %digest,
444 path = ?path,
445 "directory not found",
446 );
447 io::Error::new(
448 io::ErrorKind::NotFound,
449 format!("directory {digest} does not exist"),
450 )
451 })?;
452
453 Ok(directory
455 .into_nodes()
456 .map(|(name, node)| (name.into(), node_get_type(&node)))
457 .collect())
458 }
459 Node::File { .. } => {
460 Err(io::Error::new(
462 io::ErrorKind::Unsupported,
463 "tried to readdir path {:?}, which is a file",
464 ))?
465 }
466 Node::Symlink { .. } => Err(io::Error::new(
467 io::ErrorKind::Unsupported,
468 "read_dir for symlinks is unsupported",
469 ))?,
470 }
471 } else {
472 self.std_io.read_dir(path)
473 }
474 } else {
475 self.std_io.read_dir(path)
476 }
477 }
478
479 #[instrument(skip(self), ret(level = Level::TRACE), err)]
480 fn import_path(&self, path: &Path) -> io::Result<PathBuf> {
481 let path_info = self.tokio_handle.block_on({
482 snix_store::import::import_path_as_nar_ca(
483 path,
484 snix_store::import::path_to_name(path)?,
485 &self.blob_service,
486 &self.directory_service,
487 &self.path_info_service,
488 &self.nar_calculation_service,
489 )
490 })?;
491
492 Ok(path_info.store_path.to_absolute_path().into())
494 }
495
496 #[instrument(skip(self), ret(level = Level::TRACE))]
497 fn store_dir(&self) -> Option<String> {
498 Some("/nix/store".to_string())
499 }
500
501 fn get_env(&self, key: &OsStr) -> Option<OsString> {
502 env::var_os(key)
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use std::{path::Path, rc::Rc, sync::Arc};
509
510 use bstr::ByteSlice;
511 use clap::Parser;
512 use snix_build::buildservice::DummyBuildService;
513 use snix_eval::{EvalIO, EvaluationResult};
514 use snix_store::utils::{ServiceUrlsMemory, construct_services};
515 use tempfile::TempDir;
516
517 use super::SnixStoreIO;
518 use crate::builtins::{add_derivation_builtins, add_fetcher_builtins, add_import_builtins};
519
520 fn eval(str: &str) -> EvaluationResult {
524 let tokio_runtime = tokio::runtime::Runtime::new().unwrap();
525 let (blob_service, directory_service, path_info_service, nar_calculation_service) =
526 tokio_runtime
527 .block_on(async {
528 construct_services(ServiceUrlsMemory::parse_from(std::iter::empty::<&str>()))
529 .await
530 })
531 .unwrap();
532
533 let io = Rc::new(SnixStoreIO::new(
534 blob_service,
535 directory_service,
536 path_info_service,
537 nar_calculation_service.into(),
538 Arc::<DummyBuildService>::default(),
539 tokio_runtime.handle().clone(),
540 Vec::new(),
541 ));
542
543 let mut eval_builder =
544 snix_eval::Evaluation::builder(io.clone() as Rc<dyn EvalIO>).enable_import();
545 eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&io));
546 eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&io));
547 eval_builder = add_import_builtins(eval_builder, io);
548 let eval = eval_builder.build();
549
550 eval.evaluate(str, None)
552 }
553
554 fn import_path_and_compare<P: AsRef<Path>>(p: P) -> Option<String> {
558 let code = format!(r#""${{{}}}""#, p.as_ref().display());
562 let result = eval(&code);
563
564 if !result.errors.is_empty() {
565 return None;
566 }
567
568 let value = result.value.expect("must be some");
569 match value {
570 snix_eval::Value::String(s) => Some(s.to_str_lossy().into_owned()),
571 _ => panic!("unexpected value type: {value:?}"),
572 }
573 }
574
575 #[test]
578 fn import_directory() {
579 let tmpdir = TempDir::new().unwrap();
580
581 let src_path = tmpdir.path().join("test");
583 std::fs::create_dir(&src_path).unwrap();
584
585 std::fs::write(src_path.join(".keep"), vec![]).unwrap();
587
588 assert_eq!(
590 Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
591 import_path_and_compare(&src_path)
592 );
593
594 assert_eq!(
596 Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
597 import_path_and_compare(src_path.join("."))
598 );
599 }
600
601 #[test]
604 fn import_file() {
605 let tmpdir = TempDir::new().unwrap();
606
607 std::fs::write(tmpdir.path().join("empty"), vec![]).unwrap();
609
610 assert_eq!(
611 Some("/nix/store/lx5i78a4izwk2qj1nq8rdc07y8zrwy90-empty".to_string()),
612 import_path_and_compare(tmpdir.path().join("empty"))
613 );
614
615 std::fs::write(tmpdir.path().join("hello.txt"), b"Hello World!").unwrap();
617
618 assert_eq!(
619 Some("/nix/store/925f1jb1ajrypjbyq7rylwryqwizvhp0-hello.txt".to_string()),
620 import_path_and_compare(tmpdir.path().join("hello.txt"))
621 );
622 }
623
624 #[test]
628 fn nonexisting_path_without_import() {
629 let result = eval("toString ({ line = 42; col = 42; file = /deep/thought; }.file)");
630
631 assert!(result.errors.is_empty(), "expect evaluation to succeed");
632 let value = result.value.expect("must be some");
633
634 match value {
635 snix_eval::Value::String(s) => {
636 assert_eq!(*s, "/deep/thought");
637 }
638 _ => panic!("unexpected value type: {value:?}"),
639 }
640 }
641}