use std::path::Path;
use anyhow::{Context, bail};
use toml::Value;
use crate::model::{DefaultTarget, 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()
}
fn default_run_from_manifest(manifest: &Value) -> Option<String> {
manifest
.get("package")
.and_then(Value::as_table)
.and_then(|package| package.get("default-run"))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
}
fn lib_name_from_manifest(
manifest: &Value,
manifest_dir: &Path,
package_name: &str,
) -> Option<String> {
if let Some(lib) = manifest.get("lib").and_then(Value::as_table) {
return Some(
lib.get("name")
.and_then(Value::as_str)
.unwrap_or(package_name)
.to_string(),
);
}
manifest_dir
.join("src")
.join("lib.rs")
.is_file()
.then(|| package_name.to_string())
}
fn bin_names_from_manifest(
manifest: &Value,
manifest_dir: &Path,
package_name: &str,
) -> Vec<String> {
let mut bins = manifest
.get("bin")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|bin| {
bin.as_table()
.and_then(|table| table.get("name"))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>();
if manifest_dir.join("src").join("main.rs").is_file()
&& !bins.iter().any(|bin| bin == package_name)
{
bins.push(package_name.to_string());
}
bins.sort();
bins.dedup();
bins
}
fn default_targets_from_manifest(
manifest: &Value,
manifest_dir: &Path,
package_name: &str,
) -> Vec<DefaultTarget> {
let mut targets = Vec::new();
if let Some(lib_name) = lib_name_from_manifest(manifest, manifest_dir, package_name) {
let target_identity = format!("lib:{lib_name}:normal");
targets.push(DefaultTarget {
package_name: package_name.to_string(),
target_name: lib_name,
target_kind: "lib".to_string(),
target_identity,
});
}
let bin_names = bin_names_from_manifest(manifest, manifest_dir, package_name);
let default_bin = default_run_from_manifest(manifest)
.or_else(|| bin_names.iter().find(|bin| *bin == package_name).cloned())
.or_else(|| bin_names.first().cloned());
if let Some(bin_name) = default_bin {
targets.push(DefaultTarget {
package_name: package_name.to_string(),
target_name: bin_name.clone(),
target_kind: "bin".to_string(),
target_identity: format!("bin:{bin_name}:normal"),
});
}
targets
}
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")?
.to_path_buf();
let package_name = package_name_from_manifest(&manifest);
let workspace_members = workspace_members_from_manifest(&manifest);
if let Some(package_name) = package_name.clone() {
return Ok(TargetProject {
input_kind: InputKind::SingleCrate,
project_root: manifest_dir.clone(),
manifest_path,
expected_package_names: vec![package_name.clone()],
default_targets: default_targets_from_manifest(
&manifest,
&manifest_dir,
package_name.as_str(),
),
});
}
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,
manifest_path,
expected_package_names,
default_targets: Vec::new(),
});
}
bail!("failed to resolve target 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::SingleCrate);
assert_eq!(project.expected_package_names, vec!["root_pkg"]);
}
#[test]
fn root_package_name_wins_before_reading_workspace_members() {
let temp = TempDir::new().unwrap();
write(
&temp.path().join("Cargo.toml"),
r#"
[package]
name = "rustdesk"
version = "0.1.0"
edition = "2024"
[workspace]
members = ["rustdesk"]
"#,
);
let project = try_resolve_target_project(temp.path()).unwrap();
assert_eq!(project.input_kind, InputKind::SingleCrate);
assert_eq!(project.expected_package_names, vec!["rustdesk"]);
}
#[test]
fn bin_only_package_uses_package_name_for_implicit_main_bin() {
let temp = TempDir::new().unwrap();
write(
&temp.path().join("Cargo.toml"),
r#"
[package]
name = "minigrep_by_shyoy"
version = "0.1.0"
edition = "2024"
"#,
);
write(&temp.path().join("src/main.rs"), "fn main() {}\n");
let project = try_resolve_target_project(temp.path()).unwrap();
assert_eq!(project.default_targets.len(), 1);
assert_eq!(project.default_targets[0].target_kind, "bin");
assert_eq!(project.default_targets[0].target_name, "minigrep_by_shyoy");
assert_eq!(
project.default_targets[0].target_identity,
"bin:minigrep_by_shyoy:normal"
);
}
#[test]
fn package_with_lib_and_default_run_uses_only_lib_and_default_bin() {
let temp = TempDir::new().unwrap();
write(
&temp.path().join("Cargo.toml"),
r#"
[package]
name = "rustdesk"
version = "0.1.0"
edition = "2024"
default-run = "rustdesk"
[lib]
name = "librustdesk"
[[bin]]
name = "rustdesk"
path = "src/main.rs"
[[bin]]
name = "service"
path = "src/bin/service.rs"
"#,
);
write(&temp.path().join("src/lib.rs"), "pub fn x() {}\n");
write(&temp.path().join("src/main.rs"), "fn main() {}\n");
write(&temp.path().join("src/bin/service.rs"), "fn main() {}\n");
let project = try_resolve_target_project(temp.path()).unwrap();
let identities = project
.default_targets
.iter()
.map(|target| target.target_identity.as_str())
.collect::<Vec<_>>();
assert_eq!(
identities,
vec!["lib:librustdesk:normal", "bin:rustdesk:normal"]
);
}
#[test]
fn package_without_lib_file_does_not_add_lib_target() {
let temp = TempDir::new().unwrap();
write(
&temp.path().join("Cargo.toml"),
r#"
[package]
name = "tool"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "helper"
path = "src/bin/helper.rs"
"#,
);
write(&temp.path().join("src/bin/helper.rs"), "fn main() {}\n");
let project = try_resolve_target_project(temp.path()).unwrap();
assert_eq!(project.default_targets.len(), 1);
assert_eq!(
project.default_targets[0].target_identity,
"bin:helper:normal"
);
}
}