use super::gc::{schedule_sweep, take_sweep_report};
use crate::config::{CliSettings, Config, RetentionSettings, SummaryStyle};
use crate::session::{self, CacheSession};
use crate::{policy, target};
use bytesize::ByteSize;
use eyre::{Context, Result};
use mbx_cache_core::AgentStats;
use std::collections::BTreeMap;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
const NOTICE_STAMP: &str = "notice/v1/explained";
pub(super) fn run(
config: &Config,
settings: &CliSettings,
arguments: &[String],
) -> Result<ExitCode> {
cargo_with_settings_bypass_log_and_roots(config, settings, arguments, None, None)
}
pub(super) fn run_with_roots(
config: &Config,
settings: &CliSettings,
arguments: &[String],
roots: Roots,
) -> Result<ExitCode> {
cargo_with_settings_bypass_log_and_roots(config, settings, arguments, None, Some(roots))
}
pub(crate) fn cargo_with_bypass_log(
config: &Config,
arguments: &[String],
bypass_log: Option<&Path>,
) -> Result<ExitCode> {
cargo_with_settings_bypass_log_and_roots(
config,
&CliSettings::default(),
arguments,
bypass_log,
None,
)
}
pub(crate) fn cargo_with_settings_and_bypass_log(
config: &Config,
settings: &CliSettings,
arguments: &[String],
bypass_log: Option<&Path>,
) -> Result<ExitCode> {
cargo_with_settings_bypass_log_and_roots(config, settings, arguments, bypass_log, None)
}
fn cargo_with_settings_bypass_log_and_roots(
config: &Config,
settings: &CliSettings,
arguments: &[String],
bypass_log: Option<&Path>,
roots: Option<Roots>,
) -> Result<ExitCode> {
let retention = &settings.retention;
let summary = if cargo_is_quiet(arguments) {
SummaryStyle::Off
} else {
settings.summary
};
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
if super::launch::needs_plain_launch(arguments) {
return super::launch::plain_launch(&cargo, arguments);
}
let os_arguments = arguments
.iter()
.map(std::ffi::OsString::from)
.collect::<Vec<_>>();
if super::shim::cargo_proxy_passthrough(&os_arguments) {
return run_cargo(&cargo, arguments, BTreeMap::new());
}
let cache_links = settings.cache_links && session::cache_links_supported();
if settings.cache_links && !cache_links && std::env::var_os(session::CACHE_LINKS_ENV).is_some()
{
log::warn!("caching native links is not supported on this platform");
}
if cargo_help_requested(arguments) {
return run_cargo(&cargo, arguments, BTreeMap::new());
}
crate::storage::check_cache(config)?;
let working_dir = std::env::current_dir()?;
let mut roots = match roots {
Some(roots) => roots,
None => {
let target_dir_env = std::env::var_os(CARGO_TARGET_DIR_ENV);
match cargo_roots(&cargo, arguments, target_dir_env.as_deref()) {
Some(roots) => roots,
None => match super::shim::failed_probe(
&cargo,
&os_arguments,
target_dir_env.as_deref(),
) {
super::shim::FailedProbe::Roots(roots) => *roots,
super::shim::FailedProbe::Passthrough => {
return run_cargo(&cargo, arguments, BTreeMap::new());
}
super::shim::FailedProbe::Reject => eyre::bail!(
"could not verify Cargo build storage: metadata probing failed; run cargo metadata --no-deps --format-version 1 with the same manifest and configuration options to diagnose it"
),
},
}
}
};
let mut config = config.clone();
config.apply_workspace_policy(&roots.workspace_root)?;
let eager_incremental = config.eager_incremental && !config.verify;
let incremental = policy::incremental_allowed(config.incremental) && !eager_incremental;
if config.incremental && !incremental && !eager_incremental {
log::warn!(
"incremental compilation is disabled here; it needs an earlier build to build on"
);
}
if incremental && std::env::var("CARGO_INCREMENTAL").as_deref() == Ok("0") {
log::warn!(
"CARGO_INCREMENTAL=0 is set in the environment, so this build is not incremental after all"
);
}
config.incremental = incremental;
let learned_incremental = policy::learned_incremental_allowed(settings.learned_incremental)
&& !incremental
&& !config.verify
&& !eager_incremental;
let config = &config;
let require_local_build_dir = |build_dir: &Path| {
crate::storage::require_local(
build_dir,
"Cargo intermediate build directory",
"CARGO_BUILD_BUILD_DIR or build.build-dir",
)
};
let separate_build_dir = roots
.build_dir
.as_deref()
.filter(|build_dir| **build_dir != *roots.target_dir);
let nested_build_dir =
separate_build_dir.filter(|build_dir| build_dir.starts_with(&roots.target_dir));
if nested_build_dir.is_none()
&& let Some(build_dir) = separate_build_dir
{
require_local_build_dir(build_dir)?;
}
if std::fs::symlink_metadata(&roots.target_dir).is_ok_and(|metadata| metadata.is_dir()) {
crate::storage::require_local(
&roots.target_dir,
"Cargo target directory",
"CARGO_TARGET_DIR or build.target-dir",
)?;
}
let existing_target = manage_existing_target(config, &roots, arguments)?;
let default_target = roots.workspace_root.join("target");
let placing_editor = roots.target_dir_requested
&& roots.target_dir == roots.workspace_root.join(super::RUST_ANALYZER_TARGET_DIR);
let placement_candidate = if placing_editor {
target::placement_candidate(config, &roots.workspace_root, &default_target, false)
} else {
target::placement_candidate(
config,
&roots.workspace_root,
&roots.target_dir,
roots.target_dir_requested,
)
};
if existing_target.is_some() || placement_candidate {
crate::storage::require_local(
&config.target.root,
"managed target directory",
"MBX_TARGET_ROOT",
)?;
crate::storage::require_local(
&target::view_dir(&config.target.root, &roots.workspace_root),
"managed target directory",
"MBX_TARGET_ROOT",
)?;
}
let view_lease = if existing_target.is_some()
|| placement_candidate
|| target::is_recorded(&config.target.root, &roots.workspace_root)
{
match target::ViewLease::acquire(&config.target.root, &roots.workspace_root) {
Ok(lease) => Some(lease),
Err(error) => {
log::warn!(
"the managed target directory is not protected from collection: {error:#}"
);
None
}
}
} else {
None
};
let (placement, removed_target_bytes, adopted_target_bytes) = match existing_target {
Some(ExistingTarget::Adopt) => {
match target::adopt_existing(
config,
&roots.workspace_root,
&roots.target_dir,
roots.target_dir_requested,
) {
Ok(outcome) => {
let adopted = outcome.managed.is_some().then_some(outcome.adopted_bytes);
(
TargetViewPlacement {
directory: outcome.managed,
touch_path: roots.target_dir.clone(),
},
None,
adopted,
)
}
Err(error) => {
match adoption_failure(&error, &roots) {
AdoptionFailure::NotMoved => log::debug!("{error:#}"),
AdoptionFailure::LeftInPlace => log::warn!(
"{error:#}; the build continues in the existing target directory"
),
AdoptionFailure::Stranded => log::warn!("{error:#}"),
}
(place_target_view(config, &roots), None, None)
}
}
}
Some(ExistingTarget::Remove) => {
let outcome = target::migrate_existing(
config,
&roots.workspace_root,
&roots.target_dir,
roots.target_dir_requested,
)?;
(
TargetViewPlacement {
directory: outcome.managed,
touch_path: roots.target_dir.clone(),
},
outcome.removed_bytes,
None,
)
}
None => (place_target_view(config, &roots), None, None),
};
let check_lane = check_lane(config, &roots, &placement, &cargo, &working_dir, arguments);
let lane_arguments = check_lane
.as_ref()
.map(|lane| lane_cargo_arguments(arguments, lane));
if let Some(lane) = &check_lane {
roots.target_dir.clone_from(lane);
}
crate::storage::require_local(
&roots.target_dir,
"Cargo target directory",
"CARGO_TARGET_DIR or build.target-dir",
)?;
if let Some(build_dir) = nested_build_dir {
require_local_build_dir(build_dir)?;
}
let managed_linker =
crate::managed_linker::resolve(&config.linker, &config.cache_dir, &config.http, arguments)?;
if let Some(bytes) = removed_target_bytes {
crate::session::note(&format!(
"mbx[gc]: removed the existing target/ directory ({} logical)",
ByteSize::b(bytes).display().iec()
));
}
if let Some(bytes) = adopted_target_bytes {
crate::session::note(&format!(
"mbx[cache]: moved the existing target/ directory under the managed root ({} logical)",
ByteSize::b(bytes).display().iec()
));
}
if config.target.seed
&& let Some(view) = placement.directory.as_deref()
{
let lane = if placing_editor {
super::RUST_ANALYZER_TARGET_DIR
} else if check_lane.is_some() {
super::CHECK_LANE_TARGET_DIR
} else {
"target"
};
let lane = Path::new(lane)
.strip_prefix("target")
.unwrap_or(Path::new(""));
let view = if placing_editor {
target::view_dir(&config.target.root, &roots.workspace_root)
} else {
view.to_path_buf()
};
seed_target_view(
config,
&cargo,
&roots.workspace_root,
&view,
lane,
arguments,
);
}
if placement.directory.is_none() {
target::touch_managed(config, &roots.workspace_root, &placement.touch_path);
}
if !policy::is_ci() && !cargo_help_requested(arguments) && !was_explained(&config.store_dir()) {
let target_dir = placement.directory.as_deref().unwrap_or(&roots.target_dir);
let reflinks = crate::util::reflinks_work(&config.cache_dir, target_dir);
crate::session::note(&first_run_notice(config, retention, reflinks));
mark_explained(&config.store_dir());
}
let session_dir = tempfile::Builder::new().prefix("mbx-session-").tempdir()?;
let launch = super::launch::Launch::prepare(arguments, session_dir.path())?;
let test_runner = super::test_runner::TestRunner::prepare(
config,
arguments,
&roots.workspace_root,
session_dir.path(),
)?;
let expanded = super::cargo_invocation::expanded_arguments(&cargo, arguments)
.unwrap_or_else(|| arguments.to_vec());
let cargo_jobs = cargo_job_limit(&expanded);
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let session_outcome = runtime.block_on(async {
let session = match CacheSession::start_with_jobs(
session_dir.path(),
config,
cargo_jobs,
settings.events_max_size,
settings.retention.min_free,
)
.await
{
Ok(session) => session,
Err(error) if session::listener_unavailable(&error) => {
crate::session::note(&format!(
"mbx[warning]: cache session unavailable; running Cargo without mbx caching: {error:#}"
));
let mut environment = BTreeMap::from([
("MBX_DISABLE".into(), "1".into()),
(session::SOCKET_ENV.into(), String::new()),
]);
if settings.plain_output {
environment.insert("CARGO_TERM_PROGRESS_WHEN".into(), "never".into());
}
let fallback = lane_arguments.as_deref().unwrap_or(arguments);
return Ok((run_cargo(&cargo, fallback, environment), None));
}
Err(error) => return Err(error),
};
let mut environment = inherited_environment(|name| std::env::var(name).ok(), &working_dir);
let _lease = super::launch::lease(session_dir.path(), &mut environment)?;
if let Some(path) = bypass_log {
let path = if path.is_absolute() {
path.to_path_buf()
} else {
working_dir.join(path)
};
environment.insert(
crate::session::BYPASS_LOG_ENV.into(),
path.display().to_string(),
);
}
let run = session
.begin(
&roots.workspace_root,
&roots.target_dir,
arguments,
&mut environment,
)
.await;
environment.insert(session::MANAGED_LINKER_ENV.into(), String::new());
environment.insert(session::MANAGED_TARGET_LINKERS_ENV.into(), String::new());
if let Some(selection) = &managed_linker {
match selection {
crate::managed_linker::Selection::Single(executable) => {
environment.insert(session::MANAGED_LINKER_ENV.into(), executable.to_string_lossy().into_owned());
}
crate::managed_linker::Selection::Targets(executables) => {
environment.insert(session::MANAGED_TARGET_LINKERS_ENV.into(), serde_json::to_string(executables)?);
}
}
}
environment.insert(
session::EAGER_INCREMENTAL_ENV.into(),
if eager_incremental { "1" } else { "0" }.into(),
);
environment.insert(
session::LEARNED_INCREMENTAL_ENV.into(),
if learned_incremental { "1" } else { "0" }.into(),
);
environment.insert(
session::LEARNED_INCREMENTAL_MAX_SIZE_ENV.into(),
settings
.learned_incremental_max_size
.map_or_else(|| "none".to_string(), |bytes| bytes.to_string()),
);
environment.insert(
session::CACHE_LINKS_ENV.into(),
if cache_links { "1" } else { "0" }.into(),
);
environment.insert(
session::AR_DETERMINISM_ENV.into(),
config.ar_determinism.clone(),
);
if let Some(launch) = &launch {
launch.environment(&mut environment)?;
}
if let Some(test_runner) = &test_runner {
test_runner.environment(&mut environment)?;
}
if settings.plain_output {
environment.insert("CARGO_TERM_PROGRESS_WHEN".into(), "never".into());
}
super::launch::record_overlay(&mut environment)?;
let placed_arguments;
let cargo_arguments = if check_lane.is_some() {
placed_arguments = lane_cargo_arguments(arguments, &roots.target_dir);
&placed_arguments
} else if placement.directory.is_some() {
placed_arguments = placed_cargo_arguments(arguments, &roots.target_dir);
&placed_arguments
} else {
arguments
};
let status = if !settings.plain_output && super::pretty::enabled(arguments) {
match super::pretty::run(&cargo, cargo_arguments, &environment, settings.pretty_inspect, || session.progress_stats()) {
Ok(Some(status)) => Ok(status),
Ok(None) => run_cargo(&cargo, cargo_arguments, environment),
Err(error) => Err(error),
}
} else if super::plain_progress::eligible(arguments, std::env::var("CARGO_TERM_PROGRESS_WHEN").ok().as_deref())
&& !matches!(settings.summary, SummaryStyle::Off)
&& log::max_level() < log::LevelFilter::Debug
&& (settings.plain_output || !std::io::stderr().is_terminal())
{
super::plain_progress::run(&cargo, cargo_arguments, environment, || session.progress_stats())
} else {
run_cargo(&cargo, cargo_arguments, environment)
};
if let Some(run) = run
&& let Err(error) = run.commit().await
{
log::warn!("the completed build was not fully recorded: {error}");
}
let stats = match session.finish().await {
Ok(stats) => {
crate::session::display_stats(&stats, config, summary);
Some(stats)
}
Err(error) => {
log::warn!("the cache session did not shut down cleanly: {error}");
None
}
};
Ok((status, stats))
});
let session_outcome = match session_outcome {
Ok((Ok(status), stats))
if status == ExitCode::SUCCESS
&& launch.as_ref().is_some_and(|launch| launch.was_captured()) =>
{
Ok((launch.unwrap().run(), stats))
}
other => other,
};
drop(view_lease);
account_session(config, settings, session_outcome, removed_target_bytes)
}
pub(super) fn placed_cargo_arguments(arguments: &[String], target: &Path) -> Vec<String> {
let mut arguments = arguments.to_vec();
let index = arguments
.iter()
.take_while(|arg| arg.starts_with('+'))
.count();
arguments.splice(
index..index,
[
"--config".into(),
format!(
"build.target-dir={}",
toml::Value::String(target.to_string_lossy().into_owned())
),
],
);
arguments
}
pub(super) fn lane_cargo_arguments(arguments: &[String], lane: &Path) -> Vec<String> {
let mut arguments = arguments.to_vec();
let after_subcommand = super::launch::cargo_subcommand_at(&arguments)
.map_or(arguments.len(), |(index, _)| index + 1);
arguments.splice(
after_subcommand..after_subcommand,
["--target-dir".into(), lane.to_string_lossy().into_owned()],
);
arguments
}
pub(super) struct TargetViewPlacement {
pub(super) directory: Option<PathBuf>,
pub(super) touch_path: PathBuf,
}
pub(super) fn check_lane(
config: &Config,
roots: &Roots,
placement: &TargetViewPlacement,
cargo: &std::ffi::OsStr,
working_dir: &Path,
arguments: &[String],
) -> Option<PathBuf> {
let separate_build_dir = roots
.build_dir
.as_deref()
.is_some_and(|build_dir| *build_dir != *roots.target_dir);
if !(config.target.lanes
&& placement.directory.is_some()
&& !roots.target_dir_requested
&& !separate_build_dir
&& roots.target_dir == roots.workspace_root.join("target"))
|| build_dir_configured(working_dir, arguments)
{
return None;
}
let expanded = super::cargo_invocation::expanded_arguments(cargo, arguments)?;
(matches!(
super::launch::cargo_subcommand(&expanded),
Some("check" | "clippy")
) && !target_dir_named_in(&expanded))
.then(|| roots.workspace_root.join(super::CHECK_LANE_TARGET_DIR))
}
pub(super) fn target_dir_named_in(arguments: &[String]) -> bool {
arguments
.iter()
.take_while(|argument| *argument != "--")
.any(|argument| argument == "--target-dir" || argument.starts_with("--target-dir="))
}
pub(super) fn build_dir_configured(working_dir: &Path, arguments: &[String]) -> bool {
std::env::var_os("CARGO_BUILD_BUILD_DIR").is_some()
|| build_dir_named_in(arguments)
|| match cargo_config2::Config::load_with_cwd(invocation_dir(working_dir, arguments)) {
Ok(config) => config.build.build_dir.is_some(),
Err(_) => true,
}
}
pub(super) fn invocation_dir(working_dir: &Path, arguments: &[String]) -> PathBuf {
let mut directory = working_dir.to_path_buf();
let mut arguments = arguments.iter().take_while(|argument| *argument != "--");
while let Some(argument) = arguments.next() {
let moved = if argument == "-C" || argument == "--directory" {
arguments.next().map(String::as_str)
} else if let Some(value) = argument.strip_prefix("--directory=") {
Some(value)
} else {
argument
.strip_prefix("-C")
.map(|value| value.strip_prefix('=').unwrap_or(value))
};
if let Some(moved) = moved {
directory = directory.join(moved);
}
}
directory
}
pub(super) fn build_dir_named_in(arguments: &[String]) -> bool {
let mut arguments = arguments.iter().take_while(|argument| *argument != "--");
while let Some(argument) = arguments.next() {
let value = match argument.strip_prefix("--config=") {
Some(value) => value,
None if argument == "--config" => match arguments.next() {
Some(value) => value,
None => return false,
},
None => continue,
};
if value.contains("build-dir") || !value.contains('=') {
return true;
}
}
false
}
pub(super) fn place_target_view(config: &Config, roots: &Roots) -> TargetViewPlacement {
let default = roots.workspace_root.join("target");
if roots.target_dir_requested
&& roots.target_dir == roots.workspace_root.join(super::RUST_ANALYZER_TARGET_DIR)
{
let directory = target::place(config, &roots.workspace_root, &default, false)
.map(|_| roots.target_dir.clone());
return TargetViewPlacement {
directory,
touch_path: default,
};
}
TargetViewPlacement {
directory: target::place(
config,
&roots.workspace_root,
&roots.target_dir,
roots.target_dir_requested,
),
touch_path: roots.target_dir.clone(),
}
}
fn seed_target_view(
config: &Config,
cargo: &std::ffi::OsStr,
workspace_root: &Path,
view: &Path,
lane: &Path,
arguments: &[String],
) {
let profiles = crate::target_seed::profile_directories(arguments);
if profiles
.iter()
.all(|profile| view.join(lane).join(profile).join("build").exists())
{
return;
}
let donors = target::seed_donors(&config.target.root, workspace_root);
if donors.is_empty() {
return;
}
let Ok(lockfile) = std::fs::read_to_string(workspace_root.join("Cargo.lock")) else {
return;
};
let packages = crate::target_seed::registry_packages(&lockfile);
if packages.is_empty() {
return;
}
let mut version = Command::new(cargo);
if let Some(toolchain) = arguments
.first()
.filter(|argument| argument.starts_with('+'))
{
version.arg(toolchain);
}
let supported = version
.arg("-V")
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.is_some_and(|version| crate::target_seed::keeps_units_in_directories(&version));
if !supported {
return;
}
let outcome = crate::target_seed::seed(view, lane, &profiles, &packages, &donors);
if let Some(donor) = outcome.donor {
crate::session::note(&format!(
"mbx[target]: copied {} registry build units from {}",
outcome.units,
donor.display()
));
}
}
pub(super) fn account_session(
config: &Config,
settings: &CliSettings,
session_outcome: Result<(Result<ExitCode>, Option<AgentStats>)>,
removed_target_bytes: Option<u64>,
) -> Result<ExitCode> {
let retention = &settings.retention;
let (status, stats) = match session_outcome {
Ok((status, stats)) => (status, stats),
Err(error) => (Err(error), None),
};
for line in take_sweep_report(&config.store_dir()) {
session::note(&format!("mbx[gc]: {line}"));
}
let mut delta = crate::savings::Delta {
freed_requested_bytes: removed_target_bytes.unwrap_or_default(),
..crate::savings::Delta::default()
};
let facts = stats
.as_ref()
.map_or_else(crate::savings::SessionFacts::default, |stats| {
crate::savings::SessionFacts {
hits: stats.hits,
avoided_compiler_ns: stats.avoided_compiler_duration_ns,
}
});
if let Some(stats) = &stats {
if crate::session::session_was_active(stats) {
delta.builds = 1;
}
delta.cached_compilations = stats.hits;
delta.avoided_compiler_ns = stats.avoided_compiler_duration_ns;
delta.reflinked_bytes = stats
.reflinked_output_bytes
.saturating_add(stats.hardlinked_output_bytes);
}
if let Some(line) =
crate::savings::record_and_describe(&config.store_dir(), &delta, &facts, settings.savings)
{
crate::session::note(&line);
}
schedule_sweep(config, retention);
status
}
pub(super) fn was_explained(store: &Path) -> bool {
store.join(NOTICE_STAMP).exists()
}
pub(super) fn mark_explained(store: &Path) {
if let Err(error) = crate::util::write_atomic(&store.join(NOTICE_STAMP), b"") {
log::debug!("the first-run notice was not recorded: {error}");
}
}
pub(super) fn first_run_notice(
config: &Config,
retention: &RetentionSettings,
reflinks: bool,
) -> String {
let mut lines = vec!["mbx[setup]: first build on this machine".to_string()];
if config.gc.auto {
lines.push(format!(
"mbx[setup]: cache is {}, shared by every checkout and worktree, pruned to {}",
config.cache_dir.display(),
ByteSize::b(config.gc.max_bytes).display().iec(),
));
} else {
lines.push(format!(
"mbx[setup]: cache is {}, shared by every checkout and worktree; automatic collection is off, so only `mbx gc` reclaims it",
config.cache_dir.display(),
));
}
if reflinks {
lines.push(
"mbx[setup]: this filesystem supports reflinks, so target/ shares disk with the cache instead of copying"
.to_string(),
);
}
if config.target.views && config.gc.auto {
let mut reasons = vec!["its checkout is gone".to_string()];
if let Some(age) = retention.target_max_age {
reasons.push(format!("unused for {}", crate::util::format_span(age)));
}
if let Some(bytes) = retention.target_max_bytes {
reasons.push(format!("over {} total", ByteSize::b(bytes).display().iec()));
}
if let Some(bytes) = retention.max_total_bytes {
reasons.push(format!(
"combined managed data exceeds {} logical",
ByteSize::b(bytes).display().iec(),
));
}
if retention.min_free.is_some() {
reasons.push("the disk runs low".to_string());
}
lines.push(format!(
"mbx[setup]: target/ is managed: deleted when {}",
join_clauses(&reasons),
));
}
lines.push(
"mbx[setup]: `mbx gc --dry-run` previews cleanup; `mbx settings ls gc` shows every limit"
.to_string(),
);
lines.join("\n")
}
pub(super) fn join_clauses(clauses: &[String]) -> String {
match clauses {
[] => String::new(),
[only] => only.clone(),
[first, second] => format!("{first} or {second}"),
[rest @ .., last] => format!("{}, or {last}", rest.join(", ")),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum AdoptionFailure {
NotMoved,
LeftInPlace,
Stranded,
}
pub(super) fn adoption_failure(error: &eyre::Report, roots: &Roots) -> AdoptionFailure {
if error.downcast_ref::<target::StrandedAdoption>().is_some() {
AdoptionFailure::Stranded
} else if std::fs::symlink_metadata(&roots.target_dir).is_ok_and(|metadata| metadata.is_dir()) {
AdoptionFailure::LeftInPlace
} else {
AdoptionFailure::NotMoved
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ExistingTarget {
Adopt,
Remove,
}
pub(super) fn manage_existing_target(
config: &Config,
roots: &Roots,
arguments: &[String],
) -> Result<Option<ExistingTarget>> {
if cargo_help_requested(arguments) || policy::is_ci() {
return Ok(None);
}
manage_existing_target_with(config, roots, |directory| {
if !std::io::stdin().is_terminal() || !std::io::stderr().is_terminal() {
return Ok(false);
}
let description = format!(
"mbx can remove {} and replace it with a managed target that is pruned after this checkout is deleted. It cannot be moved there: the managed root is on another filesystem.",
directory.display()
);
match demand::Confirm::new("Use a managed target directory?")
.description(&description)
.affirmative("Remove target/")
.negative("Keep it")
.selected(false)
.run()
{
Ok(answer) => Ok(answer),
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => Ok(false),
Err(error) => Err(error.into()),
}
})
}
pub(super) fn cargo_help_requested(arguments: &[String]) -> bool {
arguments.first().is_some_and(|argument| argument == "help")
|| arguments
.iter()
.any(|argument| argument == "--help" || argument == "-h")
}
pub(super) fn manage_existing_target_with(
config: &Config,
roots: &Roots,
ask_to_remove: impl FnOnce(&Path) -> Result<bool>,
) -> Result<Option<ExistingTarget>> {
if !target::can_remove_existing(
config,
&roots.workspace_root,
&roots.target_dir,
roots.target_dir_requested,
) {
return Ok(None);
}
if target::can_move_existing(config, &roots.workspace_root, &roots.target_dir) {
return Ok(Some(ExistingTarget::Adopt));
}
Ok(ask_to_remove(&roots.target_dir)?.then_some(ExistingTarget::Remove))
}
pub(super) fn run_cargo(
cargo: &std::ffi::OsStr,
arguments: &[impl AsRef<std::ffi::OsStr>],
environment: BTreeMap<String, String>,
) -> Result<ExitCode> {
let mut command = Command::new(cargo);
command.args(arguments);
command.envs(environment);
let status = command
.status()
.wrap_err_with(|| format!("failed to run {}", cargo.to_string_lossy()))?;
Ok(exit_code(status))
}
pub(super) fn cargo_is_quiet(arguments: &[String]) -> bool {
arguments
.iter()
.take_while(|argument| argument.as_str() != "--")
.any(|argument| matches!(argument.as_str(), "-q" | "--quiet"))
}
#[cfg(unix)]
pub(super) fn exit_code(status: std::process::ExitStatus) -> ExitCode {
use std::os::unix::process::ExitStatusExt as _;
match (status.code(), status.signal()) {
(Some(code), _) => ExitCode::from(code as u8),
(None, Some(signal)) => ExitCode::from(128u8.saturating_add(signal as u8)),
(None, None) => ExitCode::FAILURE,
}
}
#[cfg(not(unix))]
pub(super) fn exit_code(status: std::process::ExitStatus) -> ExitCode {
match status.code() {
Some(code) => ExitCode::from(code as u8),
None => ExitCode::FAILURE,
}
}
#[derive(Debug, PartialEq, Eq)]
pub(super) struct Roots {
pub(super) workspace_root: PathBuf,
pub(super) target_dir: PathBuf,
pub(super) build_dir: Option<PathBuf>,
pub(super) target_dir_requested: bool,
}
pub(super) fn inherited_environment(
get_env: impl Fn(&str) -> Option<String>,
working_dir: &Path,
) -> BTreeMap<String, String> {
let mut environment = BTreeMap::new();
if let Some(wrapper) = get_env("RUSTC_WRAPPER").filter(|value| !value.is_empty()) {
environment.insert("RUSTC_WRAPPER".into(), wrapper);
}
if let Some(wrapper) = get_env("RUSTC_WORKSPACE_WRAPPER").filter(|value| !value.is_empty()) {
environment.insert("RUSTC_WORKSPACE_WRAPPER".into(), wrapper);
}
if let Some(log) = get_env(crate::session::BYPASS_LOG_ENV).filter(|value| !value.is_empty()) {
environment.insert(
crate::session::BYPASS_LOG_ENV.into(),
absolute(working_dir, &log).display().to_string(),
);
}
environment
}
pub(super) fn resolve_roots(
cargo: &std::ffi::OsStr,
arguments: &[String],
working_dir: &Path,
) -> Roots {
resolve_roots_with(
cargo,
arguments,
working_dir,
std::env::var_os(CARGO_TARGET_DIR_ENV),
)
}
pub(super) const CARGO_TARGET_DIR_ENV: &str = "CARGO_TARGET_DIR";
pub(super) fn resolve_roots_with(
cargo: &std::ffi::OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<std::ffi::OsString>,
) -> Roots {
let resolved = mbx_cache_cargo::resolve(cargo, arguments, working_dir, target_dir_env);
Roots {
workspace_root: resolved.workspace_root,
target_dir: resolved.target_dir,
build_dir: resolved.build_dir,
target_dir_requested: resolved.target_dir_requested,
}
}
pub(super) fn absolute(working_dir: &Path, value: &str) -> PathBuf {
let path = PathBuf::from(value);
if path.is_absolute() {
path
} else {
working_dir.join(path)
}
}
#[cfg(test)]
pub(super) const PROBE_GLOBAL_FLAGS: [&str; 3] = ["-C", "--config", "-Z"];
#[cfg(test)]
pub(super) fn forwarded_flags(arguments: &[String], flags: &[&str]) -> Vec<String> {
let mut forwarded = Vec::new();
let mut remaining = arguments.iter();
while let Some(argument) = remaining.next() {
if let Some((flag, value)) = argument
.split_once('=')
.filter(|(flag, _)| flags.contains(flag))
{
forwarded.push(flag.to_string());
forwarded.push(value.to_string());
} else if flags.contains(&argument.as_str()) {
if let Some(value) = remaining.next() {
forwarded.push(argument.clone());
forwarded.push(value.clone());
}
} else if flags
.iter()
.any(|flag| flag.len() == 2 && argument.len() > 2 && argument.starts_with(flag))
{
forwarded.push(argument.clone());
}
}
forwarded
}
pub(super) fn cargo_roots(
cargo: &std::ffi::OsStr,
arguments: &[String],
target_dir_env: Option<&std::ffi::OsStr>,
) -> Option<Roots> {
let working_dir = std::env::current_dir().ok()?;
let resolved = mbx_cache_cargo::resolve_reported(
cargo,
arguments,
&working_dir,
target_dir_env.map(std::ffi::OsStr::to_os_string),
)?;
Some(Roots {
workspace_root: resolved.workspace_root,
target_dir: resolved.target_dir,
build_dir: resolved.build_dir,
target_dir_requested: resolved.target_dir_requested,
})
}
#[cfg(test)]
pub(super) fn parse_cargo_roots(metadata: &[u8]) -> Option<Roots> {
let metadata: serde_json::Value = serde_json::from_slice(metadata).ok()?;
Some(Roots {
workspace_root: PathBuf::from(metadata.get("workspace_root")?.as_str()?),
target_dir: PathBuf::from(metadata.get("target_directory")?.as_str()?),
build_dir: metadata
.get("build_directory")
.and_then(|value| value.as_str())
.map(PathBuf::from),
target_dir_requested: false,
})
}
fn cargo_job_limit(arguments: &[String]) -> Option<u64> {
cargo_job_limit_with(
arguments,
std::env::var("CARGO_BUILD_JOBS").ok().as_deref(),
std::thread::available_parallelism().map_or(1, |cpus| cpus.get() as u64),
)
}
pub(super) fn cargo_job_limit_with(
arguments: &[String],
environment: Option<&str>,
logical_cpus: u64,
) -> Option<u64> {
let nextest = super::launch::cargo_subcommand(arguments) == Some("nextest");
let mut cli_value = None;
let mut cli_seen = false;
let mut index = 0;
while index < arguments.len() {
let argument = &arguments[index];
if argument == "--" {
break;
}
let value = if nextest {
if argument == "--build-jobs" {
index += 1;
arguments.get(index).map(String::as_str)
} else {
argument.strip_prefix("--build-jobs=")
}
} else if argument == "-j" || argument == "--jobs" {
index += 1;
arguments.get(index).map(String::as_str)
} else if let Some(value) = argument.strip_prefix("--jobs=") {
Some(value)
} else if let Some(value) = argument.strip_prefix("-j=") {
Some(value)
} else {
argument
.strip_prefix("-j")
.filter(|value| !value.is_empty())
};
if let Some(value) = value {
cli_seen = true;
cli_value = resolve_cargo_jobs(value, logical_cpus);
}
index += 1;
}
if cli_seen {
cli_value
} else {
environment.and_then(|value| resolve_cargo_jobs(value, logical_cpus))
}
}
fn resolve_cargo_jobs(value: &str, logical_cpus: u64) -> Option<u64> {
if value == "default" {
return None;
}
let jobs = value.parse::<i64>().ok()?;
if jobs > 0 {
return Some(jobs as u64);
}
if jobs < 0 {
return i128::from(logical_cpus)
.checked_add(i128::from(jobs))
.filter(|jobs| *jobs > 0)
.and_then(|jobs| u64::try_from(jobs).ok());
}
None
}