spin_common/
paths.rs

1//! Resolves a file path to a manifest file
2
3use anyhow::{anyhow, Context, Result};
4use std::path::{Path, PathBuf};
5
6use crate::ui::quoted_path;
7
8/// The name given to the default manifest file.
9pub const DEFAULT_MANIFEST_FILE: &str = "spin.toml";
10
11/// Attempts to find a manifest. If a path is provided, that path is resolved
12/// using `resolve_manifest_file_path`; otherwise, a directory search is carried out
13/// using `search_upwards_for_manifest`. If we had to search, and a manifest is found,
14/// a (non-zero) usize is returned indicating how far above the current directory it
15/// was found. (A usize of 0 indicates that the manifest was provided or found
16/// in the current directory.) This can be used to notify the user that a
17/// non-default manifest is being used.
18pub fn find_manifest_file_path(
19    provided_path: Option<impl AsRef<Path>>,
20) -> Result<(PathBuf, usize)> {
21    match provided_path {
22        Some(provided_path) => resolve_manifest_file_path(provided_path).map(|p| (p, 0)),
23        None => search_upwards_for_manifest()
24            .ok_or_else(|| anyhow!("\"{}\" not found", DEFAULT_MANIFEST_FILE)),
25    }
26}
27
28/// Resolves a manifest path provided by a user, which may be a file or
29/// directory, to a path to a manifest file.
30pub fn resolve_manifest_file_path(provided_path: impl AsRef<Path>) -> Result<PathBuf> {
31    let path = provided_path.as_ref();
32
33    if path.is_file() {
34        Ok(path.to_owned())
35    } else if path.is_dir() {
36        let file_path = path.join(DEFAULT_MANIFEST_FILE);
37        if file_path.is_file() {
38            Ok(file_path)
39        } else {
40            Err(anyhow!(
41                "Directory {} does not contain a file named 'spin.toml'",
42                path.display()
43            ))
44        }
45    } else {
46        let pd = path.display();
47        let err = match path.try_exists() {
48            Err(e) => anyhow!("Error accessing path {pd}: {e:#}"),
49            Ok(false) => anyhow!("No such file or directory '{pd}'"),
50            Ok(true) => anyhow!("Path {pd} is neither a file nor a directory"),
51        };
52        Err(err)
53    }
54}
55
56/// Starting from the current directory, searches upward through
57/// the directory tree for a manifest (that is, a file with the default
58/// manifest name `spin.toml`). If found, the path to the manifest
59/// is returned, with a usize indicating how far above the current directory it
60/// was found. (A usize of 0 indicates that the manifest was provided or found
61/// in the current directory.) This can be used to notify the user that a
62/// non-default manifest is being used.
63/// If no matching file is found, the function returns None.
64///
65/// The search is abandoned if it reaches the root directory, or the
66/// root of a Git repository, without finding a 'spin.toml'.
67pub fn search_upwards_for_manifest() -> Option<(PathBuf, usize)> {
68    let candidate = PathBuf::from(DEFAULT_MANIFEST_FILE);
69
70    if candidate.is_file() {
71        return Some((candidate, 0));
72    }
73
74    for distance in 1..20 {
75        let inferred_dir = PathBuf::from("../".repeat(distance));
76        if !inferred_dir.is_dir() {
77            return None;
78        }
79
80        let candidate = inferred_dir.join(DEFAULT_MANIFEST_FILE);
81        if candidate.is_file() {
82            return Some((candidate, distance));
83        }
84
85        if is_git_root(&inferred_dir) {
86            return None;
87        }
88    }
89
90    None
91}
92
93/// Resolves the parent directory of a path, returning an error if the path
94/// has no parent. A path with a single component will return ".".
95pub fn parent_dir(path: impl AsRef<Path>) -> Result<PathBuf> {
96    let path = path.as_ref();
97    let mut parent = path
98        .parent()
99        .with_context(|| format!("No parent directory for path {}", quoted_path(path)))?;
100    if parent == Path::new("") {
101        parent = Path::new(".");
102    }
103    Ok(parent.into())
104}
105
106fn is_git_root(dir: &Path) -> bool {
107    dir.join(".git").is_dir()
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn parent_returns_parent() {
116        assert_eq!(parent_dir("foo/bar").unwrap(), Path::new("foo"));
117    }
118
119    #[test]
120    fn blank_parent_returns_dot() {
121        assert_eq!(parent_dir("foo").unwrap(), Path::new("."));
122    }
123
124    #[test]
125    fn no_parent_returns_err() {
126        parent_dir("").unwrap_err();
127    }
128}