use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::commands::upgrade_details::{UpgradeDetail, UpgradeDetails, UpgradeDetailsManager};
use crate::runner::Runner;
#[derive(Debug, Deserialize, Serialize)]
struct ToolchainVersion {
name: String,
version: String,
}
fn get_toolchain_versions_json(
runner: &dyn Runner,
tmpdir: &Path,
) -> Result<Vec<ToolchainVersion>> {
match get_toolchain_versions_from_show(runner, tmpdir) {
Ok(versions) => {
if !versions.is_empty() {
return Ok(versions);
}
}
Err(e) => {
if let Ok(mut file) = File::create(tmpdir.join("rustup_errors.log")) {
let _ = writeln!(file, "rustup show method failed: {}", e);
}
}
}
match get_toolchain_versions_from_list(runner, tmpdir) {
Ok(versions) => Ok(versions),
Err(e) => {
if let Ok(mut file) = File::create(tmpdir.join("rustup_errors.log")) {
let _ = writeln!(file, "All methods failed: {}", e);
}
Ok(Vec::new())
}
}
}
fn get_toolchain_versions_from_show(
runner: &dyn Runner,
tmpdir: &Path,
) -> Result<Vec<ToolchainVersion>> {
let mut versions = Vec::new();
let (_, toolchains_output) =
runner.run("rustup show", &tmpdir.join("rustup_show.log"), false)?;
if toolchains_output.trim().is_empty() {
return Err(anyhow::anyhow!("Empty output from rustup show"));
}
for line in toolchains_output.lines() {
let line = line.trim();
if line.contains("stable-") || line.contains("nightly-") || line.contains("beta-") {
if let Some(toolchain) = line.split_whitespace().next() {
if !toolchain.is_empty() && toolchain.len() > 3 {
let cmd = format!("rustup run {} rustc --version", toolchain);
match runner.run(&cmd, &tmpdir.join("toolchain_version.log"), false) {
Ok((_, version_output)) => {
if let Some(version) = extract_rust_version(&version_output) {
if !version.is_empty() && version.contains('.') {
versions.push(ToolchainVersion {
name: toolchain.to_string(),
version,
});
}
}
}
Err(e) => {
if let Ok(mut file) = File::create(tmpdir.join("rustup_errors.log")) {
let _ = writeln!(
file,
"Failed to get version for {}: {}",
toolchain, e
);
}
}
}
}
}
}
}
let json_file = tmpdir.join("toolchain_versions.json");
if let Ok(mut file) = File::create(&json_file) {
let _ = writeln!(file, "{}", serde_json::to_string_pretty(&versions)?);
}
Ok(versions)
}
fn get_toolchain_versions_from_list(
runner: &dyn Runner,
tmpdir: &Path,
) -> Result<Vec<ToolchainVersion>> {
let mut versions = Vec::new();
let (_, toolchains_output) = runner.run(
"rustup toolchain list",
&tmpdir.join("rustup_toolchain_list.log"),
false,
)?;
if toolchains_output.trim().is_empty() {
return Err(anyhow::anyhow!("Empty output from rustup toolchain list"));
}
for line in toolchains_output.lines() {
let line = line.trim();
if line.contains("stable-") || line.contains("nightly-") || line.contains("beta-") {
let toolchain = line.split_whitespace().next().unwrap_or("").to_string();
if !toolchain.is_empty() {
let cmd = format!("rustup run {} rustc --version", toolchain);
match runner.run(&cmd, &tmpdir.join("toolchain_version.log"), false) {
Ok((_, version_output)) => {
if let Some(version) = extract_rust_version(&version_output) {
if !version.is_empty() && version.contains('.') {
versions.push(ToolchainVersion {
name: toolchain,
version,
});
}
}
}
Err(e) => {
if let Ok(mut file) = File::create(tmpdir.join("rustup_errors.log")) {
let _ =
writeln!(file, "Failed to get version for {}: {}", toolchain, e);
}
}
}
}
}
}
let json_file = tmpdir.join("toolchain_versions_list.json");
if let Ok(mut file) = File::create(&json_file) {
let _ = writeln!(file, "{}", serde_json::to_string_pretty(&versions)?);
}
Ok(versions)
}
fn extract_rust_version(version_output: &str) -> Option<String> {
version_output
.split_whitespace()
.nth(1)
.filter(|_| version_output.starts_with("rustc"))
.map(|s| s.to_string())
}
fn detect_version_changes(before: &[ToolchainVersion], after: &[ToolchainVersion]) -> bool {
for before_tc in before {
if let Some(after_tc) = after.iter().find(|tc| tc.name == before_tc.name) {
if before_tc.version != after_tc.version {
return true;
}
}
}
for after_tc in after {
if !before.iter().any(|tc| tc.name == after_tc.name) {
return true;
}
}
false
}
fn detect_output_indicators(output: &str) -> bool {
let out_text = output.to_lowercase();
if out_text.contains("unchanged") || out_text.contains("up to date") {
return false;
}
let update_indicators = [
"updated",
"upgraded",
"installed",
"downloaded",
"installing",
"downloading",
];
if update_indicators
.iter()
.any(|indicator| out_text.contains(indicator))
{
return true;
}
if out_text.contains("->") || out_text.contains("→") {
return true;
}
if out_text.contains("from") && out_text.contains("to") && !out_text.contains("up to date") {
return true;
}
false
}
fn determine_upgrade_status(
versions_before: &[ToolchainVersion],
versions_after: &[ToolchainVersion],
output: &str,
) -> bool {
let has_version_changes = detect_version_changes(versions_before, versions_after);
let has_output_indicators = detect_output_indicators(output);
has_version_changes || has_output_indicators
}
pub fn rustup_update(
runner: &dyn Runner,
tmpdir: &Path,
verbose: bool,
) -> Result<(String, i32, PathBuf)> {
let logfile = tmpdir.join("rustup_update.log");
let versions_before = get_toolchain_versions_json(runner, tmpdir)?;
let (rc, out) = runner.run("rustup update", &logfile, verbose)?;
if rc != 0 {
return Ok(("failed".to_string(), rc, logfile));
}
let versions_after = get_toolchain_versions_json(runner, tmpdir)?;
let has_upgrade = determine_upgrade_status(&versions_before, &versions_after, &out);
let mut upgrade_details = Vec::new();
if has_upgrade {
for before_tc in &versions_before {
if let Some(after_tc) = versions_after.iter().find(|tc| tc.name == before_tc.name) {
if before_tc.version != after_tc.version {
upgrade_details.push(UpgradeDetail::version_upgrade(
before_tc.name.clone(),
before_tc.version.clone(),
after_tc.version.clone(),
));
}
}
}
for after_tc in &versions_after {
if !versions_before.iter().any(|tc| tc.name == after_tc.name) {
upgrade_details.push(UpgradeDetail::new_installation(
after_tc.name.clone(),
after_tc.version.clone(),
));
}
}
}
let mut details = UpgradeDetails::new("Rustup".to_string());
details.add_details(upgrade_details);
if details.has_upgrades() {
let _ = UpgradeDetailsManager::save_upgrade_details(&details, tmpdir, "rustup");
}
let state = if has_upgrade { "changed" } else { "unchanged" };
Ok((state.to_string(), rc, logfile))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_rust_version() {
let output = "rustc 1.70.0 (90c541806 2023-05-31)";
let result = extract_rust_version(output);
assert_eq!(result, Some("1.70.0".to_string()));
}
#[test]
fn test_extract_rust_version_invalid() {
let output = "1.70.0 (90c541806 2023-05-31)";
let result = extract_rust_version(output);
assert_eq!(result, None);
}
#[test]
fn test_extract_rust_version_with_beta() {
let output = "rustc 1.71.0-beta.1 (a2b1646c 2023-06-03)";
let result = extract_rust_version(output);
assert_eq!(result, Some("1.71.0-beta.1".to_string()));
}
#[test]
fn test_extract_rust_version_empty() {
let output = "";
let result = extract_rust_version(output);
assert_eq!(result, None);
}
#[test]
fn test_detect_version_changes_with_version_upgrade() {
let before = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
}];
let after = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.71.0".to_string(),
}];
assert!(detect_version_changes(&before, &after));
}
#[test]
fn test_detect_version_changes_with_new_installation() {
let before = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
}];
let after = vec![
ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
},
ToolchainVersion {
name: "nightly".to_string(),
version: "1.72.0".to_string(),
},
];
assert!(detect_version_changes(&before, &after));
}
#[test]
fn test_detect_version_changes_no_changes() {
let before = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
}];
let after = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
}];
assert!(!detect_version_changes(&before, &after));
}
#[test]
fn test_detect_output_indicators_with_updates() {
let output = "info: downloading component 'rustc' for 'stable-x86_64-apple-darwin'\ninfo: installing component 'rustc' for 'stable-x86_64-apple-darwin'";
assert!(detect_output_indicators(output));
}
#[test]
fn test_detect_output_indicators_with_arrows() {
let output = "stable-x86_64-apple-darwin updated -> 1.71.0";
assert!(detect_output_indicators(output));
}
#[test]
fn test_detect_output_indicators_no_updates() {
let output = "info: all toolchains are up to date";
assert!(!detect_output_indicators(output));
}
#[test]
fn test_determine_upgrade_status_with_version_changes() {
let before = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
}];
let after = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.71.0".to_string(),
}];
let output = "info: all toolchains are up to date";
assert!(determine_upgrade_status(&before, &after, output));
}
#[test]
fn test_determine_upgrade_status_with_output_indicators() {
let before = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
}];
let after = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
}];
let output = "info: downloading component 'rustc'";
assert!(determine_upgrade_status(&before, &after, output));
}
#[test]
fn test_determine_upgrade_status_no_changes() {
let before = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
}];
let after = vec![ToolchainVersion {
name: "stable".to_string(),
version: "1.70.0".to_string(),
}];
let output = "info: all toolchains are up to date";
assert!(!determine_upgrade_status(&before, &after, output));
}
}