use crate::config::Config;
#[cfg(test)]
use crate::config::RetentionSettings;
#[cfg(test)]
use crate::util::workspace_root;
use eyre::Result;
#[cfg(test)]
use std::path::Path;
#[cfg(test)]
use std::path::PathBuf;
use std::process::ExitCode;
mod adopt;
mod analyze;
mod cache;
mod cargo;
mod cargo_invocation;
mod clean;
mod doctor;
mod edit;
mod exec;
mod explain;
mod gc;
pub(crate) use gc::{schedule_low_disk_sweep, spawn_collector_from};
pub mod launch;
mod mascot;
mod plain_progress;
mod prefetch;
mod pretty;
mod reserve;
mod settings;
mod setup;
mod shim;
mod stats;
pub mod test_runner;
mod tui;
const CARGO_SHIM_TARGET_FILE: &str = "mbx-target";
const RUST_ANALYZER_TARGET_DIR: &str = "target/rust-analyzer";
const CHECK_LANE_TARGET_DIR: &str = "target/check";
pub(crate) use cargo::{cargo_with_bypass_log, cargo_with_settings_and_bypass_log};
pub(crate) use setup::{cargo_shim_is_current, setup_install_dir};
pub use shim::{is_cargo_shim, run_cargo_shim};
pub(crate) fn cargo_activation_path() -> Option<std::ffi::OsString> {
shim::activation_path()
}
pub(crate) use shim::is_mise_command_wrapper_path;
#[cfg(test)]
use prefetch::validate_prefetch_config;
#[cfg(all(test, unix))]
pub(crate) use setup::CARGO_SHIM_LAUNCHER as DOCTOR_CARGO_SHIM_LAUNCHER;
#[cfg(test)]
pub(crate) use setup::{
MiseScope as DoctorMiseScope, SetupAction as DoctorSetupAction,
setup_at_action as doctor_setup_at_action,
};
#[cfg(test)]
use {cache::*, cargo::*, exec::*, gc::*, settings::*, setup::*};
#[derive(usage_rs::Cli)]
#[usage(completion = true)]
#[usage(
bin = "mbx",
version,
config = crate::config::RawConfig,
about = "A build cache for Rust projects",
long_about = "Enable mise's `mr_boxington` Rust option, or run `mbx setup` once, then keep using Cargo normally. Compiled work is shared across every checkout, and build storage prunes itself. Use mbx directly for its own commands, such as `tui`, `stats`, `cache`, `gc`, and `doctor`, or prefix Cargo commands with `mbx` for zero-config use.\n\nExamples:\n mbx setup\n cargo build --release\n cargo test --workspace\n cargo clippy --all-targets -- -D warnings\n mbx gc --dry-run",
unknown_flags = "error"
)]
struct Cli {
#[usage(sigil = "+", value_name = "TOOLCHAIN")]
toolchain: Option<String>,
#[usage(subcommand)]
command: Commands,
}
#[derive(usage_rs::Subcommands)]
enum Commands {
Completion {
#[usage(arg)]
shell: String,
},
Doctor(doctor::DoctorArgs),
Explain(explain::ExplainArgs),
Analyze(analyze::AnalyzeArgs),
Edit,
Setup(setup::SetupArgs),
Settings(settings::SettingsArgs),
Gc(gc::GcArgs),
Cache(cache::CacheArgs),
Clean(clean::CleanArgs),
Adopt(adopt::AdoptArgs),
Tui(tui::TuiArgs),
Stats(stats::StatsArgs),
Prefetch(prefetch::PrefetchArgs),
Exec(exec::ExecArgs),
Reserve(reserve::ReserveArgs),
#[usage(external_subcommand)]
Cargo(Vec<String>),
}
fn with_toolchain(toolchain: Option<&str>, arguments: Vec<String>) -> Vec<String> {
let Some(toolchain) = toolchain else {
return arguments;
};
std::iter::once(format!("+{toolchain}"))
.chain(arguments)
.collect()
}
fn compiles_nothing(command: &Commands) -> Option<&'static str> {
match command {
Commands::Completion { .. } => Some("completion"),
Commands::Edit => Some("edit"),
Commands::Setup(_) => Some("setup"),
Commands::Settings(_) => Some("settings"),
Commands::Gc(_) => Some("gc"),
Commands::Cache(_) => Some("cache"),
Commands::Clean(_) => Some("clean"),
Commands::Adopt(_) => Some("adopt"),
Commands::Tui(_) => Some("tui"),
Commands::Stats(_) => Some("stats"),
Commands::Analyze(_) => Some("analyze"),
Commands::Exec(_) => Some("exec"),
Commands::Reserve(_) => Some("reserve"),
Commands::Doctor(_) | Commands::Explain(_) | Commands::Prefetch(_) | Commands::Cargo(_) => {
None
}
}
}
pub fn run() -> Result<ExitCode> {
let original = std::env::args_os().collect::<Vec<_>>();
if let Some(answer) = Cli::completion_request(&original[1..]) {
print!("{answer}");
return Ok(ExitCode::SUCCESS);
}
let mut cli = Cli::parse();
let toolchain = cli.toolchain.take();
let toolchain = toolchain.as_deref();
if let Some(toolchain) = toolchain
&& let Some(command) = compiles_nothing(&cli.command)
{
eyre::bail!(
"+{toolchain} names a toolchain to compile with, and `mbx {command}` compiles nothing"
);
}
if let Commands::Prefetch(args) = &mut cli.command {
args.cargo_args = with_toolchain(toolchain, original_prefetch_arguments(&original)?);
}
if let Commands::Exec(args) = &mut cli.command {
args.command = original_exec_arguments(&original)?;
}
if let Commands::Reserve(args) = &mut cli.command {
args.command = original_reserve_arguments(&original)?;
}
if let Commands::Doctor(args) = &cli.command {
return doctor::run(args, toolchain);
}
if let Commands::Completion { shell } = &cli.command {
let shell = usage_rs::complete::Shell::from_name(shell)
.ok_or_else(|| eyre::eyre!("unsupported shell: {shell}"))?;
print!("{}", Cli::completion_script(shell));
return Ok(ExitCode::SUCCESS);
}
if let Commands::Edit = cli.command {
return edit::run();
}
if let Commands::Settings(args) = cli.command {
return settings::run(args);
}
let (config, settings) = Config::load_for_cli()?;
match cli.command {
Commands::Completion { .. } => {
unreachable!("completion was handled before configuration loading")
}
Commands::Doctor(_) => unreachable!("doctor was handled before configuration loading"),
Commands::Edit => unreachable!("edit was handled before configuration loading"),
Commands::Settings(_) => {
unreachable!("settings were handled before configuration loading")
}
Commands::Explain(args) => {
shim::prepare_explicit_cargo()?;
explain::run(&config, &settings, args, toolchain)
}
Commands::Analyze(args) => analyze::run(&config, args),
Commands::Setup(args) => setup::run(&args, args.action()?),
Commands::Gc(args) if args.automatic => {
if args.json || args.dry_run || args.max_size.is_some() {
eyre::bail!("--automatic runs the configured sweep and takes no other option");
}
gc::run_automatic(&config, &settings.retention).map(|()| ExitCode::SUCCESS)
}
Commands::Gc(args) => gc::run(
&config,
args.max_size
.map_or(config.gc.max_bytes, |requested| requested.as_u64()),
args.dry_run,
args.json,
&settings.retention,
)
.map(|()| ExitCode::SUCCESS),
Commands::Cache(args) => cache::run(&config, args.command),
Commands::Clean(args) => clean::run(&config, &args),
Commands::Adopt(args) => adopt::run(&config, &args),
Commands::Tui(args) => tui::run(
&config,
args,
settings.savings == crate::config::SavingsStyle::Quips,
),
Commands::Stats(args) => stats::run(&config, args, settings.savings),
Commands::Prefetch(args) => {
shim::prepare_explicit_cargo()?;
prefetch::run(&config, settings.events_max_size, &args.cargo_args)
}
Commands::Exec(args) => exec::run(&config, &settings, &args),
Commands::Reserve(args) => reserve::run(&config, args),
Commands::Cargo(arguments) => {
shim::prepare_explicit_cargo()?;
cargo::run(&config, &settings, &with_toolchain(toolchain, arguments))
}
}
}
fn original_exec_arguments(arguments: &[std::ffi::OsString]) -> Result<Vec<String>> {
let Some(index) = arguments
.iter()
.position(|argument| argument == std::ffi::OsStr::new("exec"))
else {
eyre::bail!("could not recover exec arguments");
};
let mut rest = &arguments[index + 1..];
while let Some(first) = rest.first().and_then(|argument| argument.to_str()) {
if first == "--project-root" {
rest = rest.get(2..).unwrap_or_default();
} else if first.starts_with("--project-root=") {
rest = &rest[1..];
} else {
break;
}
}
if rest.first().is_some_and(|argument| argument == "--") {
rest = &rest[1..];
}
strings(rest)
}
fn original_reserve_arguments(arguments: &[std::ffi::OsString]) -> Result<Vec<String>> {
let Some(index) = arguments
.iter()
.position(|argument| argument == std::ffi::OsStr::new("reserve"))
else {
eyre::bail!("could not recover reserve arguments");
};
let mut rest = &arguments[index + 1..];
while let Some(first) = rest.first().and_then(|argument| argument.to_str()) {
if matches!(first, "--cpus" | "--memory" | "--priority") {
rest = rest.get(2..).unwrap_or_default();
} else if first.starts_with("--cpus=")
|| first.starts_with("--memory=")
|| first.starts_with("--priority=")
{
rest = &rest[1..];
} else {
break;
}
}
if rest.first().is_some_and(|argument| argument == "--") {
rest = &rest[1..];
}
strings(rest)
}
fn original_prefetch_arguments(arguments: &[std::ffi::OsString]) -> Result<Vec<String>> {
let Some(index) = arguments
.iter()
.position(|argument| argument == std::ffi::OsStr::new("prefetch"))
else {
eyre::bail!("could not recover prefetch arguments");
};
strings(&arguments[index + 1..])
}
fn strings(arguments: &[std::ffi::OsString]) -> Result<Vec<String>> {
arguments
.iter()
.map(|argument| {
argument
.to_str()
.map(str::to_owned)
.ok_or_else(|| eyre::eyre!("Cargo arguments must be valid UTF-8"))
})
.collect()
}
#[cfg(test)]
mod adopt_tests;
#[cfg(test)]
mod cache_tests;
#[cfg(test)]
mod cargo_tests;
#[cfg(test)]
mod dispatch_tests;
#[cfg(test)]
mod exec_tests;
#[cfg(test)]
mod gc_tests;
#[cfg(test)]
mod prefetch_tests;
#[cfg(test)]
mod reserve_tests;
#[cfg(test)]
mod settings_tests;
#[cfg(test)]
mod setup_tests;