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 })
}