tvix_eval/
nix_search_path.rs1use path_clean::PathClean;
2use std::convert::Infallible;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5
6use crate::errors::{CatchableErrorKind, ErrorKind};
7use crate::EvalIO;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum NixSearchPathEntry {
11 Path(PathBuf),
27
28 Prefix { prefix: PathBuf, path: PathBuf },
44}
45
46fn canonicalise(path: PathBuf) -> Result<PathBuf, ErrorKind> {
47 let absolute = if path.is_absolute() {
48 path
49 } else {
50 std::env::current_dir()
52 .map_err(|e| ErrorKind::IO {
53 path: Some(path.clone()),
54 error: e.into(),
55 })?
56 .join(path)
57 }
58 .clean();
59
60 Ok(absolute)
61}
62
63impl NixSearchPathEntry {
64 fn resolve<IO>(&self, io: IO, lookup_path: &Path) -> Result<Option<PathBuf>, ErrorKind>
72 where
73 IO: AsRef<dyn EvalIO>,
74 {
75 let path = match self {
76 NixSearchPathEntry::Path(parent) => canonicalise(parent.join(lookup_path))?,
77
78 NixSearchPathEntry::Prefix { prefix, path } => {
79 if let Ok(child_path) = lookup_path.strip_prefix(prefix) {
80 canonicalise(path.join(child_path))?
81 } else {
82 return Ok(None);
83 }
84 }
85 };
86
87 if io.as_ref().path_exists(&path).map_err(|e| ErrorKind::IO {
88 path: Some(path.clone()),
89 error: e.into(),
90 })? {
91 Ok(Some(path))
92 } else {
93 Ok(None)
94 }
95 }
96
97 pub fn get_path(&self) -> &Path {
98 match self {
99 Self::Prefix { path, .. } => path,
100 Self::Path(path) => path,
101 }
102 }
103}
104
105impl FromStr for NixSearchPathEntry {
106 type Err = Infallible;
107
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 match s.split_once('=') {
110 Some((prefix, path)) => Ok(Self::Prefix {
111 prefix: prefix.into(),
112 path: path.into(),
113 }),
114 None => Ok(Self::Path(s.into())),
115 }
116 }
117}
118
119#[derive(Default, Debug, Clone, PartialEq, Eq)]
126pub struct NixSearchPath {
127 entries: Vec<NixSearchPathEntry>,
128}
129
130impl NixSearchPath {
131 pub fn resolve<P, IO>(
134 &self,
135 io: IO,
136 path: P,
137 ) -> Result<Result<PathBuf, CatchableErrorKind>, ErrorKind>
138 where
139 P: AsRef<Path>,
140 IO: AsRef<dyn EvalIO>,
141 {
142 let path = path.as_ref();
143 for entry in &self.entries {
144 if let Some(p) = entry.resolve(&io, path)? {
145 return Ok(Ok(p));
146 }
147 }
148 Ok(Err(CatchableErrorKind::NixPathResolution(
149 format!(
150 "path '{}' was not found in the Nix search path",
151 path.display()
152 )
153 .into_boxed_str(),
154 )))
155 }
156
157 pub fn get_entries(&self) -> &[NixSearchPathEntry] {
158 &self.entries
159 }
160}
161
162impl FromStr for NixSearchPath {
163 type Err = Infallible;
164
165 fn from_str(s: &str) -> Result<Self, Self::Err> {
166 let entries = s
167 .split(':')
168 .map(|s| s.parse())
169 .collect::<Result<Vec<_>, _>>()?;
170 Ok(NixSearchPath { entries })
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use std::rc::Rc;
178
179 mod parse {
180 use super::*;
181
182 #[test]
183 fn bare_paths() {
184 assert_eq!(
185 NixSearchPath::from_str("/foo/bar:/baz").unwrap(),
186 NixSearchPath {
187 entries: vec![
188 NixSearchPathEntry::Path("/foo/bar".into()),
189 NixSearchPathEntry::Path("/baz".into())
190 ],
191 }
192 );
193 }
194
195 #[test]
196 fn mixed_prefix_and_paths() {
197 assert_eq!(
198 NixSearchPath::from_str("nixpkgs=/my/nixpkgs:/etc/nixos").unwrap(),
199 NixSearchPath {
200 entries: vec![
201 NixSearchPathEntry::Prefix {
202 prefix: "nixpkgs".into(),
203 path: "/my/nixpkgs".into()
204 },
205 NixSearchPathEntry::Path("/etc/nixos".into())
206 ],
207 }
208 );
209 }
210 }
211
212 #[cfg(feature = "impure")]
214 mod resolve {
215 use crate::StdIO;
216 use path_clean::PathClean;
217 use std::env::current_dir;
218
219 use super::*;
220
221 #[test]
222 fn simple_dir() {
223 let nix_search_path = NixSearchPath::from_str("./.").unwrap();
224 let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
225 let res = nix_search_path.resolve(&io, "src").unwrap();
226 assert_eq!(
227 res.unwrap().to_path_buf(),
228 current_dir().unwrap().join("src").clean()
229 );
230 }
231
232 #[test]
233 fn failed_resolution() {
234 let nix_search_path = NixSearchPath::from_str("./.").unwrap();
235 let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
236 let err = nix_search_path.resolve(&io, "nope").unwrap();
237 assert!(
238 matches!(err, Err(CatchableErrorKind::NixPathResolution(..))),
239 "err = {err:?}"
240 );
241 }
242
243 #[test]
244 fn second_in_path() {
245 let nix_search_path = NixSearchPath::from_str("./.:/").unwrap();
246 let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
247 let res = nix_search_path.resolve(&io, "etc").unwrap();
248 assert_eq!(res.unwrap().to_path_buf(), Path::new("/etc"));
249 }
250
251 #[test]
252 fn prefix() {
253 let nix_search_path = NixSearchPath::from_str("/:tvix=.").unwrap();
254 let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
255 let res = nix_search_path.resolve(&io, "tvix/src").unwrap();
256 assert_eq!(
257 res.unwrap().to_path_buf(),
258 current_dir().unwrap().join("src").clean()
259 );
260 }
261
262 #[test]
263 fn matching_prefix() {
264 let nix_search_path = NixSearchPath::from_str("/:tvix=.").unwrap();
265 let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
266 let res = nix_search_path.resolve(&io, "tvix").unwrap();
267 assert_eq!(res.unwrap().to_path_buf(), current_dir().unwrap().clean());
268 }
269 }
270}