use std::path::Path;
use super::{Cli, CliError, Result};
pub const CLI: Cli = Cli {
name: "cargo",
hint: "Install from https://rustup.rs",
};
pub fn require() -> Result<Cargo> {
CLI.require()?;
Ok(Cargo)
}
pub struct Cargo;
impl Cargo {
pub fn project<'a>(&self, manifest_path: &'a Path) -> Project<'a> {
project(manifest_path)
}
pub fn init(&self, path: &Path, name: &str) -> Result<()> {
init(path, name)
}
}
#[derive(Debug)]
pub struct CargoMetadata {
pub package_name: String,
pub version: String,
pub binary_targets: Vec<String>,
pub manifest_path: String,
}
pub fn raw_metadata(manifest_path: &Path) -> Result<String> {
CLI.run_command(
CLI.command()
.args([
"metadata",
"--no-deps",
"--format-version",
"1",
"--manifest-path",
])
.arg(manifest_path),
)
}
pub fn metadata(manifest_path: &Path) -> Result<CargoMetadata> {
let json_str = CLI.run_command(
CLI.command()
.args([
"metadata",
"--no-deps",
"--format-version",
"1",
"--manifest-path",
])
.arg(manifest_path),
)?;
let json: serde_json::Value =
serde_json::from_str(&json_str).map_err(|e| CliError::Failed {
cli: CLI.name.to_string(),
code: -1,
stderr: format!("Failed to parse cargo metadata JSON: {e}"),
})?;
let packages = json["packages"]
.as_array()
.ok_or_else(|| CliError::Failed {
cli: CLI.name.to_string(),
code: -1,
stderr: "No packages in cargo metadata output".to_string(),
})?;
let manifest_str = manifest_path.to_string_lossy();
let package = packages
.iter()
.find(|p| {
p["manifest_path"]
.as_str()
.is_some_and(|mp| mp == manifest_str.as_ref())
})
.or_else(|| packages.first())
.ok_or_else(|| CliError::Failed {
cli: CLI.name.to_string(),
code: -1,
stderr: "No packages found in cargo metadata".to_string(),
})?;
let package_name = package["name"].as_str().unwrap_or("unknown").to_string();
let version = package["version"].as_str().unwrap_or("0.0.0").to_string();
let manifest_path_str = package["manifest_path"].as_str().unwrap_or("").to_string();
let mut binary_targets = Vec::new();
if let Some(targets) = package["targets"].as_array() {
for target in targets {
let is_bin = target["kind"]
.as_array()
.is_some_and(|kinds| kinds.iter().any(|k| k.as_str() == Some("bin")));
if is_bin && let Some(name) = target["name"].as_str() {
binary_targets.push(name.to_string());
}
}
}
Ok(CargoMetadata {
package_name,
version,
binary_targets,
manifest_path: manifest_path_str,
})
}
pub fn build_release(manifest_path: &Path) -> Result<()> {
CLI.run_status(
CLI.command()
.args(["build", "--release", "--manifest-path"])
.arg(manifest_path),
)
}
pub fn build_release_bin(manifest_path: &Path, bin: &str) -> Result<()> {
CLI.run_status(
CLI.command()
.args(["build", "--release", "--manifest-path"])
.arg(manifest_path)
.args(["--bin", bin]),
)
}
pub fn run(manifest_path: &Path, bin: &str, args: &[&str]) -> Result<String> {
CLI.run_command(
CLI.command()
.args(["run", "-q", "--manifest-path"])
.arg(manifest_path)
.args(["--bin", bin, "--"])
.args(args),
)
}
pub fn run_interactive(manifest_path: &Path, bin: &str, args: &[&str]) -> Result<()> {
CLI.run_status(
CLI.command()
.args(["run", "-q", "--manifest-path"])
.arg(manifest_path)
.args(["--bin", bin, "--"])
.args(args),
)
}
pub fn init(path: &Path, name: &str) -> Result<()> {
CLI.run_status(CLI.command().arg("init").arg(path).args(["--name", name]))
}
pub fn project(manifest_path: &Path) -> Project<'_> {
Project { manifest_path }
}
pub struct Project<'a> {
manifest_path: &'a Path,
}
impl<'a> Project<'a> {
pub fn metadata(&self) -> Result<CargoMetadata> {
metadata(self.manifest_path)
}
pub fn build_release(&self) -> Result<()> {
build_release(self.manifest_path)
}
pub fn build_release_bin(&self, bin: &str) -> Result<()> {
build_release_bin(self.manifest_path, bin)
}
pub fn run(&self, bin: &str, args: &[&str]) -> Result<String> {
self::run(self.manifest_path, bin, args)
}
pub fn run_interactive(&self, bin: &str, args: &[&str]) -> Result<()> {
run_interactive(self.manifest_path, bin, args)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_metadata_parses_cargo_project() {
let dir = tempfile::tempdir().unwrap();
let cargo_toml = dir.path().join("Cargo.toml");
fs::write(
&cargo_toml,
r#"[package]
name = "test-proj"
version = "1.2.3"
edition = "2021"
[[bin]]
name = "test-proj"
path = "src/main.rs"
"#,
)
.unwrap();
let src_dir = dir.path().join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("main.rs"), "fn main() {}\n").unwrap();
let meta = metadata(&cargo_toml).unwrap();
assert_eq!(meta.package_name, "test-proj");
assert_eq!(meta.version, "1.2.3");
assert!(meta.binary_targets.contains(&"test-proj".to_string()));
assert!(meta.manifest_path.ends_with("Cargo.toml"));
}
#[test]
fn test_metadata_no_explicit_bin_target() {
let dir = tempfile::tempdir().unwrap();
let cargo_toml = dir.path().join("Cargo.toml");
fs::write(
&cargo_toml,
r#"[package]
name = "implicit-bin"
version = "0.5.0"
edition = "2021"
"#,
)
.unwrap();
let src_dir = dir.path().join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("main.rs"), "fn main() {}\n").unwrap();
let meta = metadata(&cargo_toml).unwrap();
assert_eq!(meta.package_name, "implicit-bin");
assert_eq!(meta.version, "0.5.0");
assert!(meta.binary_targets.contains(&"implicit-bin".to_string()));
}
#[test]
fn test_init_creates_cargo_project() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("my-new-project");
init(&project_dir, "my-new-project").unwrap();
assert!(project_dir.join("Cargo.toml").exists());
assert!(project_dir.join("src").join("main.rs").exists());
let meta = metadata(&project_dir.join("Cargo.toml")).unwrap();
assert_eq!(meta.package_name, "my-new-project");
}
}