xtask-toolkit 0.1.12

A collection of useful tools for xtask building
Documentation
use std::fs::read_dir;
use std::path::{Path, PathBuf};
use std::{env, fs};

use xshell::{Shell, cmd};

#[derive(Debug, thiserror::Error)]
pub enum ProjectRootError {
    #[error("Unspecified IO error during project root discovery: {0}")]
    Io(std::io::Error),

    #[error("Project root (Cargo.lock) cannot be found")]
    MissingCargoLock,
}

impl From<std::io::Error> for ProjectRootError {
    fn from(e: std::io::Error) -> Self {
        ProjectRootError::Io(e)
    }
}

pub fn get_project_root() -> Result<PathBuf, ProjectRootError> {
    let path = env::current_dir()?;
    let path_ancestors = path.as_path().ancestors();

    for p in path_ancestors {
        let has_cargo = read_dir(p)?.any(|p| p.unwrap().file_name() == *"Cargo.lock");
        if has_cargo {
            return Ok(PathBuf::from(p));
        }
    }
    Err(ProjectRootError::MissingCargoLock)
}

#[derive(Debug, Clone)]
pub struct CargoToml(PathBuf);

impl CargoToml {

    pub fn autodiscovery() -> Vec<Self> {
        Self::autodiscovery_with(&[])
    }

    pub fn autodiscovery_with(additional_filenames: &[&str]) -> Vec<Self> {
        get_project_root().map(|p| Self::find_all(&p, additional_filenames)).unwrap_or_default()
    }

    pub fn find_all<P: AsRef<Path>>(dir: P, additional_filenames: &[&str]) -> Vec<Self> {
        let mut matches = Vec::new();
        let target_name = "Cargo.toml";

        if let Ok(entries) = fs::read_dir(&dir) {
            for entry in entries.flatten() {
                let path = entry.path();

                if path.is_dir() {
                    matches.extend(Self::find_all(&path, additional_filenames));
                } else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
                    if file_name == target_name || additional_filenames.contains(&file_name) {
                        matches.push(Self(path));
                    }
                }
            }
        }

        matches
    }
    pub fn find_first<P: AsRef<Path>>(dir: P, additional_filenames: &[&str]) -> Option<Self> {
        let target_name = "Cargo.toml";

        let mut entries = match fs::read_dir(&dir) {
            Ok(e) => e.filter_map(Result::ok).collect::<Vec<_>>(),
            Err(_) => return None,
        };

        entries.sort_by_key(|e| e.path());

        for entry in &entries {
            let path = entry.path();

            if path.is_file() {
                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                    if name == target_name || additional_filenames.contains(&name) {
                        return Some(Self(path));
                    }
                }
            }
        }

        for entry in entries {
            let path = entry.path();
            if path.is_dir() {
                if let Some(found) = Self::find_first(&path, additional_filenames) {
                    return Some(found);
                }
            }
        }

        None
    }

    fn get_toml_key<'a, T>(&self, keypath: &[&str]) -> Option<T>
    where
        T: serde::Deserialize<'a>,
    {
        let mut result = toml::from_str::<toml::Value>(&fs::read_to_string(&self.0).ok()?).ok()?;

        for key in keypath {
            if result.is_table() && result.get(key).is_some() {
                let table = result.as_table_mut().unwrap();
                result = table.remove(*key).unwrap();
            } else {
                break;
            }
        }

        result.try_into().ok()
    }

    pub fn path<'a>(&self) -> &Path {
        &self.0
    }

    pub fn version(&self) -> Option<String> {
        self.get_toml_key(&["package", "version"])
    }

    pub fn name(&self) -> Option<String> {
        self.get_toml_key(&["package", "name"])
    }

    pub fn license(&self) -> Option<String> {
        self.get_toml_key(&["package", "license"])
    }

    pub fn authors(&self) -> Option<Vec<String>> {
        self.get_toml_key(&["package", "authors"])
    }

    pub fn description(&self) -> Option<String> {
        self.get_toml_key(&["package", "description"])
    }

    pub fn versioned_name(&self) -> Option<String> {
        let name = self.name();
        let version = self.version();
        name.zip(version).map(|(name, version)| format!("{}-{}", name, version))
    }
}

pub struct BinaryBuild {
    projects: Vec<String>,
    target: Option<String>,
}

impl BinaryBuild {
    pub fn new() -> Self {
        Self {
            projects: Vec::new(),
            target: None,
        }
    }

    pub fn with_project(&mut self, project: &str) -> &mut Self {
        self.projects.push(project.to_string());
        self
    }

    pub fn with_projects<T, I>(&mut self, projects: I) -> &mut Self
    where
        I: IntoIterator<Item = T>,
        T: ToString,
    {
        self.projects.extend(projects.into_iter().map(|x| x.to_string()));
        self
    }

    pub fn with_target(&mut self, target: &str) -> &mut Self {
        self.target = Some(target.to_string());
        self
    }

    pub fn build(&self) -> Result<(), xshell::Error> {
        let sh = Shell::new()?;

        let projects: Vec<String> = self.projects.iter().map(|x| format!("-p={}", x)).collect();

        let cmd = sh.cmd("cargo").args([
            "build",
            "--release",
        ]);

        let cmd = if let Some(target) = &self.target {
            cmd.args(["--target", target])
        } else {
            cmd
        };

        cmd.args(projects).run()?;

        Ok(())
    }
}

pub fn force_fmt() -> Result<(), xshell::Error> {
    let sh = Shell::new()?;
    cmd!(sh, "cargo fmt").read()?;
    cmd!(sh, "cargo clippy --fix --allow-dirty --allow-staged").read()?;
    Ok(())
}