use std::{
fs::{self, OpenOptions},
io::Write,
path::{Path, PathBuf},
process::{Command, ExitStatus},
str::{from_utf8, FromStr},
};
use crate::error::{self, IOResultExt};
use cargo_metadata::MetadataCommand;
use log::{debug, info};
#[derive(Debug)]
pub struct WorkspaceMetadata {
pub binaries: Vec<String>,
pub examples: Vec<String>,
}
pub fn locate_project() -> error::Result<PathBuf> {
let output = Command::new("cargo")
.args(vec![
"locate-project",
"--workspace",
"--message-format",
"plain",
])
.log()
.output()?;
if !output.status.success() {
return Err(error::Error::CargoLocateProjectFailed);
}
Ok(PathBuf::from(from_utf8(&output.stdout)?.trim()))
}
const SAMPLY_PROFILE: &str = "
[profile.samply]
inherits = \"release\"
debug = true
";
pub fn ensure_samply_profile(cargo_toml: &Path) -> error::Result<()> {
let cargo_toml_content: String = fs::read_to_string(cargo_toml).path_ctx(cargo_toml)?;
let manifest = toml::Table::from_str(&cargo_toml_content)?;
let profile_samply = manifest
.get("profile")
.and_then(|p| p.as_table())
.and_then(|p| p.get("samply"));
if profile_samply.is_none() {
let mut f = OpenOptions::new()
.append(true)
.open(cargo_toml)
.path_ctx(cargo_toml)?;
f.write(SAMPLY_PROFILE.as_bytes()).path_ctx(cargo_toml)?;
info!("'samply' profile was added to 'Cargo.toml'");
}
Ok(())
}
pub fn get_workspace_metadata_from(cargo_toml: &Path) -> error::Result<WorkspaceMetadata> {
let work_dir = cargo_toml.parent().unwrap_or_else(|| Path::new("."));
let metadata = MetadataCommand::new()
.current_dir(work_dir)
.no_deps()
.exec()
.map_err(|e| error::Error::Io(std::io::Error::other(e)))?;
let mut binaries = Vec::new();
let mut examples = Vec::new();
for package in metadata.packages {
for target in package.targets {
if target.is_bin() {
if !binaries.contains(&target.name) {
binaries.push(target.name);
}
} else if target.is_example() && !examples.contains(&target.name) {
examples.push(target.name);
}
}
}
binaries.sort();
examples.sort();
Ok(WorkspaceMetadata { binaries, examples })
}
pub fn guess_bin(cargo_toml: &Path) -> error::Result<String> {
let manifest = cargo_toml::Manifest::from_path(cargo_toml)?;
let default_run = manifest.package.and_then(|p| p.default_run);
if let Some(bin) = default_run {
return Ok(bin);
}
if manifest.bin.len() == 1 {
if let Some(name) = manifest.bin.first().and_then(|b| b.name.as_ref()) {
return Ok(name.clone());
}
}
let local_binaries: Vec<String> = manifest
.bin
.iter()
.filter_map(|b| b.name.as_ref())
.cloned()
.collect();
let local_examples: Vec<String> = manifest
.example
.iter()
.filter_map(|e| e.name.as_ref())
.cloned()
.collect();
if !local_binaries.is_empty() || !local_examples.is_empty() {
return create_suggestions_error(local_binaries, local_examples);
}
let workspace_metadata = get_workspace_metadata_from(cargo_toml).unwrap_or_else(|_| {
WorkspaceMetadata {
binaries: Vec::new(),
examples: Vec::new(),
}
});
if workspace_metadata.binaries.is_empty() {
return Err(error::Error::NoBinaryFound);
}
if workspace_metadata.binaries.len() == 1 {
return Ok(workspace_metadata.binaries[0].clone());
}
create_suggestions_error(workspace_metadata.binaries, workspace_metadata.examples)
}
fn create_suggestions_error(binaries: Vec<String>, examples: Vec<String>) -> error::Result<String> {
let mut suggestions = Vec::new();
if !binaries.is_empty() {
suggestions.push("\n\nAvailable binaries:".to_string());
for bin in &binaries {
suggestions.push(format!(" {}: cargo samply --bin {}", bin, bin));
}
}
if !examples.is_empty() {
suggestions.push("\n\nAvailable examples:".to_string());
for example in &examples {
suggestions.push(format!(" {}: cargo samply --example {}", example, example));
}
}
let suggestions_text = suggestions.join("\n");
Err(error::Error::BinaryToRunNotDetermined {
suggestions: suggestions_text,
})
}
pub trait CommandExt {
fn call(&mut self) -> error::Result<ExitStatus>;
fn log(&mut self) -> &mut Command;
}
impl CommandExt for Command {
fn call(&mut self) -> error::Result<ExitStatus> {
self.log();
Ok(self.spawn()?.wait()?)
}
fn log(&mut self) -> &mut Command {
debug!(
"running {:?} with args: {:?}",
self.get_program(),
self.get_args().collect::<Vec<&std::ffi::OsStr>>()
);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_ensure_samply_profile_adds_profile() {
let temp_dir = TempDir::new().unwrap();
let cargo_toml_path = temp_dir.path().join("Cargo.toml");
let initial_content = r#"
[package]
name = "test"
version = "0.1.0"
"#;
fs::write(&cargo_toml_path, initial_content).unwrap();
ensure_samply_profile(&cargo_toml_path).unwrap();
let content = fs::read_to_string(&cargo_toml_path).unwrap();
assert!(content.contains("[profile.samply]"));
assert!(content.contains("inherits = \"release\""));
assert!(content.contains("debug = true"));
}
#[test]
fn test_ensure_samply_profile_already_exists() {
let temp_dir = TempDir::new().unwrap();
let cargo_toml_path = temp_dir.path().join("Cargo.toml");
let initial_content = r#"
[package]
name = "test"
version = "0.1.0"
[profile.samply]
inherits = "release"
debug = true
"#;
fs::write(&cargo_toml_path, initial_content).unwrap();
let original_content = fs::read_to_string(&cargo_toml_path).unwrap();
ensure_samply_profile(&cargo_toml_path).unwrap();
let new_content = fs::read_to_string(&cargo_toml_path).unwrap();
assert_eq!(original_content, new_content); }
#[test]
fn test_guess_bin_with_default_run() {
let temp_dir = TempDir::new().unwrap();
let cargo_toml_path = temp_dir.path().join("Cargo.toml");
let content = r#"
[package]
name = "test"
version = "0.1.0"
default-run = "mybin"
[[bin]]
name = "mybin"
path = "src/main.rs"
"#;
fs::write(&cargo_toml_path, content).unwrap();
let bin = guess_bin(&cargo_toml_path).unwrap();
assert_eq!(bin, "mybin");
}
#[test]
fn test_guess_bin_single_bin() {
let temp_dir = TempDir::new().unwrap();
let cargo_toml_path = temp_dir.path().join("Cargo.toml");
let content = r#"
[package]
name = "test"
version = "0.1.0"
[[bin]]
name = "single"
path = "src/main.rs"
"#;
fs::write(&cargo_toml_path, content).unwrap();
let bin = guess_bin(&cargo_toml_path).unwrap();
assert_eq!(bin, "single");
}
#[test]
fn test_guess_bin_multiple_bins_no_default() {
let temp_dir = TempDir::new().unwrap();
let cargo_toml_path = temp_dir.path().join("Cargo.toml");
let content = r#"
[package]
name = "test"
version = "0.1.0"
[[bin]]
name = "first"
path = "src/first.rs"
[[bin]]
name = "second"
path = "src/second.rs"
"#;
fs::write(&cargo_toml_path, content).unwrap();
let result = guess_bin(&cargo_toml_path);
assert!(result.is_err());
if let Err(error::Error::BinaryToRunNotDetermined { suggestions: _ }) = result {
} else {
panic!("Expected BinaryToRunNotDetermined with suggestions");
}
}
#[test]
fn test_guess_bin_no_bins() {
let temp_dir = TempDir::new().unwrap();
let cargo_toml_path = temp_dir.path().join("Cargo.toml");
let content = r#"
[package]
name = "test"
version = "0.1.0"
"#;
fs::write(&cargo_toml_path, content).unwrap();
let result = guess_bin(&cargo_toml_path);
assert!(result.is_err());
if let Err(error::Error::NoBinaryFound) = result {
} else {
panic!("Expected NoBinaryFound, got: {:?}", result);
}
}
}