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 super::features::ResolvedFeatures;
use super::lockfile::CargoLockPackage;
use super::manifest::CargoToml;
use super::manifest_models::DiscoveredTarget;
use super::types::{EnvMacroDiscovery, PlannedCompilationUnit, RustBuildOptions};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// Builds planned compilation units for all active packages and targets,
/// emitting separate target and host units for dual-compiled crates (Resolver 2).
pub fn build_planned_units(
    resolved_packages: &[(&CargoLockPackage, bool, PathBuf, CargoToml)],
    path_dependencies: &HashMap<String, Vec<(PathBuf, CargoToml)>>,
    package_features: &ResolvedFeatures,
    opt: &RustBuildOptions,
    is_primary_pkg: impl Fn(&str) -> bool,
    ws_root: Option<&Path>,
) -> Vec<PlannedCompilationUnit> {
    let mut units = Vec::new();

    for (pkg, is_workspace, pkg_dir, pkg_toml) in resolved_packages {
        let is_local = *is_workspace
            || (pkg.source.is_none()
                && path_dependencies.get(&pkg.name).is_some_and(|entries| {
                    entries.iter().any(|(_, toml)| {
                        toml.package
                            .as_ref()
                            .and_then(|p| p.version.as_deref())
                            .unwrap_or("0.0.0")
                            == pkg.version
                    })
                }));
        // A dependency that shares a member's name (an older published version pulled in
        // by a dev-dependency) is neither primary nor the `-p` target.
        let is_target_pkg =
            *is_workspace && opt.selected_package_names().any(|name| name == pkg.name);
        let is_primary = *is_workspace && is_primary_pkg(&pkg.name);
        let mut targets = pkg_toml.discover_targets(pkg_dir);
        if (is_primary || is_target_pkg) && !opt.crate_types.is_empty() {
            for target in &mut targets {
                if target.kind == "lib" || target.kind == "proc-macro" {
                    target.crate_types = opt.crate_types.clone();
                }
            }
        }
        if opt.all_examples {
            targets.retain(|target| target.kind != "example" || is_primary || is_target_pkg);
        } else if opt.example_targets.is_empty() {
            targets.retain(|target| target.kind != "example");
        } else {
            targets.retain(|target| {
                target.kind != "example"
                    || (*is_workspace
                        && opt.example_targets.iter().any(|name| name == &target.name))
            });
        }
        let has_tests = opt.mode == crate::adapter::BuildMode::Test
            || opt.all_tests
            || opt.selects_benches()
            || !opt.test_targets.is_empty();
        if has_tests && (is_primary || is_target_pkg) {
            let mut test_targets = pkg_toml.discover_test_targets(pkg_dir);
            if opt.selects_benches() {
                test_targets.extend(pkg_toml.discover_bench_targets(pkg_dir));
            }
            if opt.doc_only {
                test_targets.retain(|t| {
                    t.test_role.as_ref().map_or_else(
                        || t.name.ends_with("_unit_test"),
                        |r| *r == super::manifest_models::TestRole::LibUnit,
                    )
                });
            } else {
                super::test_selection::select_test_targets(&mut test_targets, opt);
                let has_doctest = opt.mode == crate::adapter::BuildMode::Test
                    && pkg_toml.discover_doctest_target(pkg_dir).is_some();
                let has_lib_unit = test_targets.iter().any(|t| {
                    t.test_role.as_ref().map_or_else(
                        || t.name.ends_with("_unit_test"),
                        |r| *r == super::manifest_models::TestRole::LibUnit,
                    )
                });
                let should_run_doctests = !super::test_selection::has_target_selection(opt);
                if should_run_doctests && has_doctest && !has_lib_unit {
                    if let Some(mut doc_dep_target) = pkg_toml
                        .discover_test_targets(pkg_dir)
                        .into_iter()
                        .find(|t| {
                            t.test_role.as_ref().map_or_else(
                                || t.name.ends_with("_unit_test"),
                                |r| *r == super::manifest_models::TestRole::LibUnit,
                            )
                        })
                    {
                        doc_dep_target.kind = "test-deps".to_string();
                        test_targets.push(doc_dep_target);
                    }
                }
            }
            targets.extend(test_targets);
        }
        if !*is_workspace && !is_target_pkg {
            targets.retain(|t| t.kind != "bin" && t.kind != "test");
        }

        let (env_discovery, env_vars, manifest_dir_readers) = if is_local {
            let scan = super::env_macro_cache::discover_detailed(pkg_dir);
            (scan.discovery, scan.vars, scan.manifest_dir_readers)
        } else {
            (
                EnvMacroDiscovery::Tracked(Vec::new()),
                Vec::new(),
                Vec::new(),
            )
        };
        let has_untracked_env = matches!(env_discovery, EnvMacroDiscovery::UntrackedDynamic);
        let pkg_id =
            super::features::format_package_id(&pkg.name, &pkg.version, pkg.source.as_deref());
        let is_pm_pkg = pkg_toml.lib.as_ref().is_some_and(|l| {
            l.proc_macro == Some(true) || l.crate_type.iter().any(|k| k == "proc-macro")
        });
        let in_target = (!is_pm_pkg && (*is_workspace || is_primary))
            || package_features.active_target_packages.contains(&pkg_id)
            || package_features.active_target_packages.contains(&pkg.name);
        let in_host = (is_primary && is_pm_pkg)
            || package_features.active_host_packages.contains(&pkg_id)
            || package_features.active_host_packages.contains(&pkg.name);

        for target in targets {
            let is_pm = target.kind == "proc-macro";
            let is_executable = matches!(
                target.kind.as_str(),
                "bin" | "example" | "test" | "test-deps"
            );

            if is_pm {
                if in_host || is_primary {
                    let feats = package_features
                        .host
                        .get(&pkg_id)
                        .or_else(|| package_features.host.get(&pkg.name))
                        .cloned()
                        .unwrap_or_default();
                    if target_features_satisfied(&target, &feats, opt) {
                        units.push(create_unit(
                            pkg,
                            pkg_dir,
                            pkg_toml,
                            &target,
                            &pkg_id,
                            true,
                            feats,
                            *is_workspace,
                            has_untracked_env,
                            &env_vars,
                            &manifest_dir_readers,
                            ws_root,
                        ));
                    }
                }
            } else if is_executable {
                let unit_in_scope = if is_pm_pkg {
                    in_host || is_primary
                } else {
                    in_target
                };
                if unit_in_scope {
                    let feat_map = if is_pm_pkg {
                        &package_features.host
                    } else {
                        &package_features.target
                    };
                    let feats = feat_map
                        .get(&pkg_id)
                        .or_else(|| feat_map.get(&pkg.name))
                        .cloned()
                        .unwrap_or_default();
                    if target_features_satisfied(&target, &feats, opt) {
                        units.push(create_unit(
                            pkg,
                            pkg_dir,
                            pkg_toml,
                            &target,
                            &pkg_id,
                            is_pm_pkg,
                            feats,
                            *is_workspace,
                            has_untracked_env,
                            &env_vars,
                            &manifest_dir_readers,
                            ws_root,
                        ));
                    }
                }
            } else {
                // Library target (kind == "lib", "rlib", etc.)
                let target_feats = package_features
                    .target
                    .get(&pkg_id)
                    .or_else(|| package_features.target.get(&pkg.name))
                    .cloned()
                    .unwrap_or_default();
                let host_feats = package_features
                    .host
                    .get(&pkg_id)
                    .or_else(|| package_features.host.get(&pkg.name))
                    .cloned()
                    .unwrap_or_default();

                if in_target && target_features_satisfied(&target, &target_feats, opt) {
                    units.push(create_unit(
                        pkg,
                        pkg_dir,
                        pkg_toml,
                        &target,
                        &pkg_id,
                        false,
                        target_feats.clone(),
                        *is_workspace,
                        has_untracked_env,
                        &env_vars,
                        &manifest_dir_readers,
                        ws_root,
                    ));
                }

                if in_host && target_features_satisfied(&target, &host_feats, opt) {
                    units.push(create_unit(
                        pkg,
                        pkg_dir,
                        pkg_toml,
                        &target,
                        &pkg_id,
                        true,
                        host_feats,
                        *is_workspace,
                        has_untracked_env,
                        &env_vars,
                        &manifest_dir_readers,
                        ws_root,
                    ));
                }
            }
        }
    }

    units
}
fn target_features_satisfied(
    target: &DiscoveredTarget,
    features: &[String],
    opt: &RustBuildOptions,
) -> bool {
    target.required_features.as_ref().is_none_or(|req_feats| {
        req_feats
            .iter()
            .all(|req| features.contains(req) || opt.features.contains(req))
    })
}

#[allow(clippy::too_many_arguments)]
fn create_unit(
    pkg: &CargoLockPackage,
    pkg_dir: &Path,
    pkg_toml: &CargoToml,
    target: &DiscoveredTarget,
    pkg_id: &str,
    for_host: bool,
    features: Vec<String>,
    is_workspace: bool,
    has_untracked_env: bool,
    env_vars: &[String],
    manifest_dir_readers: &[PathBuf],
    ws_root: Option<&Path>,
) -> PlannedCompilationUnit {
    // `CARGO_MANIFEST_DIR` keys a unit on its absolute checkout path, so only the units
    // whose own sources read it carry the variable; see `env_macro_scope`.
    let env_vars: Vec<String> = env_vars
        .iter()
        .filter(|v| {
            v.as_str() != "CARGO_MANIFEST_DIR"
                || super::env_macro_scope::unit_reads_manifest_dir(
                    pkg_dir,
                    &target.src_path,
                    manifest_dir_readers,
                )
        })
        .cloned()
        .collect();
    let has_mixed_or_native = target
        .crate_types
        .iter()
        .any(|k| matches!(k.as_str(), "cdylib" | "dylib" | "staticlib"));

    let (kind_str, eligibility) = if has_untracked_env {
        (
            target.kind.clone(),
            "Uncached (contains untracked dynamic env!/option_env! macro)".to_string(),
        )
    } else if has_mixed_or_native {
        (
            target.crate_types.join(","),
            "Uncached (contains native/dynamic library crate types; outside initial M2 pure rlib subset)".to_string(),
        )
    } else if target.kind == "test-deps" {
        (
            "test-deps".to_string(),
            "Doctest dependency placeholder (not compiled as test binary)".to_string(),
        )
    } else if target.kind == "test" {
        (
            if target.harness {
                "test".to_string()
            } else {
                "custom-test".to_string()
            },
            "Uncached in M1 (eligible for M2 test binary artifact cache)".to_string(),
        )
    } else {
        let kind = target.kind.clone();
        let reason = if is_workspace {
            "Uncached in M1 (eligible for M2 workspace action cache)".to_string()
        } else {
            "Uncached in M1 (eligible for M2 immutable dependency cache)".to_string()
        };
        (kind, reason)
    };

    PlannedCompilationUnit {
        package_id: pkg_id.to_string(),
        package_name: pkg.name.clone(),
        version: pkg.version.clone(),
        target_name: target.name.clone(),
        kind: kind_str,
        src_path: target.src_path.clone(),
        manifest_path: pkg_dir.join("Cargo.toml"),
        cache_eligibility: eligibility,
        is_workspace,
        is_primary: false,
        env_vars,
        dependencies: Vec::new(),
        edition: target
            .edition
            .clone()
            .or_else(|| pkg_toml.package.as_ref().and_then(|p| p.edition.clone())),
        features,
        for_host,
        workspace_root: ws_root.map(|p| p.to_path_buf()),
    }
}

/// A library unit: kind `lib`, or the `[lib] crate-type` list a native/dynamic library
/// carries as its kind (`staticlib,cdylib,rlib` for regex's C API crate `rure`).
pub fn is_library_kind(kind: &str) -> bool {
    kind == "lib"
        || kind
            .split(',')
            .all(|k| matches!(k.trim(), "lib" | "rlib" | "dylib" | "cdylib" | "staticlib"))
}