use crate::adapter::{BuildProgressCallback, BuildProgressEvent, RustAdapter, RustBuildOptions};
use crate::cli::diagnostic::render_compiler_diagnostics;
use crate::cli::terminal::TerminalGuard;
use crate::cli::ui::Ui;
use crate::cli::BuildArgs;
use crate::core::ActionKind;
pub fn to_rust_options(args: &BuildArgs) -> RustBuildOptions {
RustBuildOptions {
manifest_path: args.manifest_path.clone(),
package: args.package.clone(),
release: args.release,
target_dir: args.target_dir.clone(),
features: args.features.clone(),
no_default_features: args.no_default_features,
all_features: args.all_features,
extra_args: args.extra_args.clone(),
no_cache: args.no_cache,
cache_dir: args.cache_dir.clone(),
fast_lock: args.fast_lock,
native: args.native,
cargo: args.cargo,
verbose: args.verbose,
mode: crate::adapter::BuildMode::Build,
progress: None,
start_dir: None,
exclude: args.exclude.clone(),
bin_targets: args.bin.clone(),
all_bins: args.bins,
lib_only: args.lib,
..Default::default()
}
}
pub fn create_progress_callback(progress: Option<indicatif::ProgressBar>) -> BuildProgressCallback {
let progress_clone = progress;
BuildProgressCallback(std::sync::Arc::new(move |event| match event {
BuildProgressEvent::InitTotal { total, cached } => {
if let Some(ref pb) = progress_clone {
Ui::set_build_progress_total(pb, total, cached);
pb.set_message("Compiling...".to_string());
} else {
println!(
"[mitts::dag] Initialized DAG: {} units ({} pre-cached)",
total, cached
);
}
}
BuildProgressEvent::Compiling(crate_name) => {
if let Some(ref pb) = progress_clone {
pb.set_message(format!("• {}", crate_name));
} else {
println!("[mitts::compile] • {}", crate_name);
}
}
BuildProgressEvent::UnitFinished => {
if let Some(ref pb) = progress_clone {
if pb.position() >= pb.length().unwrap_or(0) {
pb.set_length(pb.position() + 1);
}
pb.inc(1);
}
}
BuildProgressEvent::CompilerMessage(rendered) => {
if let Some(ref pb) = progress_clone {
if pb.is_hidden() {
if rendered.ends_with('\n') {
eprint!("{}", rendered);
} else {
eprintln!("{}", rendered);
}
} else {
pb.println(rendered);
}
} else if rendered.ends_with('\n') {
eprint!("{}", rendered);
} else {
eprintln!("{}", rendered);
}
}
BuildProgressEvent::Diagnostic(line) => {
if let Some(ref pb) = progress_clone {
if pb.is_hidden() {
if line.ends_with('\n') {
eprint!("{}", line);
} else {
eprintln!("{}", line);
}
} else {
pb.println(line);
}
} else if line.ends_with('\n') {
eprint!("{}", line);
} else {
eprintln!("{}", line);
}
}
}))
}
pub fn render_build_result(
plan: &crate::core::BuildPlan,
result: &crate::core::ActionResult,
profile: &str,
start: std::time::Instant,
is_verbose: bool,
) -> i32 {
let dur = format!("{:.2}s", start.elapsed().as_secs_f64());
if result.success {
Ui::stage_start("🔨", &format!("Compiled target [{}]", profile));
if result.cached {
if result.action_id == "fast-lock-hit" {
Ui::stage_item(
plan.actions.is_empty(),
&Ui::badge_ac_hit(),
"Fast build lock verified: 0 project changes detected",
);
if let Some(bin) = plan.actions.first() {
Ui::stage_item(true, &Ui::badge_cached(), &bin.description);
}
Ui::finish(profile, &dur, "Up-to-date (no-op)");
} else {
Ui::stage_item(
true,
&Ui::badge_ac_hit(),
&format!("Restored {} actions from Action Cache", plan.actions.len()),
);
Ui::finish(profile, &dur, "100% cache hit, 0 local target bloat");
}
} else if plan.actions.len() <= 3 {
for (i, action) in plan.actions.iter().enumerate() {
let is_last = i == plan.actions.len() - 1;
Ui::stage_item(is_last, &Ui::badge_compile(), &action.description);
}
Ui::finish(
profile,
&dur,
&format!("{} actions executed into CAS", plan.actions.len()),
);
} else {
let root_bins: Vec<_> = plan
.actions
.iter()
.filter(|a| a.kind == ActionKind::CompileBin)
.collect();
let dep_count = plan.actions.len().saturating_sub(root_bins.len());
if let Some(bin) = root_bins.first() {
Ui::stage_item(
false,
&Ui::badge_compile(),
&format!(
"Compiled {} actions into CAS ({} dependencies)",
plan.actions.len(),
dep_count
),
);
Ui::stage_item(true, &Ui::badge_compile(), &bin.description);
} else {
Ui::stage_item(
true,
&Ui::badge_compile(),
&format!("Compiled {} actions into CAS", plan.actions.len()),
);
}
Ui::finish(
profile,
&dur,
&format!("{} actions executed into CAS", plan.actions.len()),
);
}
0
} else {
render_compiler_diagnostics(&result.stderr, is_verbose);
result.exit_code
}
}
pub fn handle_build(args: &BuildArgs) -> i32 {
if !args.config.is_empty() {
eprintln!("Error: Unsupported option: --config is not yet supported in mitts");
return 1;
}
let mut opt = to_rust_options(args);
let start = std::time::Instant::now();
let profile = if opt.release { "release" } else { "dev" };
let is_verbose = opt.verbose > 0 || std::env::var("MITTS_VERBOSE").is_ok();
Ui::header(&format!("build [{}]", profile));
let term_guard = TerminalGuard::acquire();
let progress = if !is_verbose {
Some(Ui::create_build_progress(profile))
} else {
None
};
opt.progress = Some(create_progress_callback(progress.clone()));
let build_res = RustAdapter::build(&opt);
if let Some(ref pb) = progress {
pb.finish_and_clear();
}
drop(term_guard);
match build_res {
Ok((plan, result)) => render_build_result(&plan, &result, profile, start, is_verbose),
Err(err) => {
Ui::report_error(&err);
1
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::action::{Action, ActionKind, ActionResult, BuildPlan, ToolchainInfo};
use std::collections::HashMap;
use std::path::PathBuf;
#[test]
fn test_progress_callback_all_events() {
let cb_none = create_progress_callback(None);
cb_none.0(BuildProgressEvent::InitTotal {
total: 5,
cached: 2,
});
cb_none.0(BuildProgressEvent::Compiling("my_crate".to_string()));
cb_none.0(BuildProgressEvent::UnitFinished);
cb_none.0(BuildProgressEvent::CompilerMessage("msg".to_string()));
cb_none.0(BuildProgressEvent::Diagnostic("diag".to_string()));
let pb = Ui::create_build_progress("dev");
let cb_some = create_progress_callback(Some(pb.clone()));
cb_some.0(BuildProgressEvent::InitTotal {
total: 5,
cached: 2,
});
cb_some.0(BuildProgressEvent::Compiling("my_crate".to_string()));
cb_some.0(BuildProgressEvent::UnitFinished);
cb_some.0(BuildProgressEvent::CompilerMessage("msg".to_string()));
cb_some.0(BuildProgressEvent::Diagnostic("diag".to_string()));
pb.finish_and_clear();
}
#[test]
fn test_render_build_result_branches() {
let dummy_tc = ToolchainInfo {
name: "rustc".to_string(),
version: "1.80.0".to_string(),
target_triple: None,
};
let make_action = |id: &str, kind: ActionKind| Action {
id: id.to_string(),
kind,
description: format!("action {}", id),
toolchain: dummy_tc.clone(),
inputs: Vec::new(),
outputs: vec![PathBuf::from(format!("/out/{}", id))],
args: Vec::new(),
env: HashMap::new(),
dependencies: Vec::new(),
};
let now = std::time::Instant::now();
let plan_fast = BuildPlan {
actions: vec![make_action("root-bin", ActionKind::CompileBin)],
target_directory: PathBuf::from("/target"),
artifacts: Default::default(),
unit_cfgs: Default::default(),
..Default::default()
};
let res_fast = ActionResult {
action_id: "fast-lock-hit".to_string(),
exit_code: 0,
success: true,
stdout: String::new(),
stderr: String::new(),
cached: true,
};
assert_eq!(
render_build_result(&plan_fast, &res_fast, "dev", now, false),
0
);
let res_cached = ActionResult {
action_id: "ac-hit".to_string(),
exit_code: 0,
success: true,
stdout: String::new(),
stderr: String::new(),
cached: true,
};
assert_eq!(
render_build_result(&plan_fast, &res_cached, "dev", now, false),
0
);
let res_exec = ActionResult {
action_id: "exec".to_string(),
exit_code: 0,
success: true,
stdout: String::new(),
stderr: String::new(),
cached: false,
};
assert_eq!(
render_build_result(&plan_fast, &res_exec, "dev", now, false),
0
);
let plan_many = BuildPlan {
actions: vec![
make_action("dep1", ActionKind::CompileRlib),
make_action("dep2", ActionKind::CompileRlib),
make_action("dep3", ActionKind::CompileRlib),
make_action("root", ActionKind::CompileBin),
],
target_directory: PathBuf::from("/target"),
artifacts: Default::default(),
unit_cfgs: Default::default(),
..Default::default()
};
assert_eq!(
render_build_result(&plan_many, &res_exec, "dev", now, false),
0
);
let plan_libs = BuildPlan {
actions: vec![
make_action("dep1", ActionKind::CompileRlib),
make_action("dep2", ActionKind::CompileRlib),
make_action("dep3", ActionKind::CompileRlib),
make_action("dep4", ActionKind::CompileRlib),
],
target_directory: PathBuf::from("/target"),
artifacts: Default::default(),
unit_cfgs: Default::default(),
..Default::default()
};
assert_eq!(
render_build_result(&plan_libs, &res_exec, "dev", now, false),
0
);
let res_fail = ActionResult {
action_id: "fail".to_string(),
exit_code: 101,
success: false,
stdout: String::new(),
stderr: "error: test failure\n".to_string(),
cached: false,
};
assert_eq!(
render_build_result(&plan_fast, &res_fail, "dev", now, true),
101
);
}
}