From cdbb3ee7b9efce69f5022bc7f17bbacbf3e4e1f1 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Thu, 23 Jan 2025 08:23:17 -0600 Subject: [PATCH] add `version check` command (#14880) # Description This PR supersedes https://github.com/nushell/nushell/pull/14813 by making it a built-in command instead of checking for the latest version at some interval when nushell starts. This is what it looks like. ![image](https://github.com/user-attachments/assets/35629425-b332-4078-aea5-4931cfb0471f) This example shows the output when the running version was 0.101.1-nightly.10 ![image](https://github.com/user-attachments/assets/71216635-fb75-4251-a443-bf0d0b9a1c07) Description from old PR. One key functionality that I thought was interesting with this and that I worked with @hustcer on was to try and make sure it works with nightlies. So, it should tell you when there's a new nightly version that is available to download. This way, you can know about it without checking. What's key from a nightly perspective is (1) the tags are now semver compliant and (2) hustcer now updates the Cargo.toml package.version version number prior to compilation so you can know you're running a nightly version, and this PR uses that information to know whether to check the nightly repo or the nushell repo for updates. This uses the [update-informer](https://docs.rs/update-informer/latest/update_informer/) crate. NOTE that this _informs_ you of updates but does not automatically update. I kind of see this as the first step to eventually having an auto updater. There was caching of the version in the old PR since it ran on every nushell startup. Since this PR makes it a command and therefore always runs on-demand, I've removed the caching so that it always checks when you run it. # User-Facing Changes # Tests + Formatting # After Submitting --- Cargo.lock | 56 +++++++ Cargo.toml | 1 + crates/nu-command/Cargo.toml | 22 +-- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/network/mod.rs | 5 + .../nu-command/src/network/version_check.rs | 156 ++++++++++++++++++ 6 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 crates/nu-command/src/network/version_check.rs diff --git a/Cargo.lock b/Cargo.lock index 7fe1c822b..22d0e50d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1787,6 +1787,17 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "ethnum" version = "1.5.0" @@ -2504,6 +2515,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -3768,6 +3795,7 @@ dependencies = [ "umask", "unicode-segmentation", "unicode-width 0.2.0", + "update-informer", "ureq", "url", "uu_cp", @@ -6026,6 +6054,7 @@ checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "h2 0.4.7", @@ -6034,11 +6063,13 @@ dependencies = [ "http-body-util", "hyper 1.5.1", "hyper-rustls 0.27.3", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -6052,6 +6083,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.1", "tokio-util", "tower-service", @@ -7346,6 +7378,16 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -7625,6 +7667,20 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "update-informer" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53813bf5d5f0d8430794f8cc48e99521cc9e298066958d16383ccb8b39d182a7" +dependencies = [ + "etcetera", + "reqwest", + "semver", + "serde", + "serde_json", + "ureq", +] + [[package]] name = "ureq" version = "2.12.1" diff --git a/Cargo.toml b/Cargo.toml index c4c2b0323..e9234036d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -162,6 +162,7 @@ tempfile = "3.15" titlecase = "3.0" toml = "0.8" trash = "5.2" +update-informer = { version = "1.2.0", default-features = false, features = ["github", "native-tls", "ureq"] } umask = "2.1" unicode-segmentation = "1.12" unicode-width = "0.2" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index bfbd09184..bd34f2f74 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -91,7 +91,8 @@ tabled = { workspace = true, features = ["ansi"], default-features = false } titlecase = { workspace = true } toml = { workspace = true, features = ["preserve_order"] } unicode-segmentation = { workspace = true } -ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json"] } +update-informer = { workspace = true, optional = true } +ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json", "native-tls"], optional = true } url = { workspace = true } uu_cp = { workspace = true, optional = true } uu_mkdir = { workspace = true, optional = true } @@ -142,7 +143,7 @@ os = [ # include other features "js", "network", - "nu-protocol/os", + "nu-protocol/os", "nu-utils/os", # os-dependant dependencies @@ -160,13 +161,13 @@ os = [ "which", ] -# The dependencies listed below need 'getrandom'. -# They work with JS (usually with wasm-bindgen) or regular OS support. +# The dependencies listed below need 'getrandom'. +# They work with JS (usually with wasm-bindgen) or regular OS support. # Hence they are also put under the 'os' feature to avoid repetition. js = [ - "getrandom", - "getrandom/js", - "rand", + "getrandom", + "getrandom/js", + "rand", "uuid", ] @@ -174,15 +175,16 @@ js = [ # interface requires openssl which is not easy to embed into wasm, # using rustls could solve this issue. network = [ - "multipart-rs", + "multipart-rs", "native-tls", - "ureq/native-tls", + "update-informer/native-tls", + "ureq", "uuid", ] plugin = [ "nu-parser/plugin", - "os", + "os", ] sqlite = ["rusqlite"] trash-support = ["trash"] diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index ca06a4c04..7175f7ff1 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -404,6 +404,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { HttpPut, HttpOptions, Port, + VersionCheck, } bind_command! { Url, diff --git a/crates/nu-command/src/network/mod.rs b/crates/nu-command/src/network/mod.rs index ea46fd3dc..2366fde59 100644 --- a/crates/nu-command/src/network/mod.rs +++ b/crates/nu-command/src/network/mod.rs @@ -3,6 +3,8 @@ mod http; #[cfg(feature = "network")] mod port; mod url; +#[cfg(feature = "network")] +mod version_check; #[cfg(feature = "network")] pub use self::http::*; @@ -10,3 +12,6 @@ pub use self::url::*; #[cfg(feature = "network")] pub use port::SubCommand as Port; + +#[cfg(feature = "network")] +pub use version_check::VersionCheck; diff --git a/crates/nu-command/src/network/version_check.rs b/crates/nu-command/src/network/version_check.rs new file mode 100644 index 000000000..180bde9fd --- /dev/null +++ b/crates/nu-command/src/network/version_check.rs @@ -0,0 +1,156 @@ +use nu_engine::command_prelude::*; +use serde::Deserialize; +use update_informer::{ + http_client::{GenericHttpClient, HttpClient}, + registry, Check, Package, Registry, Result as UpdateResult, +}; + +#[derive(Clone)] +pub struct VersionCheck; + +impl Command for VersionCheck { + fn name(&self) -> &str { + "version check" + } + + fn description(&self) -> &str { + "Checks to see if you have the latest version of nushell." + } + + fn extra_description(&self) -> &str { + "If you're running nushell nightly, `version check` will check to see if you are running the latest nightly version. If you are running the nushell release, `version check` will check to see if you're running the latest release version." + } + + fn signature(&self) -> Signature { + Signature::build("version check") + .category(Category::Platform) + .input_output_types(vec![(Type::Nothing, Type::String)]) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Check if you have the latest version of nushell", + example: "version check", + result: None, + }] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + let version_check = check_for_latest_nushell_version(); + Ok(version_check.into_pipeline_data()) + } +} + +pub struct NuShellNightly; + +impl Registry for NuShellNightly { + const NAME: &'static str = "nushell/nightly"; + + fn get_latest_version( + http_client: GenericHttpClient, + pkg: &Package, + ) -> UpdateResult> { + #[derive(Deserialize, Debug)] + struct Response { + tag_name: String, + } + + let url = format!("https://api.github.com/repos/{}/releases", pkg); + let versions = http_client + .add_header("Accept", "application/vnd.github.v3+json") + .add_header("User-Agent", "update-informer") + .get::>(&url)?; + + if let Some(v) = versions.first() { + // The nightly repo tags look like "0.101.1-nightly.4+23dc1b6" + // We want to return the "0.101.1-nightly.4" part because hustcer + // is changing the cargo.toml package.version to be that syntax + let up_through_plus = match v.tag_name.split('+').next() { + Some(v) => v, + None => &v.tag_name, + }; + return Ok(Some(up_through_plus.to_string())); + } + + Ok(None) + } +} + +struct NativeTlsHttpClient; + +impl HttpClient for NativeTlsHttpClient { + fn get( + url: &str, + timeout: std::time::Duration, + headers: update_informer::http_client::HeaderMap, + ) -> update_informer::Result { + let agent = ureq::AgentBuilder::new() + .tls_connector(std::sync::Arc::new(native_tls::TlsConnector::new()?)) + .build(); + + let mut req = agent.get(url).timeout(timeout); + + for (header, value) in headers { + req = req.set(header, value); + } + + let json = req.call()?.into_json()?; + + Ok(json) + } +} + +pub fn check_for_latest_nushell_version() -> Value { + let current_version = env!("CARGO_PKG_VERSION").to_string(); + + let mut rec = Record::new(); + + if current_version.contains("nightly") { + rec.push("channel", Value::test_string("nightly")); + + let nightly_pkg_name = "nushell/nightly"; + // The .interval() determines how long the cached check lives. Setting it to std::time::Duration::ZERO + // means that there is essentially no cache and it will check for a new version each time you run nushell. + // Since this is run on demand, there isn't really a need to cache the check. + let informer = + update_informer::new(NuShellNightly, nightly_pkg_name, current_version.clone()) + .http_client(NativeTlsHttpClient) + .interval(std::time::Duration::ZERO); + + if let Ok(Some(new_version)) = informer.check_version() { + rec.push("current", Value::test_bool(false)); + rec.push("latest", Value::test_string(format!("{}", new_version))); + Value::test_record(rec) + } else { + rec.push("current", Value::test_bool(true)); + rec.push("latest", Value::test_string(current_version.clone())); + Value::test_record(rec) + } + } else { + rec.push("channel", Value::test_string("release")); + + let normal_pkg_name = "nushell/nushell"; + // By default, this update request is cached for 24 hours so it won't check for a new version + // each time you run nushell. Since this is run on demand, there isn't really a need to cache the check which + // is why we set the interval to std::time::Duration::ZERO. + let informer = + update_informer::new(registry::GitHub, normal_pkg_name, current_version.clone()) + .interval(std::time::Duration::ZERO); + + if let Ok(Some(new_version)) = informer.check_version() { + rec.push("current", Value::test_bool(false)); + rec.push("latest", Value::test_string(format!("{}", new_version))); + Value::test_record(rec) + } else { + rec.push("current", Value::test_bool(true)); + rec.push("latest", Value::test_string(current_version.clone())); + Value::test_record(rec) + } + } +}