discovery.rs
raw
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<SourceKind>,
}
/// 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<String>) -> Result<Vec<Project>>;
}
/// What discovery found, plus anything that went wrong along the way.
#[derive(Debug, Default)]
pub struct Discovery {
pub projects: Vec<Project>,
pub warnings: Vec<String>,
}
/// 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<Discovery> {
let sources: Vec<Box<dyn ProjectSource>> = vec![
Box::new(CratesToml::new()?),
Box::new(Explicit::new(config.projects())),
];
let mut warnings = Vec::new();
let mut merged: BTreeMap<PathBuf, Project> = 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<PathBuf>) -> Result<Discovery> {
let mut warnings = Vec::new();
let projects = RootScan::new(roots).projects(&mut warnings)?;
Ok(Discovery { projects, warnings })
}