cargo_toml_workspace/
lib.rs1use std::{
2 io, mem,
3 path::{Path, PathBuf},
4};
5
6use cargo_toml::{Error as CargoTomlError, Manifest};
7use compact_str::CompactString;
8use glob::PatternError;
9use normalize_path::NormalizePath;
10use serde::de::DeserializeOwned;
11use thiserror::Error as ThisError;
12use tracing::{debug, instrument, warn};
13
14pub use cargo_toml;
15
16pub fn load_manifest_from_workspace<Metadata: DeserializeOwned>(
23 workspace_path: impl AsRef<Path>,
24 crate_name: impl AsRef<str>,
25) -> Result<Manifest<Metadata>, Error> {
26 fn inner<Metadata: DeserializeOwned>(
27 workspace_path: &Path,
28 crate_name: &str,
29 ) -> Result<Manifest<Metadata>, Error> {
30 load_manifest_from_workspace_inner(workspace_path, crate_name).map_err(|inner| Error {
31 workspace_path: workspace_path.into(),
32 crate_name: crate_name.into(),
33 inner,
34 })
35 }
36
37 inner(workspace_path.as_ref(), crate_name.as_ref())
38}
39
40#[derive(Debug, ThisError)]
41#[error("Failed to load {crate_name} from {}: {inner}", workspace_path.display())]
42pub struct Error {
43 workspace_path: Box<Path>,
44 crate_name: CompactString,
45 #[source]
46 inner: ErrorInner,
47}
48
49#[derive(Debug, ThisError)]
50enum ErrorInner {
51 #[error("Invalid pattern in workspace.members or workspace.exclude: {0}")]
52 PatternError(#[from] PatternError),
53
54 #[error("Invalid pattern `{0}`: It must be relative and point within current dir")]
55 InvalidPatternError(CompactString),
56
57 #[error("Failed to parse cargo manifest: {0}")]
58 CargoManifest(#[from] CargoTomlError),
59
60 #[error("I/O error: {0}")]
61 Io(#[from] io::Error),
62
63 #[error("Not found")]
64 NotFound,
65}
66
67#[instrument]
68fn load_manifest_from_workspace_inner<Metadata: DeserializeOwned>(
69 workspace_path: &Path,
70 crate_name: &str,
71) -> Result<Manifest<Metadata>, ErrorInner> {
72 debug!(
73 "Loading manifest of crate {crate_name} from workspace: {}",
74 workspace_path.display()
75 );
76
77 let manifest_path = if workspace_path.is_file() {
78 workspace_path.to_owned()
79 } else {
80 workspace_path.join("Cargo.toml")
81 };
82
83 let mut manifest_paths = vec![manifest_path];
84
85 while let Some(manifest_path) = manifest_paths.pop() {
86 let manifest = Manifest::<Metadata>::from_path_with_metadata(&manifest_path)?;
87
88 let name = manifest.package.as_ref().map(|p| &*p.name);
89 debug!(
90 "Loading from {}, manifest.package.name = {:#?}",
91 manifest_path.display(),
92 name
93 );
94
95 if name == Some(crate_name) {
96 return Ok(manifest);
97 }
98
99 if let Some(ws) = manifest.workspace {
100 let excludes = ws.exclude;
101 let members = ws.members;
102
103 if members.is_empty() {
104 continue;
105 }
106
107 let exclude_patterns = excludes
108 .into_iter()
109 .map(|pat| Pattern::new(&pat))
110 .collect::<Result<Vec<_>, _>>()?;
111
112 let workspace_path = manifest_path.parent().unwrap();
113
114 for member in members {
115 for path in Pattern::new(&member)?.glob_dirs(workspace_path)? {
116 if !exclude_patterns
117 .iter()
118 .any(|exclude| exclude.matches_with_trailing(&path))
119 {
120 manifest_paths.push(workspace_path.join(path).join("Cargo.toml"));
121 }
122 }
123 }
124 }
125 }
126
127 Err(ErrorInner::NotFound)
128}
129
130struct Pattern(Vec<glob::Pattern>);
131
132impl Pattern {
133 fn new(pat: &str) -> Result<Self, ErrorInner> {
134 Path::new(pat)
135 .try_normalize()
136 .ok_or_else(|| ErrorInner::InvalidPatternError(pat.into()))?
137 .iter()
138 .map(|c| glob::Pattern::new(c.to_str().unwrap()))
139 .collect::<Result<Vec<_>, _>>()
140 .map_err(Into::into)
141 .map(Self)
142 }
143
144 fn glob_dirs(&self, glob_path: &Path) -> Result<Vec<PathBuf>, ErrorInner> {
148 let mut paths = vec![PathBuf::new()];
149
150 for pattern in &self.0 {
151 if paths.is_empty() {
152 break;
153 }
154
155 for path in mem::take(&mut paths) {
156 let p = glob_path.join(&path);
157 let res = p.read_dir();
158 if res.is_err() && !p.is_dir() {
159 continue;
160 }
161 drop(p);
162
163 for res in res? {
164 let entry = res?;
165
166 let is_dir = entry
167 .file_type()
168 .map(|file_type| file_type.is_dir() || file_type.is_symlink())
169 .unwrap_or(false);
170 if !is_dir {
171 continue;
172 }
173
174 let filename = entry.file_name();
175 if filename != "." && filename != ".." && pattern.matches(&filename.to_string_lossy())
178 {
179 paths.push(path.join(filename));
180 }
181 }
182 }
183 }
184
185 Ok(paths)
186 }
187
188 fn matches_with_trailing(&self, path: &Path) -> bool {
191 let mut iter = path.iter().map(|os_str| os_str.to_string_lossy());
192 for pattern in &self.0 {
193 match iter.next() {
194 Some(s) if pattern.matches(&s) => (),
195 _ => return false,
196 }
197 }
198 true
199 }
200}
201
202#[cfg(test)]
203mod test {
204 use std::fs::create_dir_all as mkdir;
205
206 use tempfile::TempDir;
207
208 use super::*;
209
210 #[test]
211 fn test_glob_dirs() {
212 let pattern = Pattern::new("*/*/q/*").unwrap();
213 let tempdir = TempDir::new().unwrap();
214
215 mkdir(tempdir.as_ref().join("a/b/c/efe")).unwrap();
216 mkdir(tempdir.as_ref().join("a/b/q/ww")).unwrap();
217 mkdir(tempdir.as_ref().join("d/233/q/d")).unwrap();
218
219 let mut paths = pattern.glob_dirs(tempdir.as_ref()).unwrap();
220 paths.sort_unstable();
221 assert_eq!(
222 paths,
223 vec![PathBuf::from("a/b/q/ww"), PathBuf::from("d/233/q/d")]
224 );
225 }
226
227 #[test]
228 fn test_matches_with_trailing() {
229 let pattern = Pattern::new("*/*/q/*").unwrap();
230
231 assert!(pattern.matches_with_trailing(Path::new("a/b/q/d/")));
232 assert!(pattern.matches_with_trailing(Path::new("a/b/q/d")));
233 assert!(pattern.matches_with_trailing(Path::new("a/b/q/d/234")));
234 assert!(pattern.matches_with_trailing(Path::new("a/234/q/d/234")));
235
236 assert!(!pattern.matches_with_trailing(Path::new("")));
237 assert!(!pattern.matches_with_trailing(Path::new("a/")));
238 assert!(!pattern.matches_with_trailing(Path::new("a/234")));
239 assert!(!pattern.matches_with_trailing(Path::new("a/234/q")));
240 }
241
242 #[test]
243 fn test_load() {
244 let p = Path::new(env!("CARGO_MANIFEST_DIR"))
245 .parent()
246 .unwrap()
247 .parent()
248 .unwrap()
249 .join("e2e-tests/manifests/workspace");
250
251 let manifest =
252 load_manifest_from_workspace::<cargo_toml::Value>(&p, "cargo-binstall").unwrap();
253 let package = manifest.package.unwrap();
254 assert_eq!(package.name, "cargo-binstall");
255 assert_eq!(package.version.as_ref().unwrap(), "0.12.0");
256 assert_eq!(manifest.bin.len(), 1);
257 assert_eq!(manifest.bin[0].name.as_deref().unwrap(), "cargo-binstall");
258 assert_eq!(manifest.bin[0].path.as_deref().unwrap(), "src/main.rs");
259
260 let err = load_manifest_from_workspace_inner::<cargo_toml::Value>(&p, "cargo-binstall2")
261 .unwrap_err();
262 assert!(matches!(err, ErrorInner::NotFound), "{:#?}", err);
263
264 let manifest =
265 load_manifest_from_workspace::<cargo_toml::Value>(&p, "cargo-watch").unwrap();
266 let package = manifest.package.unwrap();
267 assert_eq!(package.name, "cargo-watch");
268 assert_eq!(package.version.as_ref().unwrap(), "8.4.0");
269 assert_eq!(manifest.bin.len(), 1);
270 assert_eq!(manifest.bin[0].name.as_deref().unwrap(), "cargo-watch");
271 }
272}