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, get_target, 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            .with_target(get_target())
48            .build()
49            .map_err(CliError::self_update_error)?
50            .fetch()
51            .map_err(CliError::could_not_fetch_versions)?;
52
53        let mut output = format!(
54            "\nList of available versions for: {}.\nUse the quoted name to select specific releases.\n\n",
55            get_target()
56        );
57        for release in releases {
58            let _ = writeln!(output, "  * {} | '{}'", release.version, release.name);
59        }
60
61        Ok(output)
62    }
63
64    /// Update `leo`. If a version is provided, then `leo` is updated to the specific version
65    /// otherwise the update defaults to the latest version.
66    pub fn update(show_output: bool, version: Option<String>) -> Result<Status> {
67        let mut update = github::Update::configure();
68        // Set the defaults.
69        update
70            .repo_owner(Self::LEO_REPO_OWNER)
71            .repo_name(Self::LEO_REPO_NAME)
72            .bin_name(Self::LEO_BIN_NAME)
73            .current_version(env!("CARGO_PKG_VERSION"))
74            .show_download_progress(show_output)
75            .no_confirm(true)
76            .show_output(show_output);
77        // Add the version if provided.
78        if let Some(version) = version {
79            update.target_version_tag(&version);
80        }
81        let status =
82            update.build().map_err(CliError::self_update_build_error)?.update().map_err(CliError::self_update_error)?;
83
84        Ok(status)
85    }
86
87    /// Check if there is an available update for `leo` and return the newest release.
88    pub fn update_available() -> Result<String> {
89        let updater = github::Update::configure()
90            .repo_owner(Self::LEO_REPO_OWNER)
91            .repo_name(Self::LEO_REPO_NAME)
92            .bin_name(Self::LEO_BIN_NAME)
93            .current_version(env!("CARGO_PKG_VERSION"))
94            .build()
95            .map_err(CliError::self_update_error)?;
96
97        let current_version = updater.current_version();
98        let latest_release = updater.get_latest_release().map_err(CliError::self_update_error)?;
99
100        if bump_is_greater(&current_version, &latest_release.version).map_err(CliError::self_update_error)? {
101            Ok(latest_release.version)
102        } else {
103            Err(CliError::old_release_version(current_version, latest_release.version).into())
104        }
105    }
106
107    /// Read the latest version from the version file.
108    pub fn read_latest_version() -> Result<Option<String>, CliError> {
109        let version_file_path = Self::get_version_file_path();
110        match fs::read_to_string(version_file_path) {
111            Ok(version) => Ok(Some(version.trim().to_string())),
112            Err(_) => Ok(None),
113        }
114    }
115
116    /// Generate the CLI message if a new version is available.
117    pub fn get_cli_string() -> Result<Option<String>, CliError> {
118        if let Some(latest_version) = Self::read_latest_version()? {
119            let colorized_message = format!(
120                "\n🟢 {} {} {}",
121                "A new version is available! Run".bold().green(),
122                "`leo update`".bold().white(),
123                format!("to update to v{latest_version}.").bold().green()
124            );
125            Ok(Some(colorized_message))
126        } else {
127            Ok(None)
128        }
129    }
130
131    /// Display the CLI message if a new version is available.
132    pub fn print_cli() -> Result<(), CliError> {
133        if let Some(message) = Self::get_cli_string()? {
134            println!("{message}");
135        }
136        Ok(())
137    }
138
139    /// Check for updates, respecting the update interval. (Currently once per day.)
140    /// If a new version is found, write it to a cache file and alert in every call.
141    pub fn check_for_updates(force: bool) -> Result<bool, CliError> {
142        // Get the cache directory and relevant file paths.
143        let cache_dir = Self::get_cache_dir();
144        let last_check_file = cache_dir.join(Self::LEO_CACHE_LAST_CHECK_FILE);
145        let version_file = Self::get_version_file_path();
146
147        // Determine if we should check for updates.
148        let should_check = force || Self::should_check_for_updates(&last_check_file)?;
149
150        if should_check {
151            match Self::update_available() {
152                Ok(latest_version) => {
153                    // A new version is available
154                    Self::update_check_files(&cache_dir, &last_check_file, &version_file, &latest_version)?;
155                    Ok(true)
156                }
157                Err(_) => {
158                    // No new version available or error occurred
159                    // We'll treat both cases as "no update" for simplicity
160                    Self::update_check_files(&cache_dir, &last_check_file, &version_file, env!("CARGO_PKG_VERSION"))?;
161                    Ok(false)
162                }
163            }
164        } else if version_file.exists() {
165            if let Ok(stored_version) = fs::read_to_string(&version_file) {
166                let current_version = env!("CARGO_PKG_VERSION");
167                Ok(bump_is_greater(current_version, stored_version.trim()).map_err(CliError::self_update_error)?)
168            } else {
169                // If we can't read the file, assume no update is available
170                Ok(false)
171            }
172        } else {
173            Ok(false)
174        }
175    }
176
177    /// Updates the check files with the latest version information and timestamp.
178    ///
179    /// This function creates the cache directory if it doesn't exist, writes the current time
180    /// to the last check file, and writes the latest version to the version file.
181    fn update_check_files(
182        cache_dir: &Path,
183        last_check_file: &Path,
184        version_file: &Path,
185        latest_version: &str,
186    ) -> Result<(), CliError> {
187        // Recursively create the cache directory and all of its parent components if they are missing.
188        fs::create_dir_all(cache_dir).map_err(CliError::cli_io_error)?;
189
190        // Get the current time.
191        let current_time = Self::get_current_time()?;
192
193        // Write the current time to the last check file.
194        fs::write(last_check_file, current_time.to_string()).map_err(CliError::cli_io_error)?;
195
196        // Write the latest version to the version file.
197        fs::write(version_file, latest_version).map_err(CliError::cli_io_error)?;
198
199        Ok(())
200    }
201
202    /// Determines if an update check should be performed based on the last check time.
203    ///
204    /// This function reads the last check timestamp from a file and compares it with
205    /// the current time to decide if enough time has passed for a new check.
206    fn should_check_for_updates(last_check_file: &Path) -> Result<bool, CliError> {
207        match fs::read_to_string(last_check_file) {
208            Ok(contents) => {
209                // Parse the last check timestamp from the file.
210                let last_check = contents
211                    .parse::<u64>()
212                    .map_err(|e| CliError::cli_runtime_error(format!("Failed to parse last check time: {e}")))?;
213
214                // Get the current time.
215                let current_time = Self::get_current_time()?;
216
217                // Check if enough time has passed since the last check.
218                Ok(current_time.saturating_sub(last_check) > Self::LEO_UPDATE_CHECK_INTERVAL.as_secs())
219            }
220            // If we can't read the file, assume we should check
221            Err(_) => Ok(true),
222        }
223    }
224
225    /// Gets the current system time as seconds since the Unix epoch.
226    fn get_current_time() -> Result<u64, CliError> {
227        SystemTime::now()
228            .duration_since(UNIX_EPOCH)
229            .map_err(|e| CliError::cli_runtime_error(format!("System time error: {e}")))
230            .map(|duration| duration.as_secs())
231    }
232
233    /// Get the path to the file storing the latest version information.
234    fn get_version_file_path() -> PathBuf {
235        Self::get_cache_dir().join(Self::LEO_CACHE_VERSION_FILE)
236    }
237
238    /// Get the cache directory for Leo.
239    fn get_cache_dir() -> PathBuf {
240        aleo_std::aleo_dir().join("leo")
241    }
242}