Skip to main content

find_cargo_toml/
lib.rs

1//! Find `Cargo.toml` (or a custom manifest filename) by walking up the directory tree.
2//!
3//! Starts from a given path and yields every directory that contains the manifest file,
4//! from nearest to the root. Useful for locating workspace or package roots in Rust projects.
5
6use std::path::{Path, PathBuf};
7
8/// Iterator that walks upward from a directory and yields the full path to the manifest file
9/// whenever it exists. Yields paths from the directory nearest to the start path toward the root.
10pub struct FindIter {
11    current: Option<PathBuf>,
12    file_name: String,
13}
14
15impl Iterator for FindIter {
16    type Item = PathBuf;
17
18    fn next(&mut self) -> Option<PathBuf> {
19        while let Some(ref dir) = self.current {
20            let candidate = dir.join(&self.file_name);
21            if candidate.is_file() {
22                let result = candidate;
23                self.current = dir.parent().map(PathBuf::from);
24                return Some(result);
25            }
26            self.current = dir.parent().map(PathBuf::from);
27        }
28        None
29    }
30}
31
32/// Finds manifest files by walking up from `input`. Defaults to `Cargo.toml`.
33///
34/// # Arguments
35///
36/// * **`input`** – Where to start. Can be a directory or a file path; if a file,
37///   its parent directory is used. Relative paths are resolved against `base`.
38/// * **`base`** – Base path for resolving relative `input`. If `None`, the current
39///   working directory is used.
40/// * **`file_name`** – Name of the manifest file to look for. If `None`, `"Cargo.toml"` is used.
41///
42/// # Returns
43///
44/// A [`FindIter`] that yields the full path to each manifest file found, from the directory
45/// closest to the start path upward toward the filesystem root.
46///
47/// # Example
48///
49/// ```
50/// use std::path::PathBuf;
51/// use find_cargo_toml::find;
52///
53/// for path in find(".", None::<PathBuf>, None) {
54///     println!("Found: {}", path.display());
55/// }
56/// ```
57pub fn find<P, Q>(input: P, base: Option<Q>, file_name: Option<&str>) -> FindIter
58where
59    P: AsRef<Path>,
60    Q: AsRef<Path>,
61{
62    let file_name = file_name.unwrap_or("Cargo.toml").to_string();
63    let base: PathBuf = base
64        .map(|b| b.as_ref().to_path_buf())
65        .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
66    let start: PathBuf = if input.as_ref().is_absolute() {
67        input.as_ref().to_path_buf()
68    } else {
69        base.join(input.as_ref())
70    };
71    let start_normalized = normalize_path(&start);
72    let start_dir = if start_normalized.is_file() {
73        start_normalized
74            .parent()
75            .map(PathBuf::from)
76            .unwrap_or(start_normalized)
77    } else {
78        start_normalized
79    };
80
81    FindIter {
82        current: Some(start_dir),
83        file_name,
84    }
85}
86
87/// Convenience wrapper for [`find`] that uses the current working directory as the base.
88/// Equivalent to `find(input, None::<PathBuf>, file_name)`.
89pub fn find_from_current_dir<P>(input: P, file_name: Option<&str>) -> FindIter
90where
91    P: AsRef<Path>,
92{
93    find(input, None::<PathBuf>, file_name)
94}
95
96/// Resolves `.` and `..` in `path` and returns a normalized [`PathBuf`].
97fn normalize_path(path: &Path) -> PathBuf {
98    path.components().collect()
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::fs;
105    use std::io::Write;
106
107    #[test]
108    fn find_yields_nothing_when_no_cargo_toml() {
109        let tmp = std::env::temp_dir().join("find_cargo_toml_test_empty");
110        let _ = fs::create_dir_all(&tmp);
111        let count = find(&tmp, None::<PathBuf>, None).count();
112        let _ = fs::remove_dir_all(&tmp);
113        assert_eq!(count, 0);
114    }
115
116    #[test]
117    fn find_yields_path_when_cargo_toml_in_dir() {
118        let tmp = std::env::temp_dir().join("find_cargo_toml_test_with");
119        let _ = fs::create_dir_all(&tmp);
120        let manifest = tmp.join("Cargo.toml");
121        let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
122        let collected: Vec<_> = find(&tmp, None::<PathBuf>, None).collect();
123        let _ = fs::remove_file(manifest);
124        let _ = fs::remove_dir_all(&tmp);
125        assert_eq!(collected.len(), 1);
126        assert!(collected[0].ends_with("Cargo.toml"));
127    }
128
129    #[test]
130    fn find_respects_custom_file_name() {
131        let tmp = std::env::temp_dir().join("find_cargo_toml_test_custom");
132        let _ = fs::create_dir_all(&tmp);
133        let custom = tmp.join("MyManifest.toml");
134        let _ = fs::File::create(&custom).and_then(|mut f| f.write_all(b"[package]"));
135        let collected: Vec<_> = find(&tmp, None::<PathBuf>, Some("MyManifest.toml")).collect();
136        let _ = fs::remove_file(custom);
137        let _ = fs::remove_dir_all(&tmp);
138        assert_eq!(collected.len(), 1);
139        assert!(collected[0].ends_with("MyManifest.toml"));
140    }
141
142    #[test]
143    fn find_with_absolute_input() {
144        let tmp = std::env::temp_dir().join("find_cargo_toml_test_absolute");
145        let _ = fs::create_dir_all(&tmp);
146        let manifest = tmp.join("Cargo.toml");
147        let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
148        let abs = tmp.canonicalize().unwrap();
149        let collected: Vec<_> = find(&abs, None::<PathBuf>, None).collect();
150        let _ = fs::remove_file(manifest);
151        let _ = fs::remove_dir_all(&tmp);
152        assert_eq!(collected.len(), 1);
153        assert!(collected[0].ends_with("Cargo.toml"));
154    }
155
156    #[test]
157    fn find_when_input_is_file_uses_parent_dir() {
158        let tmp = std::env::temp_dir().join("find_cargo_toml_test_file_input");
159        let _ = fs::create_dir_all(&tmp);
160        let manifest = tmp.join("Cargo.toml");
161        let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
162        let some_file = tmp.join("foo.rs");
163        let _ = fs::File::create(&some_file);
164        let collected: Vec<_> = find(&some_file, None::<PathBuf>, None).collect();
165        let _ = fs::remove_file(some_file);
166        let _ = fs::remove_file(manifest);
167        let _ = fs::remove_dir_all(&tmp);
168        assert_eq!(collected.len(), 1);
169        assert!(collected[0].ends_with("Cargo.toml"));
170    }
171
172    #[test]
173    fn find_from_current_dir_delegates_to_find() {
174        let count = find_from_current_dir(".", None).count();
175        assert!(count >= 1, "project root has Cargo.toml");
176    }
177
178    #[test]
179    fn find_with_explicit_base() {
180        let tmp = std::env::temp_dir().join("find_cargo_toml_test_base");
181        let _ = fs::create_dir_all(&tmp);
182        let manifest = tmp.join("Cargo.toml");
183        let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
184        let collected: Vec<_> = find(".", Some(&tmp), None).collect();
185        let _ = fs::remove_file(manifest);
186        let _ = fs::remove_dir_all(&tmp);
187        assert_eq!(collected.len(), 1);
188        assert!(collected[0].ends_with("Cargo.toml"));
189    }
190
191    #[test]
192    fn find_normalizes_path_with_dot_dot() {
193        let tmp = std::env::temp_dir().join("find_cargo_toml_test_normalize");
194        let sub = tmp.join("sub");
195        let _ = fs::create_dir_all(&sub);
196        let manifest = tmp.join("Cargo.toml");
197        let _ = fs::File::create(&manifest).and_then(|mut f| f.write_all(b"[package]"));
198        let input = sub.join("..");
199        let collected: Vec<_> = find(&input, None::<PathBuf>, None).collect();
200        let _ = fs::remove_file(manifest);
201        let _ = fs::remove_dir_all(&tmp);
202        assert!(
203            collected
204                .iter()
205                .any(|p| p.parent().map(|d| d == tmp).unwrap_or(false)),
206            "should find Cargo.toml in tmp when input is sub/.."
207        );
208    }
209}