use std::collections::hash_map::DefaultHasher;
use std::env;
use std::ffi::OsString;
use std::fs;
use std::hash::Hash;
use std::hash::Hasher;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process;
use std::process::Command;
use std::process::ExitStatus;
use std::process::Stdio;
use std::time::Duration;
use std::time::Instant;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use serde_json::to_string;
use super::stderr;
use crate::compiler::constants::CARGO_BIN;
use crate::compiler::constants::CARGO_FLAG_ALL_FEATURES;
use crate::compiler::constants::CARGO_FLAG_ALL_TARGETS;
use crate::compiler::constants::CARGO_FLAG_ALLOW_DIRTY;
use crate::compiler::constants::CARGO_FLAG_ALLOW_STAGED;
use crate::compiler::constants::CARGO_FLAG_FEATURES;
use crate::compiler::constants::CARGO_FLAG_NO_DEFAULT_FEATURES;
use crate::compiler::constants::CARGO_FLAG_TESTS;
use crate::compiler::constants::CARGO_SUBCOMMAND_CHECK;
use crate::compiler::constants::CARGO_SUBCOMMAND_FIX;
use crate::compiler::constants::CONFIG_FINGERPRINT_ENV;
use crate::compiler::constants::CONFIG_JSON_ENV;
use crate::compiler::constants::CONFIG_ROOT_ENV;
use crate::compiler::constants::DRIVER_ENV;
use crate::compiler::constants::DRIVER_ENV_ENABLED;
use crate::compiler::constants::FINDINGS_DIR_ENV;
use crate::compiler::constants::PASSTHROUGH_RUSTC_WRAPPER_ENV;
use crate::compiler::constants::RUSTC_WORKSPACE_WRAPPER_ENV;
use crate::compiler::constants::SCOPE_FINGERPRINT_ENV;
use crate::compiler::constants::WRAPPER_ALIAS_STAGING_EXTENSION;
use crate::compiler::constants::WRAPPER_DIR_NAME;
use crate::compiler::persistence;
use crate::compiler::persistence::AnalysisEvidence;
use crate::compiler::settings;
use crate::config::LoadedConfig;
use crate::constants::FINGERPRINT_HEX_WIDTH;
use crate::reporting::AllFeaturesCoverage;
use crate::reporting::AnalysisFailure;
use crate::reporting::CARGO_TERM_COLOR_ALWAYS;
use crate::reporting::CARGO_TERM_COLOR_ENV;
use crate::reporting::ColorMode;
use crate::reporting::CompilerFailureCause;
use crate::reporting::CompilerWarningFacts;
use crate::reporting::MendFailure;
use crate::reporting::Report;
use crate::selection::CargoCheckPlan;
use crate::selection::Selection;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BuildOutputMode {
Full,
Json,
SuppressUnusedImportWarnings,
Quiet,
}
#[derive(Debug, Clone, Copy)]
struct CommandOutcome {
exit_status: ExitStatus,
compiler_warning_facts: CompilerWarningFacts,
duration: Duration,
compiler_warnings: usize,
compiler_fixable: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CargoFixFeatures {
All,
Plan,
}
pub(crate) struct SelectionResult {
pub report: Report,
pub check_duration: Duration,
pub compiler_warnings: usize,
pub compiler_fixable: usize,
}
pub(crate) fn run_selection(
selection: &Selection,
cargo_plan: &CargoCheckPlan,
loaded_config: &LoadedConfig,
output_mode: BuildOutputMode,
color_mode: ColorMode,
) -> Result<SelectionResult, MendFailure> {
let findings_dir = persistence::prepare_findings_dir(cargo_plan.target_directory.as_path())
.map_err(|err| {
MendFailure::Analysis(AnalysisFailure {
cause: CompilerFailureCause::DriverSetup(err),
})
})?;
let scope_fingerprint = scope_fingerprint_for(cargo_plan);
let command_outcome = run_cargo_check(
cargo_plan,
loaded_config,
&findings_dir,
&scope_fingerprint,
output_mode,
color_mode,
)
.map_err(|err| {
MendFailure::Analysis(AnalysisFailure {
cause: CompilerFailureCause::DriverSetup(err),
})
})?;
if !command_outcome.exit_status.success() {
return Err(MendFailure::Analysis(AnalysisFailure {
cause: CompilerFailureCause::CargoCheck,
}));
}
let loaded = persistence::load_report(&findings_dir, selection, &loaded_config.fingerprint)
.map_err(|err| {
MendFailure::Analysis(AnalysisFailure {
cause: CompilerFailureCause::DriverExecution(err),
})
})?;
match loaded.analysis_evidence {
AnalysisEvidence::Absent => {
return Err(MendFailure::Analysis(AnalysisFailure {
cause: CompilerFailureCause::NoAnalysisProduced,
}));
},
AnalysisEvidence::Present => {},
}
let mut report = loaded.report;
report.facts.compiler_warning_facts = command_outcome.compiler_warning_facts;
Ok(SelectionResult {
report,
check_duration: command_outcome.duration,
compiler_warnings: command_outcome.compiler_warnings,
compiler_fixable: command_outcome.compiler_fixable,
})
}
fn run_cargo_check(
cargo_plan: &CargoCheckPlan,
loaded_config: &LoadedConfig,
findings_dir: &Path,
scope_fingerprint: &str,
output_mode: BuildOutputMode,
color_mode: ColorMode,
) -> Result<CommandOutcome> {
let current_exe = env::current_exe().context("failed to determine current executable path")?;
let mut command = Command::new(CARGO_BIN);
command.arg(CARGO_SUBCOMMAND_CHECK);
command.args(&cargo_plan.cargo_args);
let wrapper =
wrapper_alias(cargo_plan.target_directory.as_path(), ¤t_exe).unwrap_or(current_exe);
configure_rustc_wrapper(&mut command, &wrapper);
command
.env(DRIVER_ENV, DRIVER_ENV_ENABLED)
.env(CONFIG_ROOT_ENV, &loaded_config.root)
.env(
CONFIG_JSON_ENV,
to_string(&loaded_config.visibility_config)
.context("failed to serialize mend config for compiler driver")?,
)
.env(CONFIG_FINGERPRINT_ENV, &loaded_config.fingerprint)
.env(FINDINGS_DIR_ENV, findings_dir)
.env(SCOPE_FINGERPRINT_ENV, scope_fingerprint)
.stdin(Stdio::inherit());
run_cargo_command(&mut command, output_mode, color_mode, findings_dir)
.context("failed to run cargo check for mend")
}
fn wrapper_alias(target_directory: &Path, current_exe: &Path) -> Option<PathBuf> {
let alias_dir = target_directory
.join(WRAPPER_DIR_NAME)
.join(settings::current_analysis_fingerprint());
fs::create_dir_all(&alias_dir).ok()?;
let alias = alias_dir.join(current_exe.file_name()?);
if alias_points_at(&alias, current_exe) {
return Some(alias);
}
replace_wrapper_alias(current_exe, &alias).ok()?;
Some(alias)
}
fn alias_points_at(alias: &Path, current_exe: &Path) -> bool {
let Ok(resolved) = fs::canonicalize(alias) else {
return false;
};
fs::canonicalize(current_exe).is_ok_and(|target| resolved == target)
}
fn replace_wrapper_alias(current_exe: &Path, alias: &Path) -> io::Result<()> {
let staged = alias.with_extension(format!(
"{WRAPPER_ALIAS_STAGING_EXTENSION}-{}",
process::id()
));
drop(fs::remove_file(&staged));
link_wrapper_alias(current_exe, &staged)?;
fs::rename(&staged, alias).inspect_err(|_| drop(fs::remove_file(&staged)))
}
#[cfg(unix)]
fn link_wrapper_alias(current_exe: &Path, alias: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(current_exe, alias)
}
#[cfg(not(unix))]
fn link_wrapper_alias(current_exe: &Path, alias: &Path) -> io::Result<()> {
fs::copy(current_exe, alias).map(drop)
}
fn configure_rustc_wrapper(command: &mut Command, current_exe: &Path) {
command
.env(RUSTC_WORKSPACE_WRAPPER_ENV, current_exe)
.env_remove(PASSTHROUGH_RUSTC_WRAPPER_ENV);
}
fn scope_fingerprint_for(cargo_plan: &CargoCheckPlan) -> String {
let mut hasher = DefaultHasher::new();
cargo_plan.manifest_path.hash(&mut hasher);
for arg in &cargo_plan.cargo_args {
arg.hash(&mut hasher);
}
format!(
"{:0width$x}",
hasher.finish(),
width = FINGERPRINT_HEX_WIDTH
)
}
pub(crate) fn run_cargo_fix(
cargo_plan: &CargoCheckPlan,
color_mode: ColorMode,
all_features_coverage: AllFeaturesCoverage,
) -> Result<Duration> {
let start = Instant::now();
let cargo_fix_features = cargo_fix_features_for(&cargo_plan.cargo_args, all_features_coverage);
let status = run_cargo_fix_command(cargo_plan, color_mode, cargo_fix_features)?;
if status.success() {
return Ok(start.elapsed());
}
if cargo_fix_features == CargoFixFeatures::All {
eprintln!(
"cargo fix with --all-features exited with {status}; retrying without --all-features"
);
let retry_status = run_cargo_fix_command(cargo_plan, color_mode, CargoFixFeatures::Plan)?;
if retry_status.success() {
return Ok(start.elapsed());
}
}
bail!("cargo fix failed");
}
fn cargo_fix_features_for(
cargo_args: &[OsString],
all_features_coverage: AllFeaturesCoverage,
) -> CargoFixFeatures {
match (
has_explicit_feature_selection(cargo_args),
all_features_coverage,
) {
(false, AllFeaturesCoverage::Superset) => CargoFixFeatures::All,
(true, _) | (false, AllFeaturesCoverage::NotGuaranteed) => CargoFixFeatures::Plan,
}
}
fn run_cargo_fix_command(
cargo_plan: &CargoCheckPlan,
color_mode: ColorMode,
cargo_fix_features: CargoFixFeatures,
) -> Result<ExitStatus> {
let mut command = Command::new(CARGO_BIN);
command
.arg(CARGO_SUBCOMMAND_FIX)
.arg(CARGO_FLAG_ALLOW_DIRTY)
.arg(CARGO_FLAG_ALLOW_STAGED);
for arg in &cargo_plan.cargo_args {
if arg == CARGO_FLAG_ALL_TARGETS {
command.arg(CARGO_FLAG_TESTS);
} else {
command.arg(arg);
}
}
if cargo_fix_features == CargoFixFeatures::All {
command.arg(CARGO_FLAG_ALL_FEATURES);
}
if color_mode.is_enabled() {
command.env(CARGO_TERM_COLOR_ENV, CARGO_TERM_COLOR_ALWAYS);
}
command
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.context("failed to run cargo fix")
}
fn has_explicit_feature_selection(cargo_args: &[OsString]) -> bool {
cargo_args.iter().any(|arg| {
arg == CARGO_FLAG_ALL_FEATURES
|| arg == CARGO_FLAG_FEATURES
|| arg == CARGO_FLAG_NO_DEFAULT_FEATURES
|| arg
.to_str()
.and_then(|arg| arg.strip_prefix(CARGO_FLAG_FEATURES))
.is_some_and(|suffix| suffix.starts_with('='))
})
}
fn run_cargo_command(
command: &mut Command,
output_mode: BuildOutputMode,
color_mode: ColorMode,
findings_dir: &Path,
) -> Result<CommandOutcome> {
if color_mode.is_enabled() {
command.env(CARGO_TERM_COLOR_ENV, CARGO_TERM_COLOR_ALWAYS);
}
command.stdin(Stdio::inherit());
command.stderr(Stdio::piped());
match output_mode {
BuildOutputMode::Full => command.stdout(Stdio::inherit()),
BuildOutputMode::Json
| BuildOutputMode::SuppressUnusedImportWarnings
| BuildOutputMode::Quiet => command.stdout(Stdio::null()),
};
let start = Instant::now();
let mut child = command.spawn().context("failed to spawn cargo command")?;
let stderr = child
.stderr
.take()
.context("failed to capture cargo stderr")?;
let stderr_outcome = stderr::stream_cargo_stderr(stderr, output_mode, findings_dir)?;
let exit_status = child.wait().context("failed to wait for cargo command")?;
let duration = start.elapsed();
Ok(CommandOutcome {
exit_status,
compiler_warning_facts: stderr_outcome.compiler_warning_facts,
duration,
compiler_warnings: stderr_outcome.warning_count,
compiler_fixable: stderr_outcome.fixable_count,
})
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use super::has_explicit_feature_selection;
use crate::compiler::constants::CARGO_FLAG_ALL_FEATURES;
use crate::compiler::constants::CARGO_FLAG_FEATURES;
use crate::compiler::constants::CARGO_FLAG_MANIFEST_PATH;
use crate::compiler::constants::CARGO_FLAG_NO_DEFAULT_FEATURES;
#[test]
fn detects_explicit_cargo_feature_selections() {
let selections = [
vec![
OsString::from(CARGO_FLAG_FEATURES),
OsString::from("hidden"),
],
vec![OsString::from(format!("{CARGO_FLAG_FEATURES}=hidden"))],
vec![OsString::from(CARGO_FLAG_ALL_FEATURES)],
vec![OsString::from(CARGO_FLAG_NO_DEFAULT_FEATURES)],
];
for cargo_args in selections {
assert!(has_explicit_feature_selection(&cargo_args));
}
}
#[test]
fn ignores_cargo_arguments_without_feature_selection() {
let cargo_args = vec![
OsString::from(CARGO_FLAG_MANIFEST_PATH),
OsString::from("Cargo.toml"),
];
assert!(!has_explicit_feature_selection(&cargo_args));
}
}