use crate::config::locking::HierarchicalLockManager;
use crate::rust_version::RustVersion;
use crate::rust_version::detector::{
get_active_toolchain, get_installed_toolchains, is_rustup_available,
};
use crate::{Error, Result};
use semver::Version;
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolchainChannel {
Stable,
Beta,
Nightly,
Version(String),
Custom(String),
}
impl std::fmt::Display for ToolchainChannel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Stable => write!(f, "stable"),
Self::Beta => write!(f, "beta"),
Self::Nightly => write!(f, "nightly"),
Self::Version(v) => write!(f, "{}", v),
Self::Custom(s) => write!(f, "{}", s),
}
}
}
impl ToolchainChannel {
pub fn parse(channel: &str) -> Self {
match channel.to_lowercase().as_str() {
"stable" => Self::Stable,
"beta" => Self::Beta,
"nightly" => Self::Nightly,
s => {
if s.chars().next().is_some_and(|c| c.is_ascii_digit()) {
Self::Version(s.to_string())
} else {
Self::Custom(s.to_string())
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolchainInfo {
pub channel: ToolchainChannel,
pub is_default: bool,
pub is_installed: bool,
}
#[derive(Debug, Clone)]
pub struct VersionRequirements {
pub minimum: Option<Version>,
pub maximum: Option<Version>,
pub exact: Option<Version>,
}
impl VersionRequirements {
pub fn new() -> Self {
Self {
minimum: None,
maximum: None,
exact: None,
}
}
pub fn check(&self, version: &Version) -> bool {
if let Some(exact) = &self.exact {
return version == exact;
}
if let Some(minimum) = &self.minimum
&& version < minimum
{
return false;
}
if let Some(maximum) = &self.maximum
&& version > maximum
{
return false;
}
true
}
pub fn description(&self) -> String {
if let Some(exact) = &self.exact {
return format!("exactly {}", exact);
}
match (&self.minimum, &self.maximum) {
(Some(min), Some(max)) => format!("between {} and {}", min, max),
(Some(min), None) => format!(">= {}", min),
(None, Some(max)) => format!("<= {}", max),
(None, None) => "any version".to_string(),
}
}
}
impl Default for VersionRequirements {
fn default() -> Self {
Self::new()
}
}
pub struct RustupManager;
impl RustupManager {
pub fn new() -> Self {
Self
}
pub fn is_available(&self) -> bool {
is_rustup_available()
}
fn ensure_rustup(&self) -> Result<()> {
if !self.is_available() {
return Err(Error::rust_not_found(
"rustup not found. Please install rustup from https://rustup.rs",
));
}
Ok(())
}
pub async fn get_current_version(&self) -> Result<RustVersion> {
crate::rust_version::detector::detect_rust_version().await
}
pub async fn list_toolchains(&self) -> Result<Vec<ToolchainInfo>> {
self.ensure_rustup()?;
let active = get_active_toolchain().await?;
let installed = get_installed_toolchains().await?;
let toolchains: Vec<ToolchainInfo> = installed
.into_iter()
.map(|name| {
let channel = ToolchainChannel::parse(&name);
let is_default = name == active;
ToolchainInfo {
channel,
is_default,
is_installed: true,
}
})
.collect();
Ok(toolchains)
}
pub async fn install_toolchain(&self, channel: &ToolchainChannel) -> Result<()> {
self.ensure_rustup()?;
let channel_str = channel.to_string();
info!("Installing toolchain: {}", channel_str);
let output = tokio::process::Command::new("rustup")
.args(["toolchain", "install", &channel_str, "--no-self-update"])
.output()
.await
.map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::command(format!(
"Failed to install toolchain '{}': {}",
channel_str, stderr
)));
}
info!("Successfully installed toolchain: {}", channel_str);
Ok(())
}
pub async fn uninstall_toolchain(&self, channel: &ToolchainChannel) -> Result<()> {
self.ensure_rustup()?;
let channel_str = channel.to_string();
info!("Uninstalling toolchain: {}", channel_str);
let output = tokio::process::Command::new("rustup")
.args(["toolchain", "uninstall", &channel_str])
.output()
.await
.map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::command(format!(
"Failed to uninstall toolchain '{}': {}",
channel_str, stderr
)));
}
info!("Successfully uninstalled toolchain: {}", channel_str);
Ok(())
}
pub async fn switch_toolchain(&self, channel: &ToolchainChannel) -> Result<()> {
self.ensure_rustup()?;
let channel_str = channel.to_string();
info!("Switching to toolchain: {}", channel_str);
let output = tokio::process::Command::new("rustup")
.args(["default", &channel_str])
.output()
.await
.map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::command(format!(
"Failed to switch to toolchain '{}': {}",
channel_str, stderr
)));
}
info!("Successfully switched to toolchain: {}", channel_str);
Ok(())
}
pub async fn update_toolchains(&self) -> Result<UpdateResult> {
self.ensure_rustup()?;
info!("Updating toolchains...");
let output = tokio::process::Command::new("rustup")
.args(["update", "--no-self-update"])
.output()
.await
.map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !output.status.success() {
return Err(Error::command(format!(
"Failed to update toolchains: {}",
stderr
)));
}
let updated = stdout
.lines()
.chain(stderr.lines())
.filter(|line| line.contains("updated") || line.contains("installed"))
.map(|s| s.to_string())
.collect();
info!("Toolchain update completed");
Ok(UpdateResult {
success: true,
updated,
})
}
pub async fn install_component(&self, component: &str, toolchain: Option<&str>) -> Result<()> {
self.ensure_rustup()?;
let mut args = vec!["component", "add", component];
if let Some(tc) = toolchain {
args.push("--toolchain");
args.push(tc);
}
info!("Installing component '{}'", component);
let output = tokio::process::Command::new("rustup")
.args(&args)
.output()
.await
.map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::command(format!(
"Failed to install component '{}': {}",
component, stderr
)));
}
info!("Successfully installed component '{}'", component);
Ok(())
}
pub async fn get_version_requirements(&self) -> Result<VersionRequirements> {
let lock_manager = HierarchicalLockManager::load().await?;
let mut requirements = VersionRequirements::new();
if let Some((_, entry)) = lock_manager.is_locked("rust-version") {
debug!("Found locked rust-version: {}", entry.value);
if let Ok(version) = Version::parse(&entry.value) {
requirements.minimum = Some(version);
}
}
if let Some((_, entry)) = lock_manager.is_locked("max-rust-version") {
debug!("Found locked max-rust-version: {}", entry.value);
if let Ok(version) = Version::parse(&entry.value) {
requirements.maximum = Some(version);
}
}
Ok(requirements)
}
pub async fn check_version_requirements(&self) -> Result<VersionCheckResult> {
let current = self.get_current_version().await?;
let requirements = self.get_version_requirements().await?;
let meets_requirements = requirements.check(¤t.version);
Ok(VersionCheckResult {
current: current.version,
requirements,
meets_requirements,
})
}
pub async fn self_update(&self) -> Result<()> {
self.ensure_rustup()?;
info!("Running rustup self-update...");
let output = tokio::process::Command::new("rustup")
.args(["self", "update"])
.output()
.await
.map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::command(format!(
"Failed to self-update rustup: {}",
stderr
)));
}
info!("Rustup self-update completed");
Ok(())
}
pub async fn show_active_toolchain(&self) -> Result<String> {
self.ensure_rustup()?;
let output = tokio::process::Command::new("rustup")
.args(["show", "active-toolchain"])
.output()
.await
.map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::command(format!(
"Failed to show active toolchain: {}",
stderr
)));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
}
impl Default for RustupManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct UpdateResult {
pub success: bool,
pub updated: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct VersionCheckResult {
pub current: Version,
pub requirements: VersionRequirements,
pub meets_requirements: bool,
}
impl VersionCheckResult {
pub fn status_message(&self) -> String {
if self.meets_requirements {
format!(
"✅ Current version {} meets requirements ({})",
self.current,
self.requirements.description()
)
} else {
format!(
"❌ Current version {} does NOT meet requirements ({})",
self.current,
self.requirements.description()
)
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn test_toolchain_channel_display() {
assert_eq!(ToolchainChannel::Stable.to_string(), "stable");
assert_eq!(ToolchainChannel::Beta.to_string(), "beta");
assert_eq!(ToolchainChannel::Nightly.to_string(), "nightly");
assert_eq!(
ToolchainChannel::Version("1.70.0".to_string()).to_string(),
"1.70.0"
);
assert_eq!(
ToolchainChannel::Custom("my-toolchain".to_string()).to_string(),
"my-toolchain"
);
}
#[test]
fn test_toolchain_channel_parse() {
assert!(matches!(
ToolchainChannel::parse("stable"),
ToolchainChannel::Stable
));
assert!(matches!(
ToolchainChannel::parse("beta"),
ToolchainChannel::Beta
));
assert!(matches!(
ToolchainChannel::parse("nightly"),
ToolchainChannel::Nightly
));
assert!(matches!(
ToolchainChannel::parse("1.70.0"),
ToolchainChannel::Version(_)
));
assert!(matches!(
ToolchainChannel::parse("custom-toolchain"),
ToolchainChannel::Custom(_)
));
}
#[test]
fn test_version_requirements_check() {
let mut req = VersionRequirements::new();
let v170 = Version::new(1, 70, 0);
let v180 = Version::new(1, 80, 0);
let v190 = Version::new(1, 90, 0);
assert!(req.check(&v170));
req.minimum = Some(v180.clone());
assert!(!req.check(&v170));
assert!(req.check(&v180));
assert!(req.check(&v190));
req = VersionRequirements::new();
req.maximum = Some(v180.clone());
assert!(req.check(&v170));
assert!(req.check(&v180));
assert!(!req.check(&v190));
req = VersionRequirements::new();
req.exact = Some(v180.clone());
assert!(!req.check(&v170));
assert!(req.check(&v180));
assert!(!req.check(&v190));
}
#[test]
fn test_version_requirements_description() {
let mut req = VersionRequirements::new();
assert_eq!(req.description(), "any version");
req.minimum = Some(Version::new(1, 70, 0));
assert_eq!(req.description(), ">= 1.70.0");
req.maximum = Some(Version::new(1, 80, 0));
assert_eq!(req.description(), "between 1.70.0 and 1.80.0");
req = VersionRequirements::new();
req.exact = Some(Version::new(1, 75, 0));
assert_eq!(req.description(), "exactly 1.75.0");
}
}