mitts 0.3.26

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 crate::core::action::ToolchainInfo;
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// The compiler argument that selects JSON diagnostics (`--message-format=json`).
pub const JSON_DIAGNOSTICS_ARG: &str = "--error-format=json";

#[derive(Debug, Clone, thiserror::Error, Diagnostic)]
pub enum RustAdapterError {
    #[error("Manifest not found at {0}")]
    #[diagnostic(
        code(mitts::rust::manifest_not_found),
        help(
            "Ensure Cargo.toml exists at the specified path or run from a valid project directory."
        )
    )]
    ManifestNotFound(PathBuf),

    #[error("Unsupported build option: {0}")]
    #[diagnostic(
        code(mitts::rust::unsupported_option),
        help("Run 'mitts build --help' to see supported options.")
    )]
    UnsupportedOption(String),

    #[error("Cargo execution failed with exit code {0}")]
    #[diagnostic(
        code(mitts::rust::cargo_failed),
        help("Check the compiler diagnostic output above for details.")
    )]
    CargoFailed(i32),

    #[error("Metadata or compiler query failed: {0}")]
    #[diagnostic(
        code(mitts::rust::metadata_failed),
        help(
            "Ensure the Rust toolchain is installed and reachable. If the message names a dependency, \
             check that Cargo.toml and Cargo.lock agree."
        )
    )]
    MetadataFailed(String),
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PlannedCompilationUnit {
    #[serde(default)]
    pub package_id: String,
    pub package_name: String,
    pub version: String,
    pub target_name: String,
    pub kind: String,
    pub src_path: PathBuf,
    #[serde(default)]
    pub manifest_path: PathBuf,
    pub cache_eligibility: String,
    #[serde(default)]
    pub is_workspace: bool,
    #[serde(default)]
    pub is_primary: bool,
    #[serde(default)]
    pub env_vars: Vec<String>,
    #[serde(default)]
    pub dependencies: Vec<String>,
    #[serde(default)]
    pub edition: Option<String>,
    #[serde(default)]
    pub features: Vec<String>,
    #[serde(default)]
    pub for_host: bool,
    #[serde(default)]
    pub workspace_root: Option<PathBuf>,
}

impl PlannedCompilationUnit {
    pub fn unit_id(&self) -> String {
        if self.for_host {
            format!(
                "{}::{}:{}:host",
                self.package_id, self.kind, self.target_name
            )
        } else {
            format!("{}::{}:{}", self.package_id, self.kind, self.target_name)
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectivePlanExplanation {
    pub manifest_path: PathBuf,
    pub target_directory: PathBuf,
    pub profile: String,
    pub toolchain: ToolchainInfo,
    pub units: Vec<PlannedCompilationUnit>,
    /// Runtime-only input snapshot, omitted from the plan's wire format.
    #[serde(skip)]
    pub manifest_snapshot: super::manifest_snapshot::ManifestSnapshot,
}

#[derive(Clone)]
pub struct BuildProgressCallback(pub std::sync::Arc<dyn Fn(BuildProgressEvent) + Send + Sync>);

impl std::fmt::Debug for BuildProgressCallback {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "BuildProgressCallback")
    }
}

#[derive(Debug, Clone)]
pub enum BuildProgressEvent {
    InitTotal { total: usize, cached: usize },
    Compiling(String),
    UnitFinished,
    CompilerMessage(String),
    Diagnostic(String),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum BuildMode {
    #[default]
    Build,
    Check,
    Clippy,
    Test,
}

#[derive(Debug, Clone, Default)]
pub struct RustBuildOptions {
    pub manifest_path: Option<PathBuf>,
    pub package: Option<String>,
    /// Additional package selectors, resolved together with `package` in one graph.
    pub packages: Vec<String>,
    pub release: bool,
    pub target_dir: Option<PathBuf>,
    pub features: Vec<String>,
    pub no_default_features: bool,
    pub all_features: bool,
    pub extra_args: Vec<String>,
    pub no_cache: bool,
    pub cache_dir: Option<PathBuf>,
    pub offline: bool,
    pub locked: bool,
    pub frozen: bool,
    pub example_targets: Vec<String>,
    pub target_triple: Option<String>,
    pub crate_types: Vec<String>,
    pub quiet: bool,
    /// Ask rustc for JSON diagnostics and report them as Cargo-shaped records
    /// (`mitts check|clippy --message-format=json`). Part of the action key.
    pub json_diagnostics: bool,
    /// Whether the fast build lock may answer a no-op build. `None` keeps it active only
    /// for the default store; `Some(true)` keeps it active with an explicit `cache_dir` too.
    pub fast_lock: Option<bool>,
    pub native: bool,
    pub cargo: bool,
    pub verbose: u8,
    pub mode: BuildMode,
    pub progress: Option<BuildProgressCallback>,
    pub start_dir: Option<PathBuf>,
    pub test_targets: Vec<String>,
    pub all_tests: bool,
    pub all_benches: bool,
    /// Benchmark targets selected by name (`mitts bench --bench NAME`).
    pub bench_targets: Vec<String>,
    pub all_examples: bool,
    pub bin_targets: Vec<String>,
    pub all_bins: bool,
    pub lib_only: bool,
    pub doc_only: bool,
    pub exclude: Vec<String>,
}

impl RustBuildOptions {
    pub fn selected_package_names(&self) -> impl Iterator<Item = &str> {
        self.package
            .iter()
            .chain(self.packages.iter())
            .map(String::as_str)
    }

    /// Whether benchmark targets are part of the selection: all of them or some by name.
    pub fn selects_benches(&self) -> bool {
        self.all_benches || !self.bench_targets.is_empty()
    }
}

impl RustBuildOptions {
    pub fn effective_compiler_args(&self) -> Vec<String> {
        let mut args = if let Some(target) = self.target_triple.as_deref() {
            let mut args = vec!["--target".to_string(), target.to_string()];
            args.extend(self.extra_args.iter().cloned());
            args
        } else {
            self.extra_args.clone()
        };
        if self.json_diagnostics {
            // Every unit reports JSON, and the argument keeps these actions apart from the
            // human-readable ones in the action cache.
            args.push(JSON_DIAGNOSTICS_ARG.to_string());
        }
        args
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum EnvMacroDiscovery {
    /// Successfully discovered static environment variable names read by env!/option_env!
    Tracked(Vec<String>),
    /// Encountered untracked or dynamic env!/option_env! macro whose variable name cannot be statically resolved
    UntrackedDynamic,
}