use super::common::{
PackageError, command_exists, run_package_command, validate_package_name,
validate_package_version,
};
use super::config::{CargoConfig, PackageSpec};
pub struct CargoHandler {
config: CargoConfig,
}
impl CargoHandler {
pub fn new(config: CargoConfig) -> Self {
Self { config }
}
pub fn install(&self) -> Result<(), PackageError> {
if !command_exists("cargo") {
return Err(PackageError::PackageManagerNotInstalled(
"cargo".to_string(),
));
}
if self.config.packages.is_empty() {
println!(" No cargo packages configured");
return Ok(());
}
let current_dir = std::env::current_dir().map_err(PackageError::Io)?;
for (name, spec) in &self.config.packages {
if spec.is_optional() {
continue;
}
if let Err(e) = self.install_crate(name, spec, ¤t_dir) {
tracing::warn!(
event = "package.install_failed",
ecosystem = "cargo",
package = %name,
error = %e,
);
eprintln!(" Warning: Failed to install {}: {}", name, e);
}
}
Ok(())
}
fn install_crate(
&self,
name: &str,
spec: &PackageSpec,
working_dir: &std::path::Path,
) -> Result<(), PackageError> {
validate_package_name(name, "[cargo]")?;
validate_package_version(spec.version(), "[cargo]")?;
for feature in spec.features() {
validate_package_name(feature, "[cargo features]")?;
}
println!(" Installing {}...", name);
let telemetry_on = crate::observability::telemetry_gate::is_enabled();
if telemetry_on {
tracing::info!(
event = "package.requested",
ecosystem = "cargo",
package = %name,
version = %spec.version(),
source = "config",
platform = std::env::consts::OS,
);
}
let _pkg_span = tracing::info_span!(
"package",
ecosystem = "cargo",
name = %name,
version = %spec.version(),
)
.entered();
let started = std::time::Instant::now();
let mut args = vec!["install", name];
let version = spec.version();
if version != "latest" {
args.push("--version");
args.push(version);
}
let features = spec.features();
let features_str: String;
if !features.is_empty() {
features_str = features.join(",");
args.push("--features");
args.push(&features_str);
}
if self.config.locked {
args.push("--locked");
}
match run_package_command("cargo", &args, working_dir) {
Ok(()) => {
if telemetry_on {
tracing::info!(
event = "package.installed",
ecosystem = "cargo",
package = %name,
version = %version,
source = "config",
duration_ms = started.elapsed().as_millis() as u64,
platform = std::env::consts::OS,
);
}
Ok(())
}
Err(e) => {
if telemetry_on {
tracing::warn!(
event = "package.failed",
ecosystem = "cargo",
package = %name,
version = %version,
source = "config",
error_kind = e.kind(),
error = %e,
platform = std::env::consts::OS,
);
}
Err(e)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_cargo_handler_empty() {
let config = CargoConfig::default();
let handler = CargoHandler::new(config);
assert!(handler.config.packages.is_empty());
}
#[test]
fn test_cargo_config_with_packages() {
let mut packages = HashMap::new();
packages.insert(
"cargo-watch".to_string(),
PackageSpec::Version("latest".to_string()),
);
packages.insert(
"cargo-nextest".to_string(),
PackageSpec::Version("0.9".to_string()),
);
let config = CargoConfig {
packages,
locked: true,
};
let handler = CargoHandler::new(config);
assert!(handler.config.locked);
assert_eq!(handler.config.packages.len(), 2);
}
}