cargo_toml_workspace/
lib.rs

1use 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
16/// Load binstall metadata `Cargo.toml` from workspace at the provided path
17///
18/// WARNING: This is a blocking operation.
19///
20///  * `workspace_path` - can be a directory (path to workspace) or
21///    a file (path to `Cargo.toml`).
22pub 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    /// * `glob_path` - path to dir to glob for
145    ///
146    /// return paths relative to `glob_path`.
147    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 != "." // Ignore current dir
176                        && filename != ".." // Ignore parent dir
177                        && pattern.matches(&filename.to_string_lossy())
178                    {
179                        paths.push(path.join(filename));
180                    }
181                }
182            }
183        }
184
185        Ok(paths)
186    }
187
188    /// Return `true` if `path` matches the pattern.
189    /// It will still return `true` even if there are some trailing components.
190    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}