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 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 return SelfUpdateMode::Unmanaged(None);
75 }
76
77 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 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 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 let mut updater = AXOUPDATER.lock().await;
109 updater.run().await?;
110 Ok(())
111 }
112 SelfUpdateMode::Cargo => {
113 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 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}