1use nix_compat::store_path::StorePathRef;
3use snix_build::buildservice::BuildService;
4use snix_build_glue::build_state::BuildState;
5use snix_eval::{EvalIO, FileType, StdIO};
6use snix_store::nar::NarCalculationService;
7use std::{
8 env,
9 ffi::{OsStr, OsString},
10 io,
11 sync::Arc,
12};
13use tokio_util::io::SyncIoBridge;
14use tracing::{Level, error, instrument};
15use url::Url;
16
17use snix_castore::{Node, blobservice::BlobService, directoryservice::DirectoryService};
18use snix_store::pathinfoservice::{PathInfo, PathInfoService};
19
20pub struct SnixStoreIO {
36 pub build_state: BuildState,
37
38 std_io: StdIO,
39 pub(crate) tokio_handle: tokio::runtime::Handle,
40}
41
42impl SnixStoreIO {
43 pub fn new(
44 blob_service: Arc<dyn BlobService>,
45 directory_service: Arc<dyn DirectoryService>,
46 path_info_service: Arc<dyn PathInfoService>,
47 nar_calculation_service: Arc<dyn NarCalculationService>,
48 build_service: Arc<dyn BuildService>,
49 tokio_handle: tokio::runtime::Handle,
50 hashed_mirrors: Vec<Url>,
51 ) -> Self {
52 Self {
53 build_state: BuildState::new(
54 blob_service,
55 directory_service,
56 path_info_service,
57 nar_calculation_service,
58 build_service,
59 hashed_mirrors,
60 ),
61 std_io: StdIO {},
62 tokio_handle,
63 }
64 }
65
66 #[instrument(skip(self, store_path), fields(store_path=%store_path, indicatif.pb_show=tracing::field::Empty), ret(level = Level::TRACE), err(level = Level::TRACE))]
86 async fn store_path_to_path_info(
87 &self,
88 store_path: &StorePathRef<'_>,
89 sub_path: &snix_castore::Path,
90 ) -> io::Result<Option<PathInfo>> {
91 self.build_state
92 .store_path_to_path_info(store_path, sub_path)
93 .await
94 }
95}
96
97fn node_get_type(node: &Node) -> FileType {
99 match node {
100 Node::Directory { .. } => FileType::Directory,
101 Node::File { .. } => FileType::Regular,
102 Node::Symlink { .. } => FileType::Symlink,
103 }
104}
105
106#[cfg(unix)]
108fn parse_store_and_sub_path<'a>(
109 path: &'a std::path::Path,
110) -> io::Result<(StorePathRef<'a>, &'a snix_castore::Path)> {
111 let (store_path, rest) =
112 StorePathRef::from_absolute_path_full(path).map_err(std::io::Error::other)?;
113
114 use std::os::unix::ffi::OsStrExt;
115 let sub_path = snix_castore::Path::from_bytes(rest.as_os_str().as_bytes())
116 .ok_or_else(|| std::io::Error::other("sub_path is no valid path"))?;
117
118 Ok((store_path, sub_path))
119}
120
121impl EvalIO for SnixStoreIO {
122 #[instrument(skip(self), ret(level = Level::TRACE), err)]
123 fn path_exists(&self, path: &std::path::Path) -> io::Result<bool> {
124 if let Ok((store_path, sub_path)) = parse_store_and_sub_path(path) {
125 if self
126 .tokio_handle
127 .block_on(self.store_path_to_path_info(&store_path, sub_path))?
128 .is_some()
129 {
130 Ok(true)
131 } else {
132 self.std_io.path_exists(path)
135 }
136 } else {
137 self.std_io.path_exists(path)
139 }
140 }
141
142 #[instrument(skip(self), err)]
143 fn open(&self, path: &std::path::Path) -> io::Result<Box<dyn io::Read>> {
144 if let Ok((store_path, sub_path)) = parse_store_and_sub_path(path) {
145 self.tokio_handle.block_on(async {
146 if let Some(path_info) = self.store_path_to_path_info(&store_path, sub_path).await?
147 {
148 match path_info.node {
150 Node::Directory { .. } => {
151 Err(io::Error::new(
153 io::ErrorKind::Unsupported,
154 format!("tried to open directory at {path:?}"),
155 ))
156 }
157 Node::File { digest, .. } => {
158 let resp = self
159 .build_state
160 .blob_service
161 .as_ref()
162 .open_read(&digest)
163 .await?;
164 match resp {
165 Some(blob_reader) => {
166 Ok(Box::new(SyncIoBridge::new(blob_reader))
168 as Box<dyn io::Read>)
169 }
170 None => {
171 error!(
172 blob.digest = %digest,
173 "blob not found",
174 );
175 Err(io::Error::new(
176 io::ErrorKind::NotFound,
177 format!("blob {} not found", &digest),
178 ))
179 }
180 }
181 }
182 Node::Symlink { .. } => Err(io::Error::new(
183 io::ErrorKind::Unsupported,
184 "open for symlinks is unsupported",
185 ))?,
186 }
187 } else {
188 self.std_io.open(path)
191 }
192 })
193 } else {
194 self.std_io.open(path)
196 }
197 }
198
199 #[instrument(skip(self), ret(level = Level::TRACE), err)]
200 fn file_type(&self, path: &std::path::Path) -> io::Result<FileType> {
201 if let Ok((store_path, sub_path)) = parse_store_and_sub_path(path) {
202 if let Some(path_info) = self
203 .tokio_handle
204 .block_on(async { self.store_path_to_path_info(&store_path, sub_path).await })?
205 {
206 Ok(node_get_type(&path_info.node))
207 } else {
208 self.std_io.file_type(path)
209 }
210 } else {
211 self.std_io.file_type(path)
212 }
213 }
214
215 #[instrument(skip(self), ret(level = Level::TRACE), err)]
216 fn read_dir(&self, path: &std::path::Path) -> io::Result<Vec<(bytes::Bytes, FileType)>> {
217 if let Ok((store_path, sub_path)) = parse_store_and_sub_path(path) {
218 self.tokio_handle.block_on(async {
219 if let Some(path_info) = self.store_path_to_path_info(&store_path, sub_path).await?
220 {
221 match path_info.node {
222 Node::Directory { digest, .. } => {
223 let directory = self
225 .build_state
226 .directory_service
227 .as_ref()
228 .get(&digest)
229 .await
230 .map_err(std::io::Error::other)?
231 .ok_or_else(|| {
232 error!(
234 directory.digest = %digest,
235 path = ?path,
236 "directory not found",
237 );
238 io::Error::new(
239 io::ErrorKind::NotFound,
240 format!("directory {digest} does not exist"),
241 )
242 })?;
243
244 Ok(directory
246 .into_nodes()
247 .map(|(name, node)| (name.into(), node_get_type(&node)))
248 .collect())
249 }
250 Node::File { .. } => {
251 Err(io::Error::new(
253 io::ErrorKind::Unsupported,
254 "tried to readdir path {:?}, which is a file",
255 ))?
256 }
257 Node::Symlink { .. } => Err(io::Error::new(
258 io::ErrorKind::Unsupported,
259 "read_dir for symlinks is unsupported",
260 ))?,
261 }
262 } else {
263 self.std_io.read_dir(path)
264 }
265 })
266 } else {
267 self.std_io.read_dir(path)
268 }
269 }
270
271 #[instrument(skip(self), ret(level = Level::TRACE), err)]
272 fn import_path(&self, path: &std::path::Path) -> io::Result<std::path::PathBuf> {
273 let file_name = path.file_name().ok_or_else(|| {
274 io::Error::new(
275 io::ErrorKind::InvalidFilename,
276 "path without basename encountered",
277 )
278 })?;
279 let path_info = self.tokio_handle.block_on({
280 snix_store::import::import_path_as_nar_ca(
281 path,
282 nix_compat::store_path::validate_name_from_os_str(file_name)
283 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
284 &self.build_state.blob_service,
285 &self.build_state.directory_service,
286 &self.build_state.path_info_service,
287 &self.build_state.nar_calculation_service,
288 )
289 })?;
290
291 Ok(path_info.store_path.to_absolute_path().into())
293 }
294
295 #[instrument(skip(self), ret(level = Level::TRACE))]
296 fn store_dir(&self) -> Option<String> {
297 Some("/nix/store".to_string())
298 }
299
300 fn get_env(&self, key: &OsStr) -> Option<OsString> {
301 env::var_os(key)
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use std::{path::Path, rc::Rc, sync::Arc};
308
309 use bstr::ByteSlice;
310 use clap::Parser;
311 use snix_build::buildservice::DummyBuildService;
312 use snix_eval::{EvalIO, EvaluationResult};
313 use snix_store::utils::{ServiceUrlsMemory, construct_services};
314 use tempfile::TempDir;
315
316 use super::SnixStoreIO;
317 use crate::builtins::{add_derivation_builtins, add_fetcher_builtins, add_import_builtins};
318
319 fn eval(str: &str) -> EvaluationResult {
323 let tokio_runtime = tokio::runtime::Runtime::new().unwrap();
324 let (blob_service, directory_service, path_info_service, nar_calculation_service) =
325 tokio_runtime
326 .block_on(async {
327 construct_services(ServiceUrlsMemory::parse_from(std::iter::empty::<&str>()))
328 .await
329 })
330 .unwrap();
331
332 let io = Rc::new(SnixStoreIO::new(
333 blob_service,
334 directory_service,
335 path_info_service,
336 nar_calculation_service,
337 Arc::<DummyBuildService>::default(),
338 tokio_runtime.handle().clone(),
339 Vec::new(),
340 ));
341
342 let mut eval_builder =
343 snix_eval::Evaluation::builder(io.clone() as Rc<dyn EvalIO>).enable_import();
344 eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&io));
345 eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&io));
346 eval_builder = add_import_builtins(eval_builder, io);
347 let eval = eval_builder.build();
348
349 eval.evaluate(str, None)
351 }
352
353 fn import_path_and_compare<P: AsRef<Path>>(p: P) -> Option<String> {
357 let code = format!(r#""${{{}}}""#, p.as_ref().display());
361 let result = eval(&code);
362
363 if !result.errors.is_empty() {
364 return None;
365 }
366
367 let value = result.value.expect("must be some");
368 match value {
369 snix_eval::Value::String(s) => Some(s.to_str_lossy().into_owned()),
370 _ => panic!("unexpected value type: {value:?}"),
371 }
372 }
373
374 #[test]
377 fn import_directory() {
378 let tmpdir = TempDir::new().unwrap();
379
380 let src_path = tmpdir.path().join("test");
382 std::fs::create_dir(&src_path).unwrap();
383
384 std::fs::write(src_path.join(".keep"), vec![]).unwrap();
386
387 assert_eq!(
389 Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
390 import_path_and_compare(&src_path)
391 );
392
393 assert_eq!(
395 Some("/nix/store/gq3xcv4xrj4yr64dflyr38acbibv3rm9-test".to_string()),
396 import_path_and_compare(src_path.join("."))
397 );
398 }
399
400 #[test]
403 fn import_file() {
404 let tmpdir = TempDir::new().unwrap();
405
406 std::fs::write(tmpdir.path().join("empty"), vec![]).unwrap();
408
409 assert_eq!(
410 Some("/nix/store/lx5i78a4izwk2qj1nq8rdc07y8zrwy90-empty".to_string()),
411 import_path_and_compare(tmpdir.path().join("empty"))
412 );
413
414 std::fs::write(tmpdir.path().join("hello.txt"), b"Hello World!").unwrap();
416
417 assert_eq!(
418 Some("/nix/store/925f1jb1ajrypjbyq7rylwryqwizvhp0-hello.txt".to_string()),
419 import_path_and_compare(tmpdir.path().join("hello.txt"))
420 );
421 }
422
423 #[test]
427 fn nonexisting_path_without_import() {
428 let result = eval("toString ({ line = 42; col = 42; file = /deep/thought; }.file)");
429
430 assert!(result.errors.is_empty(), "expect evaluation to succeed");
431 let value = result.value.expect("must be some");
432
433 match value {
434 snix_eval::Value::String(s) => {
435 assert_eq!(*s, "/deep/thought");
436 }
437 _ => panic!("unexpected value type: {value:?}"),
438 }
439 }
440}