leo_lang/cli/helpers/
updater.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use leo_errors::{CliError, Result};
18
19use aleo_std;
20
21use colored::Colorize;
22use self_update::{Status, backends::github, version::bump_is_greater};
23use std::{
24    fmt::Write as _,
25    fs,
26    path::{Path, PathBuf},
27    time::{Duration, SystemTime, UNIX_EPOCH},
28};
29
30pub struct Updater;
31
32// TODO Add logic for users to easily select release versions.
33impl Updater {
34    const LEO_BIN_NAME: &'static str = "leo";
35    const LEO_CACHE_LAST_CHECK_FILE: &'static str = "leo_cache_last_update_check";
36    const LEO_CACHE_VERSION_FILE: &'static str = "leo_cache_latest_version";
37    const LEO_REPO_NAME: &'static str = "leo";
38    const LEO_REPO_OWNER: &'static str = "ProvableHQ";
39    // 24 hours
40    const LEO_UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
41
42    /// Show all available releases for `leo`.
43    pub fn show_available_releases() -> Result<String> {
44        let releases = github::ReleaseList::configure()
45            .repo_owner(Self::LEO_REPO_OWNER)
46            .repo_name(Self::LEO_REPO_NAME)
47            .build()
48            .map_err(CliError::self_update_error)?
49            .fetch()
50            .map_err(CliError::could_not_fetch_versions)?;
51
52        let mut output = "\nList of available versions\n".to_string();
53        for release in releases {
54            let _ = writeln!(output, "  * {}", release.version);
55        }
56
57        Ok(output)
58    }
59
60    /// Update `leo` to the latest release.
61    pub fn update_to_latest_release(show_output: bool) -> Result<Status> {
62        let status = github::Update::configure()
63            .repo_owner(Self::LEO_REPO_OWNER)
64            .repo_name(Self::LEO_REPO_NAME)
65            .bin_name(Self::LEO_BIN_NAME)
66            .current_version(env!("CARGO_PKG_VERSION"))
67            .show_download_progress(show_output)
68            .no_confirm(true)
69            .show_output(show_output)
70            .build()
71            .map_err(CliError::self_update_build_error)?
72            .update()
73            .map_err(CliError::self_update_error)?;
74
75        Ok(status)
76    }
77
78    /// Check if there is an available update for `leo` and return the newest release.
79    pub fn update_available() -> Result<String> {
80        let updater = github::Update::configure()
81            .repo_owner(Self::LEO_REPO_OWNER)
82            .repo_name(Self::LEO_REPO_NAME)
83            .bin_name(Self::LEO_BIN_NAME)
84            .current_version(env!("CARGO_PKG_VERSION"))
85            .build()
86            .map_err(CliError::self_update_error)?;
87
88        let current_version = updater.current_version();
89        let latest_release = updater.get_latest_release().map_err(CliError::self_update_error)?;
90
91        if bump_is_greater(&current_version, &latest_release.version).map_err(CliError::self_update_error)? {
92            Ok(latest_release.version)
93        } else {
94            Err(CliError::old_release_version(current_version, latest_release.version).into())
95        }
96    }
97
98    /// Read the latest version from the version file.
99    pub fn read_latest_version() -> Result<Option<String>, CliError> {
100        let version_file_path = Self::get_version_file_path();
101        match fs::read_to_string(version_file_path) {
102            Ok(version) => Ok(Some(version.trim().to_string())),
103            Err(_) => Ok(None),
104        }
105    }
106
107    /// Generate the CLI message if a new version is available.
108    pub fn get_cli_string() -> Result<Option<String>, CliError> {
109        if let Some(latest_version) = Self::read_latest_version()? {
110            let colorized_message = format!(
111                "\n🟢 {} {} {}",
112                "A new version is available! Run".bold().green(),
113                "`leo update`".bold().white(),
114                format!("to update to v{}.", latest_version).bold().green()
115            );
116            Ok(Some(colorized_message))
117        } else {
118            Ok(None)
119        }
120    }
121
122    /// Display the CLI message if a new version is available.
123    pub fn print_cli() -> Result<(), CliError> {
124        if let Some(message) = Self::get_cli_string()? {
125            println!("{}", message);
126        }
127        Ok(())
128    }
129
130    /// Check for updates, respecting the update interval. (Currently once per day.)
131    /// If a new version is found, write it to a cache file and alert in every call.
132    pub fn check_for_updates(force: bool) -> Result<bool, CliError> {
133        // Get the cache directory and relevant file paths.
134        let cache_dir = Self::get_cache_dir();
135        let last_check_file = cache_dir.join(Self::LEO_CACHE_LAST_CHECK_FILE);
136        let version_file = Self::get_version_file_path();
137
138        // Determine if we should check for updates.
139        let should_check = force || Self::should_check_for_updates(&last_check_file)?;
140
141        if should_check {
142            match Self::update_available() {
143                Ok(latest_version) => {
144                    // A new version is available
145                    Self::update_check_files(&cache_dir, &last_check_file, &version_file, &latest_version)?;
146                    Ok(true)
147                }
148                Err(_) => {
149                    // No new version available or error occurred
150                    // We'll treat both cases as "no update" for simplicity
151                    Self::update_check_files(&cache_dir, &last_check_file, &version_file, env!("CARGO_PKG_VERSION"))?;
152                    Ok(false)
153                }
154            }
155        } else if version_file.exists() {
156            if let Ok(stored_version) = fs::read_to_string(&version_file) {
157                let current_version = env!("CARGO_PKG_VERSION");
158                Ok(bump_is_greater(current_version, stored_version.trim()).map_err(CliError::self_update_error)?)
159            } else {
160                // If we can't read the file, assume no update is available
161                Ok(false)
162            }
163        } else {
164            Ok(false)
165        }
166    }
167
168    /// Updates the check files with the latest version information and timestamp.
169    ///
170    /// This function creates the cache directory if it doesn't exist, writes the current time
171    /// to the last check file, and writes the latest version to the version file.
172    fn update_check_files(
173        cache_dir: &Path,
174        last_check_file: &Path,
175        version_file: &Path,
176        latest_version: &str,
177    ) -> Result<(), CliError> {
178        // Recursively create the cache directory and all of its parent components if they are missing.
179        fs::create_dir_all(cache_dir).map_err(CliError::cli_io_error)?;
180
181        // Get the current time.
182        let current_time = Self::get_current_time()?;
183
184        // Write the current time to the last check file.
185        fs::write(last_check_file, current_time.to_string()).map_err(CliError::cli_io_error)?;
186
187        // Write the latest version to the version file.
188        fs::write(version_file, latest_version).map_err(CliError::cli_io_error)?;
189
190        Ok(())
191    }
192
193    /// Determines if an update check should be performed based on the last check time.
194    ///
195    /// This function reads the last check timestamp from a file and compares it with
196    /// the current time to decide if enough time has passed for a new check.
197    fn should_check_for_updates(last_check_file: &Path) -> Result<bool, CliError> {
198        match fs::read_to_string(last_check_file) {
199            Ok(contents) => {
200                // Parse the last check timestamp from the file.
201                let last_check = contents
202                    .parse::<u64>()
203                    .map_err(|e| CliError::cli_runtime_error(format!("Failed to parse last check time: {}", e)))?;
204
205                // Get the current time.
206                let current_time = Self::get_current_time()?;
207
208                // Check if enough time has passed since the last check.
209                Ok(current_time.saturating_sub(last_check) > Self::LEO_UPDATE_CHECK_INTERVAL.as_secs())
210            }
211            // If we can't read the file, assume we should check
212            Err(_) => Ok(true),
213        }
214    }
215
216    /// Gets the current system time as seconds since the Unix epoch.
217    fn get_current_time() -> Result<u64, CliError> {
218        SystemTime::now()
219            .duration_since(UNIX_EPOCH)
220            .map_err(|e| CliError::cli_runtime_error(format!("System time error: {}", e)))
221            .map(|duration| duration.as_secs())
222    }
223
224    /// Get the path to the file storing the latest version information.
225    fn get_version_file_path() -> PathBuf {
226        Self::get_cache_dir().join(Self::LEO_CACHE_VERSION_FILE)
227    }
228
229    /// Get the cache directory for Leo.
230    fn get_cache_dir() -> PathBuf {
231        aleo_std::aleo_dir().join("leo")
232    }
233}