ferrous_forge/rust_version/
rustup.rs1use crate::config::locking::HierarchicalLockManager;
10use crate::rust_version::RustVersion;
11use crate::rust_version::detector::{
12 get_active_toolchain, get_installed_toolchains, is_rustup_available,
13};
14use crate::{Error, Result};
15use semver::Version;
16use serde::{Deserialize, Serialize};
17use std::process::Command;
18use tracing::{debug, info};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub enum ToolchainChannel {
23 Stable,
25 Beta,
27 Nightly,
29 Version(String),
31 Custom(String),
33}
34
35impl std::fmt::Display for ToolchainChannel {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::Stable => write!(f, "stable"),
39 Self::Beta => write!(f, "beta"),
40 Self::Nightly => write!(f, "nightly"),
41 Self::Version(v) => write!(f, "{}", v),
42 Self::Custom(s) => write!(f, "{}", s),
43 }
44 }
45}
46
47impl ToolchainChannel {
48 pub fn parse(channel: &str) -> Self {
50 match channel.to_lowercase().as_str() {
51 "stable" => Self::Stable,
52 "beta" => Self::Beta,
53 "nightly" => Self::Nightly,
54 s => {
55 if s.chars().next().map_or(false, |c| c.is_ascii_digit()) {
57 Self::Version(s.to_string())
58 } else {
59 Self::Custom(s.to_string())
60 }
61 }
62 }
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ToolchainInfo {
69 pub channel: ToolchainChannel,
71 pub is_default: bool,
73 pub is_installed: bool,
75}
76
77#[derive(Debug, Clone)]
79pub struct VersionRequirements {
80 pub minimum: Option<Version>,
82 pub maximum: Option<Version>,
84 pub exact: Option<Version>,
86}
87
88impl VersionRequirements {
89 pub fn new() -> Self {
91 Self {
92 minimum: None,
93 maximum: None,
94 exact: None,
95 }
96 }
97
98 pub fn check(&self, version: &Version) -> bool {
100 if let Some(exact) = &self.exact {
101 return version == exact;
102 }
103
104 if let Some(minimum) = &self.minimum {
105 if version < minimum {
106 return false;
107 }
108 }
109
110 if let Some(maximum) = &self.maximum {
111 if version > maximum {
112 return false;
113 }
114 }
115
116 true
117 }
118
119 pub fn description(&self) -> String {
121 if let Some(exact) = &self.exact {
122 return format!("exactly {}", exact);
123 }
124
125 match (&self.minimum, &self.maximum) {
126 (Some(min), Some(max)) => format!("between {} and {}", min, max),
127 (Some(min), None) => format!(">= {}", min),
128 (None, Some(max)) => format!("<= {}", max),
129 (None, None) => "any version".to_string(),
130 }
131 }
132}
133
134impl Default for VersionRequirements {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140pub struct RustupManager;
142
143impl RustupManager {
144 pub fn new() -> Self {
146 Self
147 }
148
149 pub fn is_available(&self) -> bool {
151 is_rustup_available()
152 }
153
154 fn ensure_rustup(&self) -> Result<()> {
156 if !self.is_available() {
157 return Err(Error::rust_not_found(
158 "rustup not found. Please install rustup from https://rustup.rs",
159 ));
160 }
161 Ok(())
162 }
163
164 pub async fn get_current_version(&self) -> Result<RustVersion> {
170 crate::rust_version::detector::detect_rust_version()
171 }
172
173 pub fn list_toolchains(&self) -> Result<Vec<ToolchainInfo>> {
179 self.ensure_rustup()?;
180
181 let active = get_active_toolchain()?;
182 let installed = get_installed_toolchains()?;
183
184 let toolchains: Vec<ToolchainInfo> = installed
185 .into_iter()
186 .map(|name| {
187 let channel = ToolchainChannel::parse(&name);
188 let is_default = name == active;
189
190 ToolchainInfo {
191 channel,
192 is_default,
193 is_installed: true,
194 }
195 })
196 .collect();
197
198 Ok(toolchains)
199 }
200
201 pub async fn install_toolchain(&self, channel: &ToolchainChannel) -> Result<()> {
207 self.ensure_rustup()?;
208
209 let channel_str = channel.to_string();
210 info!("Installing toolchain: {}", channel_str);
211
212 let output = Command::new("rustup")
213 .args(["toolchain", "install", &channel_str, "--no-self-update"])
214 .output()
215 .map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
216
217 if !output.status.success() {
218 let stderr = String::from_utf8_lossy(&output.stderr);
219 return Err(Error::command(format!(
220 "Failed to install toolchain '{}': {}",
221 channel_str, stderr
222 )));
223 }
224
225 info!("Successfully installed toolchain: {}", channel_str);
226 Ok(())
227 }
228
229 pub async fn uninstall_toolchain(&self, channel: &ToolchainChannel) -> Result<()> {
235 self.ensure_rustup()?;
236
237 let channel_str = channel.to_string();
238 info!("Uninstalling toolchain: {}", channel_str);
239
240 let output = Command::new("rustup")
241 .args(["toolchain", "uninstall", &channel_str])
242 .output()
243 .map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
244
245 if !output.status.success() {
246 let stderr = String::from_utf8_lossy(&output.stderr);
247 return Err(Error::command(format!(
248 "Failed to uninstall toolchain '{}': {}",
249 channel_str, stderr
250 )));
251 }
252
253 info!("Successfully uninstalled toolchain: {}", channel_str);
254 Ok(())
255 }
256
257 pub async fn switch_toolchain(&self, channel: &ToolchainChannel) -> Result<()> {
263 self.ensure_rustup()?;
264
265 let channel_str = channel.to_string();
266 info!("Switching to toolchain: {}", channel_str);
267
268 let output = Command::new("rustup")
269 .args(["default", &channel_str])
270 .output()
271 .map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
272
273 if !output.status.success() {
274 let stderr = String::from_utf8_lossy(&output.stderr);
275 return Err(Error::command(format!(
276 "Failed to switch to toolchain '{}': {}",
277 channel_str, stderr
278 )));
279 }
280
281 info!("Successfully switched to toolchain: {}", channel_str);
282 Ok(())
283 }
284
285 pub async fn update_toolchains(&self) -> Result<UpdateResult> {
291 self.ensure_rustup()?;
292
293 info!("Updating toolchains...");
294
295 let output = Command::new("rustup")
296 .args(["update", "--no-self-update"])
297 .output()
298 .map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
299
300 let stdout = String::from_utf8_lossy(&output.stdout);
301 let stderr = String::from_utf8_lossy(&output.stderr);
302
303 if !output.status.success() {
304 return Err(Error::command(format!(
305 "Failed to update toolchains: {}",
306 stderr
307 )));
308 }
309
310 let updated = stdout
312 .lines()
313 .chain(stderr.lines())
314 .filter(|line| line.contains("updated") || line.contains("installed"))
315 .map(|s| s.to_string())
316 .collect();
317
318 info!("Toolchain update completed");
319
320 Ok(UpdateResult {
321 success: true,
322 updated,
323 })
324 }
325
326 pub async fn install_component(&self, component: &str, toolchain: Option<&str>) -> Result<()> {
332 self.ensure_rustup()?;
333
334 let mut args = vec!["component", "add", component];
335 if let Some(tc) = toolchain {
336 args.push("--toolchain");
337 args.push(tc);
338 }
339
340 info!("Installing component '{}'", component);
341
342 let output = Command::new("rustup")
343 .args(&args)
344 .output()
345 .map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
346
347 if !output.status.success() {
348 let stderr = String::from_utf8_lossy(&output.stderr);
349 return Err(Error::command(format!(
350 "Failed to install component '{}': {}",
351 component, stderr
352 )));
353 }
354
355 info!("Successfully installed component '{}'", component);
356 Ok(())
357 }
358
359 pub async fn get_version_requirements(&self) -> Result<VersionRequirements> {
365 let lock_manager = HierarchicalLockManager::load().await?;
366 let mut requirements = VersionRequirements::new();
367
368 if let Some((_, entry)) = lock_manager.is_locked("rust-version") {
370 debug!("Found locked rust-version: {}", entry.value);
371 if let Ok(version) = Version::parse(&entry.value) {
372 requirements.minimum = Some(version);
375 }
376 }
377
378 if let Some((_, entry)) = lock_manager.is_locked("max-rust-version") {
380 debug!("Found locked max-rust-version: {}", entry.value);
381 if let Ok(version) = Version::parse(&entry.value) {
382 requirements.maximum = Some(version);
383 }
384 }
385
386 Ok(requirements)
387 }
388
389 pub async fn check_version_requirements(&self) -> Result<VersionCheckResult> {
395 let current = self.get_current_version().await?;
396 let requirements = self.get_version_requirements().await?;
397
398 let meets_requirements = requirements.check(¤t.version);
399
400 Ok(VersionCheckResult {
401 current: current.version,
402 requirements,
403 meets_requirements,
404 })
405 }
406
407 pub async fn self_update(&self) -> Result<()> {
413 self.ensure_rustup()?;
414
415 info!("Running rustup self-update...");
416
417 let output = Command::new("rustup")
418 .args(["self", "update"])
419 .output()
420 .map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
421
422 if !output.status.success() {
423 let stderr = String::from_utf8_lossy(&output.stderr);
424 return Err(Error::command(format!(
425 "Failed to self-update rustup: {}",
426 stderr
427 )));
428 }
429
430 info!("Rustup self-update completed");
431 Ok(())
432 }
433
434 pub fn show_active_toolchain(&self) -> Result<String> {
440 self.ensure_rustup()?;
441
442 let output = Command::new("rustup")
443 .args(["show", "active-toolchain"])
444 .output()
445 .map_err(|e| Error::command(format!("Failed to run rustup: {}", e)))?;
446
447 if !output.status.success() {
448 let stderr = String::from_utf8_lossy(&output.stderr);
449 return Err(Error::command(format!(
450 "Failed to show active toolchain: {}",
451 stderr
452 )));
453 }
454
455 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
456 }
457}
458
459impl Default for RustupManager {
460 fn default() -> Self {
461 Self::new()
462 }
463}
464
465#[derive(Debug, Clone)]
467pub struct UpdateResult {
468 pub success: bool,
470 pub updated: Vec<String>,
472}
473
474#[derive(Debug, Clone)]
476pub struct VersionCheckResult {
477 pub current: Version,
479 pub requirements: VersionRequirements,
481 pub meets_requirements: bool,
483}
484
485impl VersionCheckResult {
486 pub fn status_message(&self) -> String {
488 if self.meets_requirements {
489 format!(
490 "✅ Current version {} meets requirements ({})",
491 self.current,
492 self.requirements.description()
493 )
494 } else {
495 format!(
496 "❌ Current version {} does NOT meet requirements ({})",
497 self.current,
498 self.requirements.description()
499 )
500 }
501 }
502}
503
504#[cfg(test)]
505#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
506mod tests {
507 use super::*;
508
509 #[test]
510 fn test_toolchain_channel_display() {
511 assert_eq!(ToolchainChannel::Stable.to_string(), "stable");
512 assert_eq!(ToolchainChannel::Beta.to_string(), "beta");
513 assert_eq!(ToolchainChannel::Nightly.to_string(), "nightly");
514 assert_eq!(
515 ToolchainChannel::Version("1.70.0".to_string()).to_string(),
516 "1.70.0"
517 );
518 assert_eq!(
519 ToolchainChannel::Custom("my-toolchain".to_string()).to_string(),
520 "my-toolchain"
521 );
522 }
523
524 #[test]
525 fn test_toolchain_channel_parse() {
526 assert!(matches!(
527 ToolchainChannel::parse("stable"),
528 ToolchainChannel::Stable
529 ));
530 assert!(matches!(
531 ToolchainChannel::parse("beta"),
532 ToolchainChannel::Beta
533 ));
534 assert!(matches!(
535 ToolchainChannel::parse("nightly"),
536 ToolchainChannel::Nightly
537 ));
538 assert!(matches!(
539 ToolchainChannel::parse("1.70.0"),
540 ToolchainChannel::Version(_)
541 ));
542 assert!(matches!(
543 ToolchainChannel::parse("custom-toolchain"),
544 ToolchainChannel::Custom(_)
545 ));
546 }
547
548 #[test]
549 fn test_version_requirements_check() {
550 let mut req = VersionRequirements::new();
551 let v170 = Version::new(1, 70, 0);
552 let v180 = Version::new(1, 80, 0);
553 let v190 = Version::new(1, 90, 0);
554
555 assert!(req.check(&v170));
557
558 req.minimum = Some(v180.clone());
560 assert!(!req.check(&v170));
561 assert!(req.check(&v180));
562 assert!(req.check(&v190));
563
564 req = VersionRequirements::new();
566 req.maximum = Some(v180.clone());
567 assert!(req.check(&v170));
568 assert!(req.check(&v180));
569 assert!(!req.check(&v190));
570
571 req = VersionRequirements::new();
573 req.exact = Some(v180.clone());
574 assert!(!req.check(&v170));
575 assert!(req.check(&v180));
576 assert!(!req.check(&v190));
577 }
578
579 #[test]
580 fn test_version_requirements_description() {
581 let mut req = VersionRequirements::new();
582 assert_eq!(req.description(), "any version");
583
584 req.minimum = Some(Version::new(1, 70, 0));
585 assert_eq!(req.description(), ">= 1.70.0");
586
587 req.maximum = Some(Version::new(1, 80, 0));
588 assert_eq!(req.description(), "between 1.70.0 and 1.80.0");
589
590 req = VersionRequirements::new();
591 req.exact = Some(Version::new(1, 75, 0));
592 assert_eq!(req.description(), "exactly 1.75.0");
593 }
594}