snix_glue/
snix_store_io.rs

1//! This module provides an implementation of EvalIO talking to snix-store.
2use 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
32/// Implements [EvalIO], asking given [PathInfoService], [DirectoryService]
33/// and [BlobService].
34///
35/// In case the given path does not exist in these stores, we ask StdIO.
36/// This is to both cover cases of syntactically valid store paths, that exist
37/// on the filesystem (still managed by Nix), as well as being able to read
38/// files outside store paths.
39///
40/// This structure is also directly used by the derivation builtins
41/// and tightly coupled to it.
42///
43/// In the future, we may revisit that coupling and figure out how to generalize this interface and
44/// hide this implementation detail of the glue itself so that glue can be used with more than one
45/// implementation of "Snix Store IO" which does not necessarily bring the concept of blob service,
46/// directory service or path info service.
47pub struct SnixStoreIO {
48    // This is public so helper functions can interact with the stores directly.
49    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    // Paths known how to produce, by building or fetching.
68    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    /// for a given [StorePath] and additional [Path] inside the store path,
101    /// look up the [PathInfo], and if it exists, and then uses
102    /// [descend_to] to return the
103    /// [Node] specified by `sub_path`.
104    ///
105    /// In case there is no PathInfo yet, this means we need to build it
106    /// (which currently is stubbed out still).
107    #[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        // Find the root node for the store_path.
114        // It asks the PathInfoService first, but in case there was a Derivation
115        // produced that would build it, fall back to triggering the build.
116        // To populate the input nodes, it might recursively trigger builds of
117        // its dependencies too.
118        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            // If there's no PathInfo found, this normally means we have to
127            // trigger the build (and insert into PathInfoService, after
128            // reference scanning).
129            // However, as Snix is (currently) not managing /nix/store itself,
130            // we return Ok(None) to let std_io take over.
131            // While reading from store paths that are not known to Snix during
132            // that evaluation clearly is an impurity, we still need to support
133            // it for things like <nixpkgs> pointing to a store path.
134            // In the future, these things will (need to) have PathInfo.
135            None => {
136                // The store path doesn't exist yet, so we need to fetch or build it.
137                // We check for fetches first, as we might have both native
138                // fetchers and FODs in KnownPaths, and prefer the former.
139                // This will also find [Fetch] synthesized from
140                // `builtin:fetchurl` Derivations.
141                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                        // Look up the derivation for this output path.
166                        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                                    // let StdIO take over
176                                    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                        // derivation_to_build_request needs castore nodes for all inputs.
186                        // Provide them, which means, here is where we recursively build
187                        // all dependencies.
188                        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                        // precompute the `ca` field in the PathInfo, so we can send off drv.
200                        let mut ca = drv.fod_digest().map(|fod_digest| {
201                            CAHash::Nar(nix_compat::nixhash::NixHash::Sha256(fod_digest))
202                        });
203
204                        // synthesize the build request.
205                        let build_request =
206                            builder::derivation_into_build_request(drv, &resolved_inputs)?;
207
208                        // Assemble a mapping table from needle back to store path, as well as a list of all outputs.
209                        // The latter is a subset of the former.
210                        // We need this to understand the response later.
211                        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                        // create a build
234                        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 each output, insert a PathInfo.
244                        for (output, output_path) in
245                            build_result.outputs.into_iter().zip(output_paths)
246                        {
247                            // calculate the nar representation
248                            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                            // assemble the PathInfo to persist
255                            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                                    // Map each output needle index back into a store path.
263                                    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                                    // Produce references sorted by name for consistency with nix narinfos
272                                    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 derivations only have one output, so this only runs once.
291                                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        // now with the root_node and sub_path, descend to the node requested.
311        // We convert sub_path to the castore model here.
312        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
326/// Helper function peeking at a [snix_castore::Node] and returning its [FileType]
327fn 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                // As snix-store doesn't manage /nix/store on the filesystem,
347                // we still need to also ask self.std_io here.
348                self.std_io.path_exists(path)
349            }
350        } else {
351            // The store path is no store path, so do regular StdIO.
352            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                // depending on the node type, treat open differently
364                match path_info.node {
365                    Node::Directory { .. } => {
366                        // This would normally be a io::ErrorKind::IsADirectory (still unstable)
367                        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                                    // The VM Response needs a sync [std::io::Reader].
378                                    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                // As snix-store doesn't manage /nix/store on the filesystem,
401                // we still need to also ask self.std_io here.
402                self.std_io.open(path)
403            }
404        } else {
405            // The store path is no store path, so do regular StdIO.
406            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                        // fetch the Directory itself.
436                        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                                // If we didn't get the directory node that's linked, that's a store inconsistency!
442                                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                        // construct children from nodes
454                        Ok(directory
455                            .into_nodes()
456                            .map(|(name, node)| (name.into(), node_get_type(&node)))
457                            .collect())
458                    }
459                    Node::File { .. } => {
460                        // This would normally be a io::ErrorKind::NotADirectory (still unstable)
461                        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        // From the returned PathInfo, extract the store path and return it.
493        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    /// evaluates a given nix expression and returns the result.
521    /// Takes care of setting up the evaluator so it knows about the
522    // `derivation` builtin.
523    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        // run the evaluation itself.
551        eval.evaluate(str, None)
552    }
553
554    /// Helper function that takes a &Path, and invokes a snix evaluator coercing that path to a string
555    /// (via "${/this/path}"). The path can be both absolute or not.
556    /// It returns Option<String>, depending on whether the evaluation succeeded or not.
557    fn import_path_and_compare<P: AsRef<Path>>(p: P) -> Option<String> {
558        // Try to import the path using "${/tmp/path/to/test}".
559        // The format string looks funny, the {} passed to Nix needs to be
560        // escaped.
561        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    /// Import a directory with a zero-sized ".keep" regular file.
576    /// Ensure it matches the (pre-recorded) store path that Nix would produce.
577    #[test]
578    fn import_directory() {
579        let tmpdir = TempDir::new().unwrap();
580
581        // create a directory named "test"
582        let src_path = tmpdir.path().join("test");
583        std::fs::create_dir(&src_path).unwrap();
584
585        // write a regular file `.keep`.
586        std::fs::write(src_path.join(".keep"), vec![]).unwrap();
587
588        // importing the path with .../test at the end.
589        assert_eq!(
590            Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
591            import_path_and_compare(&src_path)
592        );
593
594        // importing the path with .../test/. at the end.
595        assert_eq!(
596            Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
597            import_path_and_compare(src_path.join("."))
598        );
599    }
600
601    /// Import a file into the store. Nix uses the "recursive"/NAR-based hashing
602    /// scheme for these.
603    #[test]
604    fn import_file() {
605        let tmpdir = TempDir::new().unwrap();
606
607        // write a regular file `empty`.
608        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        // write a regular file `hello.txt`.
616        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    /// Invoke toString on a nonexisting file, and access the .file attribute.
625    /// This should not cause an error, because it shouldn't trigger an import,
626    /// and leave the path as-is.
627    #[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}