mitts 0.2.1

Rust-first build and package tooling with a shared artifact cache
Documentation
/*
 * Mitts Build — Rust-first build and package tooling with shared artifact cache
 * Copyright (c) 2026 Alexander Panasenko
 *
 * Contact: alex@prod.codes
 * Author: https://prod.codes/about/
 * Project: https://github.com/alex09x/mitts
 * SPDX-License-Identifier: MIT OR Apache-2.0
 */

use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;

/// Strongly typed representation of a Cargo.toml manifest.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CargoToml {
    pub package: Option<TomlPackage>,
    pub workspace: Option<TomlWorkspace>,
    pub lib: Option<TomlLib>,
    #[serde(default)]
    pub bin: Vec<TomlBin>,
    #[serde(default)]
    pub dependencies: HashMap<String, TomlDependency>,
    #[serde(default, rename = "build-dependencies")]
    pub build_dependencies: HashMap<String, TomlDependency>,
    #[serde(default, rename = "dev-dependencies", alias = "dev_dependencies")]
    pub dev_dependencies: HashMap<String, TomlDependency>,
    #[serde(default)]
    pub test: Vec<TomlTest>,
    #[serde(default)]
    pub example: Vec<TomlTest>,
    #[serde(default)]
    pub bench: Vec<TomlTest>,
    #[serde(default)]
    pub features: HashMap<String, Vec<String>>,
    #[serde(default)]
    pub target: HashMap<String, TomlTargetSpec>,
    #[serde(default)]
    pub lints: Option<toml::Value>,
}

pub use super::manifest_models::*;

impl CargoToml {
    /// Parses Cargo.toml content from a string.
    pub fn parse(content: &str) -> Result<Self> {
        toml::from_str(content).context("Failed to parse Cargo.toml content")
    }

    /// Reads and parses a Cargo.toml file from disk.
    pub fn from_file(path: &Path) -> Result<Self> {
        let content = fs::read_to_string(path)
            .with_context(|| format!("Failed to read manifest at {:?}", path))?;
        let mut toml = Self::parse(&content)?;
        toml.resolve_inherited_workspace_fields(path);
        Ok(toml)
    }

    pub fn resolve_inherited_workspace_fields(&mut self, path: &Path) {
        super::manifest_inherit::resolve_inherited_workspace_fields(self, path);
    }

    /// Returns the effective Cargo feature resolver version ("1" or "2").
    pub fn effective_resolver(&self) -> &str {
        if let Some(ref ws) = self.workspace {
            if let Some(ref r) = ws.resolver {
                return r.as_str();
            }
        }
        if let Some(ref pkg) = self.package {
            if let Some(ref r) = pkg.resolver {
                return r.as_str();
            }
            if let Some(ref ed) = pkg.edition {
                if ed == "2021" || ed == "2024" {
                    return "2";
                }
            }
        }
        "1"
    }

    /// Checks if this package defines or defaults to a build script.
    pub fn has_build_script(&self, pkg_dir: &Path) -> bool {
        if let Some(ref pkg) = self.package {
            if let Some(ref b) = pkg.build {
                return match b {
                    TomlBuild::Disabled(false) => false,
                    TomlBuild::Disabled(true) => pkg_dir.join("build.rs").is_file(),
                    TomlBuild::Path(ref p) => pkg_dir.join(p).is_file(),
                };
            }
        }
        pkg_dir.join("build.rs").is_file()
    }

    /// Extracts bidirectional mappings between dependency aliases and real package names.
    pub fn dependency_aliases(&self) -> DependencyAliases {
        let mut res = DependencyAliases::default();
        let mut record = |alias: &str, dep: &TomlDependency| {
            if let TomlDependency::Detailed(ref d) = dep {
                if let Some(ref real) = d.package_name {
                    res.alias_to_real.insert(alias.to_string(), real.clone());
                    res.alias_to_real
                        .insert(alias.replace('-', "_"), real.clone());
                    res.real_to_aliases
                        .entry(real.clone())
                        .or_default()
                        .push((alias.to_string(), d.version.clone()));
                    let norm = real.replace('-', "_");
                    if norm != *real {
                        res.real_to_aliases
                            .entry(norm)
                            .or_default()
                            .push((alias.to_string(), d.version.clone()));
                    }
                }
            }
        };
        for (alias, dep) in &self.dependencies {
            record(alias, dep);
        }
        for (alias, dep) in &self.build_dependencies {
            record(alias, dep);
        }
        for (alias, dep) in &self.dev_dependencies {
            record(alias, dep);
        }
        for target_spec in self.target.values() {
            for (alias, dep) in &target_spec.dependencies {
                record(alias, dep);
            }
            for (alias, dep) in &target_spec.build_dependencies {
                record(alias, dep);
            }
            for (alias, dep) in &target_spec.dev_dependencies {
                record(alias, dep);
            }
        }
        res
    }

    /// Checks if the manifest has a direct, unaliased dependency with the given package name,
    /// optionally matching a resolved version requirement.
    pub fn has_direct_dep_named(&self, pkg_name: &str, dep_version: Option<&str>) -> bool {
        let norm = pkg_name.replace('-', "_");
        let matches_dep = |alias: &str, dep: &TomlDependency| -> bool {
            let alias_norm = alias.replace('-', "_");
            if alias_norm != norm {
                return false;
            }
            match dep {
                TomlDependency::Simple(req_str) => {
                    if let Some(ver_str) = dep_version {
                        match (
                            semver::VersionReq::parse(req_str),
                            semver::Version::parse(ver_str),
                        ) {
                            (Ok(req), Ok(ver)) => req.matches(&ver),
                            _ => ver_str.starts_with(req_str) || req_str == ver_str,
                        }
                    } else {
                        true
                    }
                }
                TomlDependency::Detailed(d) => {
                    let is_unaliased = match d.package_name {
                        None => true,
                        Some(ref p) => p.replace('-', "_") == norm,
                    };
                    if !is_unaliased {
                        return false;
                    }
                    if let (Some(req_str), Some(ver_str)) = (&d.version, dep_version) {
                        match (
                            semver::VersionReq::parse(req_str),
                            semver::Version::parse(ver_str),
                        ) {
                            (Ok(req), Ok(ver)) => req.matches(&ver),
                            _ => ver_str.starts_with(req_str) || req_str == ver_str,
                        }
                    } else {
                        true
                    }
                }
            }
        };

        for (alias, dep) in &self.dependencies {
            if matches_dep(alias, dep) {
                return true;
            }
        }
        for (alias, dep) in &self.build_dependencies {
            if matches_dep(alias, dep) {
                return true;
            }
        }
        for (alias, dep) in &self.dev_dependencies {
            if matches_dep(alias, dep) {
                return true;
            }
        }
        for target_spec in self.target.values() {
            for (alias, dep) in &target_spec.dependencies {
                if matches_dep(alias, dep) {
                    return true;
                }
            }
            for (alias, dep) in &target_spec.build_dependencies {
                if matches_dep(alias, dep) {
                    return true;
                }
            }
            for (alias, dep) in &target_spec.dev_dependencies {
                if matches_dep(alias, dep) {
                    return true;
                }
            }
        }
        false
    }

    /// Discovers all compilation targets (lib, bin, proc-macro) in this package.
    pub fn discover_targets(&self, pkg_dir: &Path) -> Vec<DiscoveredTarget> {
        let mut targets = Vec::new();
        let pkg_name = self.package.as_ref().map(|p| p.name.as_str()).unwrap_or("");
        let edition = self.package.as_ref().and_then(|p| p.edition.clone());

        // 1. Library target
        if let Some(ref lib) = self.lib {
            let lib_name = lib
                .name
                .clone()
                .unwrap_or_else(|| pkg_name.replace('-', "_"));
            let src = match lib.path {
                Some(ref p) => pkg_dir.join(p),
                None => pkg_dir.join("src").join("lib.rs"),
            };
            let kind = if lib.proc_macro.unwrap_or(false) {
                "proc-macro".to_string()
            } else {
                "lib".to_string()
            };
            let crate_types = if !lib.crate_type.is_empty() {
                lib.crate_type.clone()
            } else {
                vec![kind.clone()]
            };
            if src.is_file() {
                targets.push(DiscoveredTarget {
                    name: lib_name,
                    kind,
                    src_path: src,
                    edition: edition.clone(),
                    crate_types,
                    required_features: None,
                    harness: self.lib.as_ref().and_then(|l| l.harness).unwrap_or(true),
                    test_role: None,
                    explicit_only: false,
                });
            }
        } else {
            let default_lib = pkg_dir.join("src").join("lib.rs");
            if default_lib.is_file() {
                targets.push(DiscoveredTarget {
                    name: pkg_name.replace('-', "_"),
                    kind: "lib".to_string(),
                    src_path: default_lib,
                    edition: edition.clone(),
                    crate_types: vec!["lib".to_string()],
                    required_features: None,
                    harness: true,
                    test_role: None,
                    explicit_only: false,
                });
            }
        }

        // 2. Binary targets
        if !self.bin.is_empty() {
            for b in &self.bin {
                let src = match b.path {
                    Some(ref p) => pkg_dir.join(p),
                    None => {
                        let candidate1 = pkg_dir
                            .join("src")
                            .join("bin")
                            .join(format!("{}.rs", b.name));
                        if candidate1.is_file() {
                            candidate1
                        } else {
                            pkg_dir.join("src").join("main.rs")
                        }
                    }
                };
                if src.is_file() {
                    targets.push(DiscoveredTarget {
                        name: b.name.clone(),
                        kind: "bin".to_string(),
                        src_path: src,
                        edition: edition.clone(),
                        crate_types: vec!["bin".to_string()],
                        required_features: b.required_features.clone(),
                        harness: b.harness.unwrap_or(true),
                        test_role: None,
                        explicit_only: false,
                    });
                }
            }
        } else {
            let default_main = pkg_dir.join("src").join("main.rs");
            if default_main.is_file() && !pkg_name.is_empty() {
                targets.push(DiscoveredTarget {
                    name: pkg_name.to_string(),
                    kind: "bin".to_string(),
                    src_path: default_main,
                    edition: edition.clone(),
                    crate_types: vec!["bin".to_string()],
                    required_features: None,
                    harness: true,
                    test_role: None,
                    explicit_only: false,
                });
            }

            // Check autobins
            let autobins = self
                .package
                .as_ref()
                .and_then(|p| p.autobins)
                .unwrap_or(true);
            if autobins {
                let bin_dir = pkg_dir.join("src").join("bin");
                if let Ok(entries) = fs::read_dir(&bin_dir) {
                    for entry in entries.flatten() {
                        let path = entry.path();
                        if path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("rs")
                        {
                            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
                                targets.push(DiscoveredTarget {
                                    name: stem.to_string(),
                                    kind: "bin".to_string(),
                                    src_path: path,
                                    edition: edition.clone(),
                                    crate_types: vec!["bin".to_string()],
                                    required_features: None,
                                    harness: true,
                                    test_role: None,
                                    explicit_only: false,
                                });
                            }
                        } else if path.is_dir() {
                            let main_rs = path.join("main.rs");
                            if main_rs.is_file() {
                                if let Some(dir_name) = path.file_name().and_then(|s| s.to_str()) {
                                    targets.push(DiscoveredTarget {
                                        name: dir_name.to_string(),
                                        kind: "bin".to_string(),
                                        src_path: main_rs,
                                        edition: edition.clone(),
                                        crate_types: vec!["bin".to_string()],
                                        required_features: None,
                                        harness: true,
                                        test_role: None,
                                        explicit_only: false,
                                    });
                                }
                            }
                        }
                    }
                }
            }
        }

        targets
    }
}