tvl-depot/tvix/eval/src/nix_search_path.rs
Florian Klink 6fb542aae6 fix(tvix/eval/nix_search_path): gate tests on impure feature
These use StdIO, which is only available if the impure feature is
enabled.

Change-Id: I18d8e191a7eba6ba5bd59f43631973eaa796c7bb
Reviewed-on: https://cl.tvl.fyi/c/depot/+/11721
Tested-by: BuildkiteCI
Autosubmit: flokli <flokli@flokli.de>
Reviewed-by: tazjin <tazjin@tvl.su>
2024-05-26 19:53:51 +00:00

258 lines
7.9 KiB
Rust

use path_clean::PathClean;
use std::convert::Infallible;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use crate::errors::{CatchableErrorKind, ErrorKind};
use crate::EvalIO;
#[derive(Debug, Clone, PartialEq, Eq)]
enum NixSearchPathEntry {
/// Resolve subdirectories of this path within `<...>` brackets. This
/// corresponds to bare paths within the `NIX_PATH` environment variable
///
/// For example, with `NixSearchPathEntry::Path("/example")` and the following
/// directory structure:
///
/// ```notrust
/// example
/// └── subdir
/// └── grandchild
/// ```
///
/// A Nix path literal `<subdir>` would resolve to `/example/subdir`, and a
/// Nix path literal `<subdir/grandchild>` would resolve to
/// `/example/subdir/grandchild`
Path(PathBuf),
/// Resolve paths starting with `prefix` as subdirectories of `path`. This
/// corresponds to `prefix=path` within the `NIX_PATH` environment variable.
///
/// For example, with `NixSearchPathEntry::Prefix { prefix: "prefix", path:
/// "/example" }` and the following directory structure:
///
/// ```notrust
/// example
/// └── subdir
/// └── grandchild
/// ```
///
/// A Nix path literal `<prefix/subdir>` would resolve to `/example/subdir`,
/// and a Nix path literal `<prefix/subdir/grandchild>` would resolve to
/// `/example/subdir/grandchild`
Prefix { prefix: PathBuf, path: PathBuf },
}
fn canonicalise(path: PathBuf) -> Result<PathBuf, ErrorKind> {
let absolute = if path.is_absolute() {
path
} else {
// TODO(tazjin): probably panics in wasm?
std::env::current_dir()
.map_err(|e| ErrorKind::IO {
path: Some(path.clone()),
error: e.into(),
})?
.join(path)
}
.clean();
Ok(absolute)
}
impl NixSearchPathEntry {
/// Determine whether this path entry matches the given lookup path.
///
/// For bare paths, an entry is considered to match if a matching
/// file exists under it.
///
/// For prefixed path, an entry matches if the prefix does.
// TODO(tazjin): verify these rules in the C++ impl, seems fishy.
fn resolve<IO>(&self, io: IO, lookup_path: &Path) -> Result<Option<PathBuf>, ErrorKind>
where
IO: AsRef<dyn EvalIO>,
{
let path = match self {
NixSearchPathEntry::Path(parent) => canonicalise(parent.join(lookup_path))?,
NixSearchPathEntry::Prefix { prefix, path } => {
if let Ok(child_path) = lookup_path.strip_prefix(prefix) {
canonicalise(path.join(child_path))?
} else {
return Ok(None);
}
}
};
if io.as_ref().path_exists(&path).map_err(|e| ErrorKind::IO {
path: Some(path.clone()),
error: e.into(),
})? {
Ok(Some(path))
} else {
Ok(None)
}
}
}
impl FromStr for NixSearchPathEntry {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('=') {
Some((prefix, path)) => Ok(Self::Prefix {
prefix: prefix.into(),
path: path.into(),
}),
None => Ok(Self::Path(s.into())),
}
}
}
/// Struct implementing the format and path resolution rules of the `NIX_PATH`
/// environment variable.
///
/// This struct can be constructed by parsing a string using the [`FromStr`]
/// impl, or via [`str::parse`]. Nix `<...>` paths can then be resolved using
/// [`NixSearchPath::resolve`].
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct NixSearchPath {
entries: Vec<NixSearchPathEntry>,
}
impl NixSearchPath {
/// Attempt to resolve the given `path` within this [`NixSearchPath`] using the
/// path resolution rules for `<...>`-style paths
pub fn resolve<P, IO>(
&self,
io: IO,
path: P,
) -> Result<Result<PathBuf, CatchableErrorKind>, ErrorKind>
where
P: AsRef<Path>,
IO: AsRef<dyn EvalIO>,
{
let path = path.as_ref();
for entry in &self.entries {
if let Some(p) = entry.resolve(&io, path)? {
return Ok(Ok(p));
}
}
Ok(Err(CatchableErrorKind::NixPathResolution(
format!(
"path '{}' was not found in the Nix search path",
path.display()
)
.into_boxed_str(),
)))
}
}
impl FromStr for NixSearchPath {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let entries = s
.split(':')
.map(|s| s.parse())
.collect::<Result<Vec<_>, _>>()?;
Ok(NixSearchPath { entries })
}
}
#[cfg(test)]
mod tests {
use super::*;
mod parse {
use super::*;
#[test]
fn bare_paths() {
assert_eq!(
NixSearchPath::from_str("/foo/bar:/baz").unwrap(),
NixSearchPath {
entries: vec![
NixSearchPathEntry::Path("/foo/bar".into()),
NixSearchPathEntry::Path("/baz".into())
],
}
);
}
#[test]
fn mixed_prefix_and_paths() {
assert_eq!(
NixSearchPath::from_str("nixpkgs=/my/nixpkgs:/etc/nixos").unwrap(),
NixSearchPath {
entries: vec![
NixSearchPathEntry::Prefix {
prefix: "nixpkgs".into(),
path: "/my/nixpkgs".into()
},
NixSearchPathEntry::Path("/etc/nixos".into())
],
}
);
}
}
// this uses StdIO, which is only available with the impure feature.
#[cfg(feature = "impure")]
mod resolve {
use crate::StdIO;
use path_clean::PathClean;
use std::env::current_dir;
use super::*;
#[test]
fn simple_dir() {
let nix_search_path = NixSearchPath::from_str("./.").unwrap();
let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
let res = nix_search_path.resolve(&io, "src").unwrap();
assert_eq!(
res.unwrap().to_path_buf(),
current_dir().unwrap().join("src").clean()
);
}
#[test]
fn failed_resolution() {
let nix_search_path = NixSearchPath::from_str("./.").unwrap();
let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
let err = nix_search_path.resolve(&io, "nope").unwrap();
assert!(
matches!(err, Err(CatchableErrorKind::NixPathResolution(..))),
"err = {err:?}"
);
}
#[test]
fn second_in_path() {
let nix_search_path = NixSearchPath::from_str("./.:/").unwrap();
let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
let res = nix_search_path.resolve(&io, "etc").unwrap();
assert_eq!(res.unwrap().to_path_buf(), Path::new("/etc"));
}
#[test]
fn prefix() {
let nix_search_path = NixSearchPath::from_str("/:tvix=.").unwrap();
let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
let res = nix_search_path.resolve(&io, "tvix/src").unwrap();
assert_eq!(
res.unwrap().to_path_buf(),
current_dir().unwrap().join("src").clean()
);
}
#[test]
fn matching_prefix() {
let nix_search_path = NixSearchPath::from_str("/:tvix=.").unwrap();
let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
let res = nix_search_path.resolve(&io, "tvix").unwrap();
assert_eq!(res.unwrap().to_path_buf(), current_dir().unwrap().clean());
}
}
}