use crate::error::{Error, Result};
use crate::target::Target;
use std::process::Command;
use std::str;
#[derive(Debug, Clone, PartialEq)]
pub struct Toolchain {
pub name: String,
pub is_default: bool,
pub targets: Vec<String>,
}
pub struct ToolchainManager {
rustup_path: String,
}
impl ToolchainManager {
pub fn new() -> Result<Self> {
let rustup_path = Self::find_rustup()?;
Ok(Self { rustup_path })
}
fn find_rustup() -> Result<String> {
let output = Command::new("rustup")
.arg("--version")
.output()
.map_err(|e| Error::Toolchain(format!(
"rustup not found. Please install rustup from https://rustup.rs/. Error: {}", e
)))?;
if !output.status.success() {
return Err(Error::Toolchain(
"rustup found but failed to execute. Please check your rustup installation.".to_string()
));
}
Ok("rustup".to_string())
}
pub fn list_toolchains(&self) -> Result<Vec<Toolchain>> {
let output = Command::new(&self.rustup_path)
.args(["toolchain", "list"])
.output()
.map_err(|e| Error::Toolchain(format!("Failed to list toolchains: {}", e)))?;
if !output.status.success() {
return Err(Error::Toolchain(
"Failed to list toolchains".to_string()
));
}
let stdout = str::from_utf8(&output.stdout)
.map_err(|e| Error::Toolchain(format!("Invalid UTF-8 in rustup output: {}", e)))?;
let mut toolchains = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let is_default = line.contains("(default)");
let name = line
.replace("(default)", "")
.trim()
.to_string();
toolchains.push(Toolchain {
name,
is_default,
targets: Vec::new(), });
}
Ok(toolchains)
}
pub fn get_default_toolchain(&self) -> Result<Option<Toolchain>> {
let toolchains = self.list_toolchains()?;
Ok(toolchains.into_iter().find(|t| t.is_default))
}
pub fn list_targets(&self, toolchain: &str) -> Result<Vec<String>> {
let output = Command::new(&self.rustup_path)
.args(["target", "list", "--installed", "--toolchain", toolchain])
.output()
.map_err(|e| Error::Toolchain(format!("Failed to list targets: {}", e)))?;
if !output.status.success() {
return Err(Error::Toolchain(format!(
"Failed to list targets for toolchain '{}'", toolchain
)));
}
let stdout = str::from_utf8(&output.stdout)
.map_err(|e| Error::Toolchain(format!("Invalid UTF-8 in rustup output: {}", e)))?;
let targets: Vec<String> = stdout
.lines()
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect();
Ok(targets)
}
pub fn is_target_installed(&self, toolchain: &str, target: &str) -> Result<bool> {
let targets = self.list_targets(toolchain)?;
Ok(targets.iter().any(|t| t == target))
}
pub fn install_target(&self, toolchain: &str, target: &str) -> Result<()> {
println!("Installing target {} for toolchain {}...", target, toolchain);
let output = Command::new(&self.rustup_path)
.args(["target", "add", target, "--toolchain", toolchain])
.output()
.map_err(|e| Error::Toolchain(format!("Failed to install target: {}", e)))?;
if !output.status.success() {
let stderr = str::from_utf8(&output.stderr).unwrap_or("<invalid UTF-8>");
return Err(Error::Toolchain(format!(
"Failed to install target '{}' for toolchain '{}': {}",
target, toolchain, stderr
)));
}
println!("Successfully installed target {}", target);
Ok(())
}
pub fn ensure_target(&self, toolchain: &str, target: &str) -> Result<()> {
if self.is_target_installed(toolchain, target)? {
return Ok(());
}
self.install_target(toolchain, target)
}
pub fn install_toolchain(&self, toolchain: &str) -> Result<()> {
println!("Installing toolchain {}...", toolchain);
let output = Command::new(&self.rustup_path)
.args(["toolchain", "install", toolchain])
.output()
.map_err(|e| Error::Toolchain(format!("Failed to install toolchain: {}", e)))?;
if !output.status.success() {
let stderr = str::from_utf8(&output.stderr).unwrap_or("<invalid UTF-8>");
return Err(Error::Toolchain(format!(
"Failed to install toolchain '{}': {}", toolchain, stderr
)));
}
println!("Successfully installed toolchain {}", toolchain);
Ok(())
}
pub fn is_toolchain_installed(&self, toolchain: &str) -> Result<bool> {
let toolchains = self.list_toolchains()?;
Ok(toolchains.iter().any(|t| t.name.starts_with(toolchain)))
}
pub fn ensure_toolchain(&self, toolchain: &str) -> Result<()> {
if self.is_toolchain_installed(toolchain)? {
return Ok(());
}
self.install_toolchain(toolchain)
}
pub fn prepare_target(&self, toolchain: &str, target: &Target) -> Result<()> {
self.ensure_toolchain(toolchain)?;
self.ensure_target(toolchain, &target.triple)?;
Ok(())
}
pub fn get_rustup_home(&self) -> Result<std::path::PathBuf> {
let output = Command::new(&self.rustup_path)
.args(["show", "home"])
.output()
.map_err(|e| Error::Toolchain(format!("Failed to get rustup home: {}", e)))?;
if !output.status.success() {
return Err(Error::Toolchain(
"Failed to determine rustup home directory".to_string()
));
}
let stdout = str::from_utf8(&output.stdout)
.map_err(|e| Error::Toolchain(format!("Invalid UTF-8 in rustup output: {}", e)))?;
let path = stdout.trim();
Ok(std::path::PathBuf::from(path))
}
pub fn show_active_toolchain(&self) -> Result<String> {
let output = Command::new(&self.rustup_path)
.args(["show", "active-toolchain"])
.output()
.map_err(|e| Error::Toolchain(format!("Failed to get active toolchain: {}", e)))?;
if !output.status.success() {
return Err(Error::Toolchain(
"Failed to determine active toolchain".to_string()
));
}
let stdout = str::from_utf8(&output.stdout)
.map_err(|e| Error::Toolchain(format!("Invalid UTF-8 in rustup output: {}", e)))?;
Ok(stdout.trim().to_string())
}
}
impl Default for ToolchainManager {
fn default() -> Self {
Self::new().expect("Failed to initialize ToolchainManager")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_rustup() {
let result = ToolchainManager::find_rustup();
assert!(result.is_ok());
assert_eq!(result.unwrap(), "rustup");
}
#[test]
fn test_new_toolchain_manager() {
let manager = ToolchainManager::new();
assert!(manager.is_ok());
}
#[test]
fn test_list_toolchains() {
let manager = ToolchainManager::new();
if manager.is_err() {
return;
}
let manager = manager.unwrap();
let toolchains = manager.list_toolchains();
assert!(toolchains.is_ok());
let toolchains = toolchains.unwrap();
assert!(!toolchains.is_empty());
}
#[test]
fn test_get_default_toolchain() {
let manager = ToolchainManager::new();
if manager.is_err() {
return;
}
let manager = manager.unwrap();
let default_toolchain = manager.get_default_toolchain();
assert!(default_toolchain.is_ok());
}
#[test]
fn test_list_targets() {
let manager = ToolchainManager::new();
if manager.is_err() {
return;
}
let manager = manager.unwrap();
let targets = manager.list_targets("stable");
if targets.is_err() {
return;
}
let targets = targets.unwrap();
assert!(!targets.is_empty());
}
#[test]
fn test_is_toolchain_installed() {
let manager = ToolchainManager::new();
if manager.is_err() {
return;
}
let manager = manager.unwrap();
let is_installed = manager.is_toolchain_installed("stable");
assert!(is_installed.is_ok());
}
#[test]
fn test_get_rustup_home() {
let manager = ToolchainManager::new();
if manager.is_err() {
return;
}
let manager = manager.unwrap();
let home = manager.get_rustup_home();
assert!(home.is_ok());
let home = home.unwrap();
assert!(home.exists());
}
#[test]
fn test_show_active_toolchain() {
let manager = ToolchainManager::new();
if manager.is_err() {
return;
}
let manager = manager.unwrap();
let active = manager.show_active_toolchain();
assert!(active.is_ok());
let active = active.unwrap();
assert!(!active.is_empty());
}
#[test]
fn test_is_target_installed() {
let manager = ToolchainManager::new();
if manager.is_err() {
return;
}
let manager = manager.unwrap();
if let Ok(host) = Target::detect_host() {
let is_installed = manager.is_target_installed("stable", &host.triple);
if is_installed.is_ok() {
assert!(is_installed.unwrap());
}
}
}
}