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> {
if let Some(exe) = std::env::var_os("RUST_MIR2_WRAPPER_BIN").map(PathBuf::from) {
if exe.is_file() {
return Ok(exe);
}
bail!(
"RUST_MIR2_WRAPPER_BIN points to `{}`, but it is not a file",
exe.display()
);
}
if let Some(exe) = std::env::var_os("CARGO_BIN_EXE_rust_mir2-wrapper").map(PathBuf::from) {
if exe.is_file() {
return Ok(exe);
}
}
if let Some(found) = find_wrapper_on_path() {
return Ok(found);
}
let current = std::env::current_exe().context("failed to locate current executable")?;
let mut search_dirs = Vec::new();
if let Some(parent) = current.parent() {
search_dirs.push(parent.to_path_buf());
if let Some(grand_parent) = parent.parent() {
search_dirs.push(grand_parent.to_path_buf());
}
}
for dir in &search_dirs {
for name in binary_candidate_names() {
let exact = dir.join(name);
if exact.is_file() {
return Ok(exact);
}
}
}
for dir in &search_dirs {
if let Some(found) = find_wrapper_in_dir(dir) {
return Ok(found);
}
}
bail!(
"failed to locate rust_mir2-wrapper binary near `{}` or on PATH; install `rust_mir2_wrapper` or set RUST_MIR2_WRAPPER_BIN",
current.display()
)
}
fn find_wrapper_on_path() -> Option<PathBuf> {
let paths = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&paths) {
for name in binary_candidate_names() {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
}
None
}
fn binary_candidate_names() -> &'static [&'static str] {
if cfg!(target_os = "windows") {
&["rust_mir2-wrapper.exe", "rust_mir2_wrapper.exe"]
} else {
&["rust_mir2-wrapper", "rust_mir2_wrapper"]
}
}
fn find_wrapper_in_dir(dir: &Path) -> Option<PathBuf> {
let mut candidates = std::fs::read_dir(dir)
.ok()?
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.is_file())
.filter(|path| {
path.file_name()
.and_then(OsStr::to_str)
.is_some_and(|name| {
name.starts_with("rust_mir2-wrapper") || name.starts_with("rust_mir2_wrapper")
})
})
.collect::<Vec<_>>();
candidates.sort();
candidates.into_iter().next()
}
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)
}