use std::path::Path;
use anyhow::{Context, bail};
use toml::Value;
use crate::model::{InputKind, RustMir2Error, TargetProject};
fn read_manifest(manifest_path: &Path) -> anyhow::Result<Value> {
let content = std::fs::read_to_string(manifest_path)
.with_context(|| format!("failed to read manifest `{}`", manifest_path.display()))?;
toml::from_str(&content)
.with_context(|| format!("failed to parse manifest `{}`", manifest_path.display()))
}
fn package_name_from_manifest(manifest: &Value) -> Option<String> {
manifest
.get("package")
.and_then(Value::as_table)
.and_then(|package| package.get("name"))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
}
fn workspace_members_from_manifest(manifest: &Value) -> Vec<String> {
manifest
.get("workspace")
.and_then(Value::as_table)
.and_then(|workspace| workspace.get("members"))
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_str)
.map(ToOwned::to_owned)
.collect()
}
pub fn resolve_target_project(input_path: &Path) -> Result<TargetProject, RustMir2Error> {
try_resolve_target_project(input_path).map_err(Into::into)
}
pub(crate) fn try_resolve_target_project(input_path: &Path) -> anyhow::Result<TargetProject> {
let canonical = input_path.canonicalize().with_context(|| {
format!(
"failed to canonicalize input path `{}`",
input_path.display()
)
})?;
let manifest_path = if canonical.is_file() {
if canonical
.file_name()
.is_some_and(|name| name == "Cargo.toml")
{
canonical.clone()
} else {
bail!("input file `{}` is not a Cargo.toml", canonical.display());
}
} else {
canonical.join("Cargo.toml")
};
if !manifest_path.is_file() {
bail!("no Cargo.toml found at `{}`", manifest_path.display());
}
let manifest = read_manifest(&manifest_path)?;
let manifest_dir = manifest_path
.parent()
.context("manifest path had no parent directory")?;
let package_name = package_name_from_manifest(&manifest);
let workspace_members = workspace_members_from_manifest(&manifest);
if !workspace_members.is_empty() {
let mut expected_package_names = workspace_members
.into_iter()
.map(|member| {
let member_manifest = manifest_dir.join(member).join("Cargo.toml");
let member_value = read_manifest(&member_manifest)?;
package_name_from_manifest(&member_value).with_context(|| {
format!(
"workspace member manifest `{}` has no package.name",
member_manifest.display()
)
})
})
.collect::<anyhow::Result<Vec<_>>>()?;
if let Some(package_name) = package_name {
expected_package_names.push(package_name);
}
expected_package_names.sort();
expected_package_names.dedup();
return Ok(TargetProject {
input_kind: InputKind::Workspace,
project_root: manifest_dir.to_path_buf(),
manifest_path,
expected_package_names,
});
}
let package_name = package_name.context("failed to resolve target package name")?;
Ok(TargetProject {
input_kind: InputKind::SingleCrate,
project_root: manifest_dir.to_path_buf(),
manifest_path,
expected_package_names: vec![package_name],
})
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
fn write(path: &Path, text: &str) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, text).unwrap();
}
#[test]
fn package_manifest_with_workspace_members_resolves_as_workspace() {
let temp = TempDir::new().unwrap();
write(
&temp.path().join("Cargo.toml"),
r#"
[package]
name = "root_pkg"
version = "0.1.0"
edition = "2024"
[workspace]
members = ["adder", "add-one", "test3"]
"#,
);
for (dir, name) in [
("adder", "adder"),
("add-one", "add-one"),
("test3", "test3"),
] {
write(
&temp.path().join(dir).join("Cargo.toml"),
&format!(
r#"
[package]
name = "{name}"
version = "0.1.0"
edition = "2024"
"#
),
);
}
let project = try_resolve_target_project(temp.path()).unwrap();
assert_eq!(project.input_kind, InputKind::Workspace);
assert_eq!(
project.expected_package_names,
vec!["add-one", "adder", "root_pkg", "test3"]
);
}
}