cargo_upgrade_command/
lib.rs

1use std::env;
2use std::fs::{create_dir, remove_dir_all, rename};
3use std::io::{BufRead, BufReader, Result};
4
5use std::process::{Command, Stdio};
6use std::time::Instant;
7
8use colored::Colorize;
9use spinoff::{spinners, Color, Spinner};
10
11pub struct Package {
12    name: String,
13    version: String,
14    new_version: Option<String>,
15}
16
17impl Package {
18    fn to_formatted(&self) -> Self {
19        let name = self.name.to_string();
20        let old_version = format!("v{}", self.version).bright_red();
21        let new_version = format!("v{}", self.new_version.as_ref().unwrap()).bright_green();
22
23        Self {
24            name,
25            version: old_version.to_string(),
26            new_version: Some(new_version.to_string()),
27        }
28    }
29}
30
31/// # Errors
32/// Will return `Err` if the command fails to execute
33///
34/// # Panics
35/// Will panic if the command fails to execute
36pub fn get_installed_packages() -> Result<Vec<Package>> {
37    let output = Command::new("cargo").args(["install", "--list"]).output()?;
38
39    let text = String::from_utf8_lossy(&output.stdout);
40
41    let mut packages = Vec::new();
42    for line in text.lines() {
43        if line.ends_with(':') {
44            let parts: Vec<_> = line.splitn(2, ' ').collect();
45            if parts.len() == 2 && parts[1].starts_with('v') {
46                let name = parts[0].trim().to_string();
47                let version = parts[1]
48                    .trim()
49                    .trim_end_matches(':')
50                    .trim_start_matches('v')
51                    .to_string();
52                packages.push(Package {
53                    name,
54                    version,
55                    new_version: None,
56                });
57            }
58        }
59    }
60
61    Ok(packages)
62}
63
64/// # Errors
65/// Will return `Err` if the command fails to execute
66///
67/// # Panics
68/// Will panic if the command fails to execute
69pub fn get_outdated_packages() -> Result<Vec<Package>> {
70    let spinner = Spinner::new(
71        spinners::Dots,
72        "Scanning for outdated crates...",
73        Color::Cyan,
74    );
75
76    let packages = get_installed_packages()?;
77
78    let mut outdated_packages = Vec::new();
79
80    for package in &packages {
81        let output = Command::new("cargo")
82            .args(["search", &package.name, "--limit=1", "--color=never", "-q"])
83            .output()?;
84        let text = String::from_utf8_lossy(&output.stdout);
85
86        let prefix = format!("{} = \"", package.name);
87
88        if !text.starts_with(&prefix) {
89            continue;
90        }
91
92        let value_start = prefix.len();
93        let quote_end = text[value_start..].find('"').unwrap();
94        let latest_version = text[value_start..value_start + quote_end].to_string();
95
96        if latest_version != package.version {
97            outdated_packages.push(Package {
98                name: package.name.to_string(),
99                version: package.version.clone(),
100                new_version: Some(latest_version),
101            });
102        }
103    }
104
105    spinner.clear();
106
107    Ok(outdated_packages)
108}
109
110/// # Errors
111/// Will return `Err` if the command fails to execute
112///
113/// # Panics
114/// Will panic if the command fails to execute
115pub fn show_outdated_packages() -> Result<()> {
116    let outdated_packages = get_outdated_packages()?;
117    if outdated_packages.is_empty() {
118        return Ok(());
119    }
120    println!("Outdated global cargo crates:");
121    println!("===============================");
122    for package in outdated_packages {
123        let formatted = package.to_formatted();
124
125        println!(
126            "📦 {}: {} -> {}",
127            formatted.name,
128            formatted.version,
129            formatted.new_version.unwrap()
130        );
131    }
132
133    Ok(())
134}
135
136/// # Errors
137///
138/// Will return `Err` if the command fails to execute
139///
140/// # Panics
141/// Will panic if the command fails to execute
142pub fn update_package(name: &str) -> Result<()> {
143    let mut spinner = Spinner::new(spinners::Dots, "Loading...", Color::Cyan);
144
145    let start_time = Instant::now();
146
147    let mut cmd = Command::new("cargo")
148        .args(["install", name, "--locked"])
149        .stderr(Stdio::piped())
150        .stdout(Stdio::piped())
151        .stdin(Stdio::piped())
152        .spawn()?;
153
154    let reader = BufReader::new(cmd.stderr.take().unwrap());
155
156    let mut last_line = String::new();
157
158    for line in reader.lines() {
159        last_line = line?;
160        spinner.update_text(last_line.trim().to_string());
161    }
162
163    let status_code = cmd.wait()?;
164
165    let status = format!("{} [{:.2?}]", last_line.trim(), start_time.elapsed());
166
167    match status_code.code().unwrap_or(1) {
168        0 => spinner.success(&status),
169        1 => spinner.fail(&status),
170        _ => spinner.warn(&status),
171    }
172
173    Ok(())
174}
175
176/// # Errors
177/// Will return `Err` if the command fails to execute
178///
179/// # Panics
180/// Will panic if the command fails to execute
181pub fn update_all_packages() -> Result<()> {
182    let packages = get_outdated_packages()?;
183
184    let mut done_one = false;
185
186    for package in packages {
187        if package.new_version.is_none() {
188            continue;
189        }
190        let formatted = package.to_formatted();
191        if done_one {
192            println!();
193        }
194        if package.name == env!("CARGO_PKG_NAME") {
195            if cfg!(debug_assertions) {
196                println!("Skipping self update in debug mode");
197                continue;
198            }
199            move_executable_to_temp_folder()?;
200        }
201        println!(
202            "Upgrading {} from {} to {}",
203            formatted.name,
204            formatted.version,
205            formatted.new_version.unwrap()
206        );
207        update_package(&package.name)?;
208        done_one = true;
209    }
210
211    Ok(())
212}
213
214fn move_executable_to_temp_folder() -> Result<()> {
215    let current_exe = env::current_exe()?;
216    let temp_dir = env::temp_dir();
217
218    // Generate a unique file name for the executable in the temp directory
219    let cloned_exe_dir = temp_dir.join(env!("CARGO_PKG_NAME"));
220    if cloned_exe_dir.exists() {
221        remove_dir_all(&cloned_exe_dir)?;
222    }
223
224    create_dir(&cloned_exe_dir)?;
225
226    let mut cloned_exe_path = cloned_exe_dir.join(current_exe.file_name().unwrap());
227    let mut i = 0;
228    while cloned_exe_path.exists() {
229        i += 1;
230
231        cloned_exe_path = cloned_exe_dir.join(format!(
232            "{}-{i}",
233            current_exe.file_stem().unwrap().to_str().unwrap()
234        ));
235        if cfg!(windows) {
236            cloned_exe_path.set_extension("exe");
237        }
238    }
239
240    // Move the current executable to the temp directory
241    rename(&current_exe, &cloned_exe_path)?;
242
243    Ok(())
244}