mod crates_toml; mod explicit; mod metadata; mod root_scan; use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::path::PathBuf; use anyhow::Result; use crate::config::Config; pub use crates_toml::CratesToml; pub use explicit::Explicit; pub use root_scan::RootScan; /// Which mechanism turned a project up. /// /// Neither of the two that run on every command touches the filesystem beyond /// the projects it already knows about: `.crates.toml` is a file, and the /// explicit list is a config key. A root scan is not among them — it walks a /// whole source tree, which is far too expensive to repeat on every run, so it /// happens only when `cargo grunk scan` is asked for and its findings are /// written into the explicit list. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum SourceKind { /// Path installs recorded in `$CARGO_HOME/.crates.toml`. CratesToml, /// A walk of a directory, run only by `cargo grunk scan`. RootScan, /// The config's `projects`. Explicit, } impl SourceKind { pub fn name(self) -> &'static str { match self { SourceKind::CratesToml => "crates-toml", SourceKind::RootScan => "root-scan", SourceKind::Explicit => "explicit", } } } impl fmt::Display for SourceKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.name()) } } /// A cargo workspace grunk manages, and where its build output lands. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Project { /// The workspace root — the directory a human would call "the project". pub root: PathBuf, /// The target directory. May not exist; nothing has necessarily been built. pub target: PathBuf, /// Every source that turned this project up, for `list` to report. pub found_by: BTreeSet, } /// A source of cargo projects. pub trait ProjectSource { fn kind(&self) -> SourceKind; /// Find projects, reporting per-project problems as warnings rather than /// failing the whole run — one unreadable manifest should not stop a clean. fn projects(&self, warnings: &mut Vec) -> Result>; } /// What discovery found, plus anything that went wrong along the way. #[derive(Debug, Default)] pub struct Discovery { pub projects: Vec, pub warnings: Vec, } /// Find every project grunk manages. /// /// Only the two cheap sources. Nothing here goes looking: a project is managed /// because cargo recorded you installing it from a path, or because you said /// so. Finding projects by searching for them is what `cargo grunk scan` is /// for, and what it finds it writes down, so that no later command has to look /// again. /// /// Projects are merged on their target directory rather than their root, /// because that is the thing being cleaned: two workspace members found via /// `.crates.toml` name one target directory between them, not two. pub fn discover(config: &Config) -> Result { let sources: Vec> = vec![ Box::new(CratesToml::new()?), Box::new(Explicit::new(config.projects())), ]; let mut warnings = Vec::new(); let mut merged: BTreeMap = BTreeMap::new(); for source in sources { let kind = source.kind(); let found = match source.projects(&mut warnings) { Ok(found) => found, Err(err) => { warnings.push(format!("{kind} source failed: {err:#}")); continue; } }; for project in found { merged .entry(project.target.clone()) .and_modify(|existing| existing.found_by.extend(project.found_by.iter().copied())) .or_insert(project); } } Ok(Discovery { projects: merged.into_values().collect(), warnings, }) } /// Search directories for cargo projects, for `cargo grunk scan`. /// /// Separate from `discover` on purpose: this is the expensive one, and it runs /// only when asked. pub fn search(roots: Vec) -> Result { let mut warnings = Vec::new(); let projects = RootScan::new(roots).projects(&mut warnings)?; Ok(Discovery { projects, warnings }) }