use super::common::{
PackageError, command_exists, run_package_command, validate_package_name,
validate_package_version,
};
use super::config::{NugetConfig, PackageSpec};
pub struct NugetHandler {
config: NugetConfig,
}
impl NugetHandler {
pub fn new(config: NugetConfig) -> Self {
Self { config }
}
pub fn install(&self) -> Result<(), PackageError> {
if !command_exists("dotnet") {
return Err(PackageError::PackageManagerNotInstalled(
"dotnet".to_string(),
));
}
if self.config.packages.is_empty() {
println!(" No NuGet global tools 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_tool(name, spec, ¤t_dir) {
tracing::warn!(
event = "package.install_failed",
ecosystem = "nuget",
package = %name,
error = %e,
);
eprintln!(" Warning: Failed to install {}: {}", name, e);
}
}
Ok(())
}
fn install_tool(
&self,
name: &str,
spec: &PackageSpec,
working_dir: &std::path::Path,
) -> Result<(), PackageError> {
validate_package_name(name, "[nuget]")?;
validate_package_version(spec.version(), "[nuget]")?;
println!(" Installing {}...", name);
let telemetry_on = crate::observability::telemetry_gate::is_enabled();
if telemetry_on {
tracing::info!(
event = "package.requested",
ecosystem = "nuget",
package = %name,
version = %spec.version(),
source = "config",
platform = std::env::consts::OS,
);
}
let started = std::time::Instant::now();
let args = build_install_args(name, spec.version());
let _pkg_span = tracing::info_span!(
"package",
ecosystem = "nuget",
name = %name,
version = %spec.version(),
)
.entered();
match run_package_command("dotnet", &args, working_dir) {
Ok(()) => {
if telemetry_on {
tracing::info!(
event = "package.installed",
ecosystem = "nuget",
package = %name,
version = %spec.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 = "nuget",
package = %name,
version = %spec.version(),
source = "config",
error_kind = e.kind(),
error = %e,
platform = std::env::consts::OS,
);
}
Err(e)
}
}
}
}
pub(crate) fn build_install_args<'a>(name: &'a str, version: &'a str) -> Vec<&'a str> {
let mut args: Vec<&str> = Vec::with_capacity(6);
args.extend_from_slice(&["tool", "update", "-g", name]);
if version != "latest" {
args.push("--version");
args.push(version);
}
args
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn nuget_handler_empty() {
let config = NugetConfig::default();
let handler = NugetHandler::new(config);
assert!(handler.config.packages.is_empty());
}
#[test]
fn nuget_handler_holds_packages() {
let mut packages = HashMap::new();
packages.insert(
"dotnet-ef".to_string(),
PackageSpec::Version("latest".to_string()),
);
packages.insert(
"csharpier".to_string(),
PackageSpec::Version("0.30.0".to_string()),
);
let config = NugetConfig { packages };
let handler = NugetHandler::new(config);
assert_eq!(handler.config.packages.len(), 2);
}
#[test]
fn build_install_args_table() {
let cases = [
(
"dotnet-ef",
"latest",
vec!["tool", "update", "-g", "dotnet-ef"],
),
(
"csharpier",
"0.30.0",
vec!["tool", "update", "-g", "csharpier", "--version", "0.30.0"],
),
(
"dotnet-aspnet-codegenerator",
"8.0.0",
vec![
"tool",
"update",
"-g",
"dotnet-aspnet-codegenerator",
"--version",
"8.0.0",
],
),
];
for (name, version, expected) in cases {
let actual = build_install_args(name, version);
assert_eq!(actual, expected, "argv mismatch for {} = {}", name, version);
assert_eq!(actual[0], "tool", "first arg must be `tool`");
assert_eq!(actual[1], "update", "must use `update` for idempotency");
assert_eq!(actual[2], "-g", "must be global install");
assert_ne!(actual[1], "install", "`install` errors when present");
}
}
#[test]
fn nuget_rejects_flag_like_tool_names() {
let mut packages = HashMap::new();
packages.insert(
"--source".to_string(),
PackageSpec::Version("latest".to_string()),
);
let config = NugetConfig { packages };
let handler = NugetHandler::new(config);
let result = handler.install();
match &result {
Ok(()) | Err(PackageError::PackageManagerNotInstalled(_)) => {
}
Err(other) => panic!("unexpected outer error: {other:?}"),
}
let spec = PackageSpec::Version("latest".to_string());
let err = handler
.install_tool("--source", &spec, std::path::Path::new("."))
.expect_err("flag-like name must be refused");
assert!(
matches!(err, PackageError::RefusedUnsafeSpec(_, _)),
"expected RefusedUnsafeSpec, got {err:?}"
);
}
}