Skip to main content

cargo_v5/
self_update.rs

1use std::{
2    borrow::Cow,
3    env::{self, consts::EXE_SUFFIX},
4    path::{Path, PathBuf},
5    sync::LazyLock,
6};
7
8use axoupdater::{AxoUpdater, AxoupdateError};
9use miette::Diagnostic;
10use thiserror::Error;
11use tokio::{process::Command, sync::Mutex, task::block_in_place};
12
13#[derive(Debug, Error, Diagnostic)]
14pub enum SelfUpdateError {
15    #[error("cargo-v5's updates are externally managed")]
16    #[diagnostic(code(cargo_v5::self_update::unavailable))]
17    SelfUpdateUnavailable {
18        #[help]
19        advice: &'static str,
20    },
21
22    #[error("Self-update failed")]
23    #[diagnostic(code(cargo_v5::self_update::failure))]
24    Axoupdate(#[from] AxoupdateError),
25    #[error("Failed to run the update command")]
26    #[diagnostic(code(cargo_v5::self_update::io))]
27    Io(#[from] std::io::Error),
28}
29
30static AXOUPDATER: LazyLock<Mutex<AxoUpdater>> =
31    LazyLock::new(|| Mutex::new(AxoUpdater::new_for("cargo-v5")));
32pub static CURRENT_MODE: LazyLock<SelfUpdateMode> = LazyLock::new(SelfUpdateMode::current);
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum SelfUpdateMode {
36    Axoupdate,
37    Cargo,
38    Unmanaged(Option<ExternalUpdateManager>),
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ExternalUpdateManager {
43    Homebrew,
44}
45
46fn cargo_bin_path() -> Option<PathBuf> {
47    let cargo_home = env::var("CARGO_HOME")
48        .map(PathBuf::from)
49        .ok()
50        .or_else(|| env::home_dir().map(|home| home.join(".cargo")))?;
51
52    Some(cargo_home.join("bin"))
53}
54
55fn exe_name<'a>(string: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
56    if EXE_SUFFIX.is_empty() {
57        string.into()
58    } else {
59        Cow::Owned(format!("{}{}", string.into(), EXE_SUFFIX))
60    }
61}
62
63impl SelfUpdateMode {
64    pub fn current() -> Self {
65        // Check if installed by shell script
66        let mut updater = block_in_place(|| AXOUPDATER.blocking_lock());
67        if updater.load_receipt().is_ok() {
68            return Self::Axoupdate;
69        }
70
71        let this_arg = std::env::args().next().unwrap_or_default();
72        if this_arg.is_empty() {
73            // Not enough information
74            return SelfUpdateMode::Unmanaged(None);
75        }
76
77        // Check if managed by cargo
78        if let Some(bin_path) = cargo_bin_path()
79            && let Ok(expected_exe_path) =
80                bin_path.join(exe_name("cargo-v5").as_ref()).canonicalize()
81            && let Ok(exe_path) = Path::new(&this_arg).canonicalize()
82            && expected_exe_path == exe_path
83        {
84            return Self::Cargo;
85        }
86
87        // Check if managed by homebrew
88        let homebrew_prefix =
89            env::var("HOMEBREW_PREFIX").unwrap_or_else(|_| "/opt/homebrew/bin/".to_string());
90        if this_arg.starts_with(&homebrew_prefix) {
91            return SelfUpdateMode::Unmanaged(Some(ExternalUpdateManager::Homebrew));
92        }
93
94        // Idk
95        SelfUpdateMode::Unmanaged(None)
96    }
97}
98
99pub async fn self_update() -> Result<(), SelfUpdateError> {
100    eprintln!("Checking for updates...");
101
102    let mode = *CURRENT_MODE;
103
104    match mode {
105        SelfUpdateMode::Axoupdate => {
106            // This will redownload the installer shell script and run it again
107
108            let mut updater = AXOUPDATER.lock().await;
109            updater.run().await?;
110            Ok(())
111        }
112        SelfUpdateMode::Cargo => {
113            // Just spawn a cargo command to update for us
114
115            let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
116
117            let cargo_binstall_path =
118                cargo_bin_path().map(|p| p.join(exe_name("cargo-binstall").as_ref()));
119
120            let mut command = Command::new(cargo);
121
122            if let Some(cargo_binstall_path) = cargo_binstall_path
123                && let Ok(canonical_path) = cargo_binstall_path.canonicalize()
124                && canonical_path.exists()
125            {
126                // Update with cargo-binstall because it's installed and faster
127                command.arg("binstall");
128            } else {
129                command.arg("install").arg("--locked");
130            }
131            command.arg("cargo-v5");
132
133            eprintln!("> {:?}", command.as_std());
134
135            command.spawn()?.wait().await?;
136
137            Ok(())
138        }
139        SelfUpdateMode::Unmanaged(manager) => Err(SelfUpdateError::SelfUpdateUnavailable {
140            advice: match manager {
141                Some(ExternalUpdateManager::Homebrew) => "run `brew upgrade cargo-v5`",
142                None => "update cargo-v5 with your package manager or redownload the executable",
143            },
144        }),
145    }
146}