use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, bail};
use crate::fragments::load_fragments_from_dir;
use crate::manifest::try_resolve_target_project;
use crate::model::{
EXPECTED_PACKAGES_ENV, InputKind, InvocationMode, OUTPUT_DIR_ENV, RustMir2Error, TargetProject,
WRAPPER_MODE_ENV, WorkspaceMirDump,
};
use crate::render::build_packages_from_targets;
pub fn detect_invocation_mode(args: &[String], forced_mode: Option<&str>) -> InvocationMode {
if matches!(forced_mode, Some("workspace-wrapper")) {
return InvocationMode::WorkspaceWrapper;
}
if args.get(1).is_some_and(|arg| {
let path = Path::new(arg);
path.file_stem()
.and_then(OsStr::to_str)
.is_some_and(|stem| stem == "rustc")
}) {
return InvocationMode::WorkspaceWrapper;
}
InvocationMode::Library
}
fn wrapper_binary_path() -> anyhow::Result<PathBuf> {
let Some(exe) = std::env::var_os("RUST_MIR2_WRAPPER_BIN").map(PathBuf::from) else {
bail!(
"RUST_MIR2_WRAPPER_BIN is required; install the published rust_mir2_wrapper crate and set RUST_MIR2_WRAPPER_BIN to its rust_mir2-wrapper binary"
);
};
if exe.is_file() {
return Ok(exe);
}
bail!(
"RUST_MIR2_WRAPPER_BIN points to `{}`, but it is not a file",
exe.display()
);
}
fn run_cargo_check(target_project: &TargetProject, output_dir: &Path) -> anyhow::Result<()> {
let wrapper = wrapper_binary_path()?;
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let target_dir = output_dir.join("cargo-target");
let mut cmd = Command::new(cargo);
cmd.current_dir(&target_project.project_root)
.arg("check")
.arg("--all-targets")
.arg("--target-dir")
.arg(&target_dir);
match target_project.input_kind {
InputKind::Workspace => {
cmd.arg("--workspace");
}
InputKind::SingleCrate => {
cmd.arg("--package").arg(
target_project
.expected_package_names
.first()
.context("missing package name")?,
);
}
}
cmd.env("RUSTC_WORKSPACE_WRAPPER", &wrapper)
.env(WRAPPER_MODE_ENV, "workspace-wrapper")
.env(OUTPUT_DIR_ENV, output_dir)
.env(
EXPECTED_PACKAGES_ENV,
target_project.expected_package_names.join(","),
);
let status = cmd.status().context("failed to run cargo check")?;
if !status.success() {
bail!("cargo check failed with status {status}");
}
Ok(())
}
pub fn analyze_workspace(path: &Path) -> Result<WorkspaceMirDump, RustMir2Error> {
try_analyze_workspace(path).map_err(Into::into)
}
fn try_analyze_workspace(path: &Path) -> anyhow::Result<WorkspaceMirDump> {
let target_project = try_resolve_target_project(path)?;
let temp_dir = tempfile::tempdir().context("failed to create temporary output directory")?;
run_cargo_check(&target_project, temp_dir.path())?;
let fragments =
load_fragments_from_dir(temp_dir.path()).map_err(|error| anyhow::anyhow!(error.message))?;
if fragments.is_empty() {
bail!("rust_mir2 produced no target fragments");
}
let expected = target_project
.expected_package_names
.iter()
.cloned()
.collect::<BTreeSet<_>>();
let actual = fragments
.iter()
.map(|fragment| fragment.package_name.clone())
.collect::<BTreeSet<_>>();
for expected_name in &expected {
if !actual.contains(expected_name) {
bail!("missing fragment for local package `{expected_name}`");
}
}
for actual_name in &actual {
if !expected.contains(actual_name) {
bail!("unexpected fragment for non-target package `{actual_name}`");
}
}
Ok(WorkspaceMirDump {
project_path: target_project.project_root,
packages: build_packages_from_targets(fragments),
})
}
pub fn analyze_path(path: &Path) -> Result<WorkspaceMirDump, RustMir2Error> {
analyze_workspace(path)
}
#[cfg(test)]
mod tests {
use super::wrapper_binary_path;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn wrapper_binary_path_requires_explicit_env() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::remove_var("RUST_MIR2_WRAPPER_BIN");
}
let error = wrapper_binary_path().expect_err("missing env should fail");
assert!(
error
.to_string()
.contains("RUST_MIR2_WRAPPER_BIN is required")
);
}
#[test]
fn wrapper_binary_path_rejects_missing_file() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var(
"RUST_MIR2_WRAPPER_BIN",
"/definitely/not/a/rust_mir2-wrapper",
);
}
let error = wrapper_binary_path().expect_err("missing wrapper file should fail");
assert!(error.to_string().contains("but it is not a file"));
}
}