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