Skip to main content

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    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
31/// Implements [EvalIO], asking given [PathInfoService], [DirectoryService]
32/// and [BlobService].
33///
34/// In case the given path does not exist in these stores, we ask StdIO.
35/// This is to both cover cases of syntactically valid store paths, that exist
36/// on the filesystem (still managed by Nix), as well as being able to read
37/// files outside store paths.
38///
39/// This structure is also directly used by the derivation builtins
40/// and tightly coupled to it.
41///
42/// In the future, we may revisit that coupling and figure out how to generalize this interface and
43/// hide this implementation detail of the glue itself so that glue can be used with more than one
44/// implementation of "Snix Store IO" which does not necessarily bring the concept of blob service,
45/// directory service or path info service.
46pub struct SnixStoreIO {
47    // This is public so helper functions can interact with the stores directly.
48    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    // Paths known how to produce, by building or fetching.
67    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    /// for a given [StorePath] and additional [snix_castore::Path] inside the store path,
100    /// look up the [PathInfo], and if it exists, and then uses
101    /// [descend_to] to return the [Node] specified by `sub_path`.
102    ///
103    /// In case there is no PathInfo yet, we check "self.known_paths" (learnt by
104    /// the evaluator)
105    ///  - If there's a fetch, we do that and return the resulting PathInfo.
106    ///  - If there's a Derivation, we build it and return the resulting
107    ///    PathInfo.
108    ///    To build it, we need to do a function call to ourselves with all
109    ///    inputs of that Derivation, which will trigger fetches / builds of
110    ///    inputs, recursively.
111    ///
112    /// This should be replaced with a proper scheduler knowing about a partial
113    /// subgraph at some point, because this design doesn't allow concurrent
114    /// builds yet.
115    #[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        // Find the root node for the store_path.
125        // It asks the PathInfoService first, but in case there was a Derivation
126        // produced that would build it, fall back to triggering the build.
127        // To populate the input nodes, it might recursively trigger builds of
128        // its dependencies too.
129        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            // If there's no PathInfo found, this normally means we have to
138            // trigger the build (and insert into PathInfoService, after
139            // reference scanning).
140            // However, as Snix is (currently) not managing /nix/store itself,
141            // we return Ok(None) to let std_io take over.
142            // While reading from store paths that are not known to Snix during
143            // that evaluation clearly is an impurity, we still need to support
144            // it for things like <nixpkgs> pointing to a store path.
145            // In the future, these things will (need to) have PathInfo.
146            None => {
147                // The store path doesn't exist yet, so we need to fetch or build it.
148                // We check for fetches first, as we might have both native
149                // fetchers and FODs in KnownPaths, and prefer the former.
150                // This will also find [Fetch] synthesized from
151                // `builtin:fetchurl` Derivations.
152                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                        // Look up the derivation for this output path.
177                        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                                    // let StdIO take over
187                                    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                        // derivation_to_build_request needs castore nodes for all inputs.
197                        // Provide them, which means, here is where we recursively build
198                        // all dependencies.
199                        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                        // precompute the `ca` field in the PathInfo, so we can send off drv.
212                        let mut ca = drv.fod_digest().map(|fod_digest| {
213                            CAHash::Nar(nix_compat::nixhash::NixHash::Sha256(fod_digest))
214                        });
215
216                        // synthesize the build request.
217                        let build_request =
218                            builder::derivation_into_build_request(drv, &resolved_inputs)?;
219
220                        // Assemble a mapping table from needle back to store path, as well as a list of all outputs.
221                        // The latter is a subset of the former.
222                        // We need this to understand the response later.
223                        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                        // create a build
246                        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 each output, insert a PathInfo.
256                        for (output, output_path) in
257                            build_result.outputs.into_iter().zip(output_paths)
258                        {
259                            // calculate the nar representation
260                            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                            // assemble the PathInfo to persist
267                            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                                    // Map each output needle index back into a store path.
275                                    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                                    // Produce references sorted by name for consistency with nix narinfos
284                                    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 derivations only have one output, so this only runs once.
303                                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        // now with the root_node and sub_path, descend to the node requested.
323        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
335/// Helper function peeking at a [snix_castore::Node] and returning its [FileType]
336fn 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// Helper function converting a [std::path::Path] to a [StorePath] and [snix_castore::Path].
345#[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                // As snix-store doesn't manage /nix/store on the filesystem,
371                // we still need to also ask self.std_io here.
372                self.std_io.path_exists(path)
373            }
374        } else {
375            // The store path is no store path, so do regular StdIO.
376            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                // depending on the node type, treat open differently
388                match path_info.node {
389                    Node::Directory { .. } => {
390                        // This would normally be a io::ErrorKind::IsADirectory (still unstable)
391                        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                                    // The VM Response needs a sync [std::io::Reader].
402                                    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                // As snix-store doesn't manage /nix/store on the filesystem,
425                // we still need to also ask self.std_io here.
426                self.std_io.open(path)
427            }
428        } else {
429            // The store path is no store path, so do regular StdIO.
430            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                        // fetch the Directory itself.
460                        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                                // If we didn't get the directory node that's linked, that's a store inconsistency!
466                                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                        // construct children from nodes
478                        Ok(directory
479                            .into_nodes()
480                            .map(|(name, node)| (name.into(), node_get_type(&node)))
481                            .collect())
482                    }
483                    Node::File { .. } => {
484                        // This would normally be a io::ErrorKind::NotADirectory (still unstable)
485                        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        // From the returned PathInfo, extract the store path and return it.
525        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    /// evaluates a given nix expression and returns the result.
553    /// Takes care of setting up the evaluator so it knows about the
554    // `derivation` builtin.
555    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        // run the evaluation itself.
583        eval.evaluate(str, None)
584    }
585
586    /// Helper function that takes a &Path, and invokes a snix evaluator coercing that path to a string
587    /// (via "${/this/path}"). The path can be both absolute or not.
588    /// It returns Option<String>, depending on whether the evaluation succeeded or not.
589    fn import_path_and_compare<P: AsRef<Path>>(p: P) -> Option<String> {
590        // Try to import the path using "${/tmp/path/to/test}".
591        // The format string looks funny, the {} passed to Nix needs to be
592        // escaped.
593        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    /// Import a directory with a zero-sized ".keep" regular file.
608    /// Ensure it matches the (pre-recorded) store path that Nix would produce.
609    #[test]
610    fn import_directory() {
611        let tmpdir = TempDir::new().unwrap();
612
613        // create a directory named "test"
614        let src_path = tmpdir.path().join("test");
615        std::fs::create_dir(&src_path).unwrap();
616
617        // write a regular file `.keep`.
618        std::fs::write(src_path.join(".keep"), vec![]).unwrap();
619
620        // importing the path with .../test at the end.
621        assert_eq!(
622            Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
623            import_path_and_compare(&src_path)
624        );
625
626        // importing the path with .../test/. at the end.
627        assert_eq!(
628            Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
629            import_path_and_compare(src_path.join("."))
630        );
631    }
632
633    /// Import a file into the store. Nix uses the "recursive"/NAR-based hashing
634    /// scheme for these.
635    #[test]
636    fn import_file() {
637        let tmpdir = TempDir::new().unwrap();
638
639        // write a regular file `empty`.
640        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        // write a regular file `hello.txt`.
648        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    /// Invoke toString on a nonexisting file, and access the .file attribute.
657    /// This should not cause an error, because it shouldn't trigger an import,
658    /// and leave the path as-is.
659    #[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}