tvix_glue/
tvix_store_io.rs

1//! This module provides an implementation of EvalIO talking to tvix-store.
2use std::{
3    cell::RefCell,
4    io,
5    path::{Path, PathBuf},
6};
7use tvix_eval::{EvalIO, FileType};
8use tvix_simstore::SimulatedStoreIO;
9
10// use crate::fetchers::Fetcher;
11use crate::known_paths::KnownPaths;
12
13/// Implements [EvalIO], asking given [PathInfoService], [DirectoryService]
14/// and [BlobService].
15///
16/// In case the given path does not exist in these stores, we ask StdIO.
17/// This is to both cover cases of syntactically valid store paths, that exist
18/// on the filesystem (still managed by Nix), as well as being able to read
19/// files outside store paths.
20///
21/// This structure is also directly used by the derivation builtins
22/// and tightly coupled to it.
23///
24/// In the future, we may revisit that coupling and figure out how to generalize this interface and
25/// hide this implementation detail of the glue itself so that glue can be used with more than one
26/// implementation of "Tvix Store IO" which does not necessarily bring the concept of blob service,
27/// directory service or path info service.
28pub struct TvixStoreIO {
29    // Field for in-progress switch to simulated store:
30    pub(crate) simulated_store: SimulatedStoreIO,
31
32    // Paths known how to produce, by building or fetching.
33    pub known_paths: RefCell<KnownPaths>,
34}
35
36impl TvixStoreIO {
37    pub fn new(simulated_store: SimulatedStoreIO) -> Self {
38        Self {
39            simulated_store,
40            known_paths: Default::default(),
41        }
42    }
43}
44
45impl EvalIO for TvixStoreIO {
46    fn path_exists(&self, path: &Path) -> io::Result<bool> {
47        self.simulated_store.path_exists(path)
48    }
49
50    fn open(&self, path: &Path) -> io::Result<Box<dyn io::Read>> {
51        self.simulated_store.open(path)
52    }
53
54    fn file_type(&self, path: &Path) -> io::Result<FileType> {
55        self.simulated_store.file_type(path)
56    }
57
58    fn read_dir(&self, path: &Path) -> io::Result<Vec<(bytes::Bytes, FileType)>> {
59        self.simulated_store.read_dir(path)
60    }
61
62    fn import_path(&self, path: &Path) -> io::Result<PathBuf> {
63        self.simulated_store.import_path(path)
64    }
65
66    fn store_dir(&self) -> Option<String> {
67        self.simulated_store.store_dir()
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use std::{path::Path, rc::Rc};
74
75    use bstr::ByteSlice;
76    use tempfile::TempDir;
77    use tvix_eval::{EvalIO, EvaluationResult};
78
79    use super::TvixStoreIO;
80    use crate::builtins::{add_derivation_builtins, add_import_builtins};
81
82    /// evaluates a given nix expression and returns the result.
83    /// Takes care of setting up the evaluator so it knows about the
84    /// `derivation` builtin.
85    fn eval(str: &str) -> EvaluationResult {
86        let io = Rc::new(TvixStoreIO::new(Default::default()));
87
88        let mut eval_builder =
89            tvix_eval::Evaluation::builder(io.clone() as Rc<dyn EvalIO>).enable_import();
90        eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&io));
91        // eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&io));
92        eval_builder = add_import_builtins(eval_builder, io);
93        let eval = eval_builder.build();
94
95        // run the evaluation itself.
96        eval.evaluate(str, None)
97    }
98
99    /// Helper function that takes a &Path, and invokes a tvix evaluator coercing that path to a string
100    /// (via "${/this/path}"). The path can be both absolute or not.
101    /// It returns Option<String>, depending on whether the evaluation succeeded or not.
102    fn import_path_and_compare<P: AsRef<Path>>(p: P) -> Option<String> {
103        // Try to import the path using "${/tmp/path/to/test}".
104        // The format string looks funny, the {} passed to Nix needs to be
105        // escaped.
106        let code = format!(r#""${{{}}}""#, p.as_ref().display());
107        let result = eval(&code);
108
109        if !result.errors.is_empty() {
110            return None;
111        }
112
113        let value = result.value.expect("must be some");
114        match value {
115            tvix_eval::Value::String(s) => Some(s.to_str_lossy().into_owned()),
116            _ => panic!("unexpected value type: {value:?}"),
117        }
118    }
119
120    /// Import a directory with a zero-sized ".keep" regular file.
121    /// Ensure it matches the (pre-recorded) store path that Nix would produce.
122    #[test]
123    fn import_directory() {
124        let tmpdir = TempDir::new().unwrap();
125
126        // create a directory named "test"
127        let src_path = tmpdir.path().join("test");
128        std::fs::create_dir(&src_path).unwrap();
129
130        // write a regular file `.keep`.
131        std::fs::write(src_path.join(".keep"), vec![]).unwrap();
132
133        // importing the path with .../test at the end.
134        assert_eq!(
135            Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
136            import_path_and_compare(&src_path)
137        );
138
139        // importing the path with .../test/. at the end.
140        assert_eq!(
141            Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
142            import_path_and_compare(src_path.join("."))
143        );
144    }
145
146    /// Import a file into the store. Nix uses the "recursive"/NAR-based hashing
147    /// scheme for these.
148    #[test]
149    fn import_file() {
150        let tmpdir = TempDir::new().unwrap();
151
152        // write a regular file `empty`.
153        std::fs::write(tmpdir.path().join("empty"), vec![]).unwrap();
154
155        assert_eq!(
156            Some("/nix/store/lx5i78a4izwk2qj1nq8rdc07y8zrwy90-empty".to_string()),
157            import_path_and_compare(tmpdir.path().join("empty"))
158        );
159
160        // write a regular file `hello.txt`.
161        std::fs::write(tmpdir.path().join("hello.txt"), b"Hello World!").unwrap();
162
163        assert_eq!(
164            Some("/nix/store/925f1jb1ajrypjbyq7rylwryqwizvhp0-hello.txt".to_string()),
165            import_path_and_compare(tmpdir.path().join("hello.txt"))
166        );
167    }
168
169    /// Invoke toString on a nonexisting file, and access the .file attribute.
170    /// This should not cause an error, because it shouldn't trigger an import,
171    /// and leave the path as-is.
172    #[test]
173    fn nonexisting_path_without_import() {
174        let result = eval("toString ({ line = 42; col = 42; file = /deep/thought; }.file)");
175
176        assert!(result.errors.is_empty(), "expect evaluation to succeed");
177        let value = result.value.expect("must be some");
178
179        match value {
180            tvix_eval::Value::String(s) => {
181                assert_eq!(*s, "/deep/thought");
182            }
183            _ => panic!("unexpected value type: {value:?}"),
184        }
185    }
186}