From 97a0ee5ae7cc3a6fe48cf7a26d6717f6eff2ea5a Mon Sep 17 00:00:00 2001 From: Thomas Knickman Date: Wed, 7 Dec 2022 13:20:47 -0500 Subject: [PATCH] feat(turbo): add update-notifier (#2867) --- Cargo.lock | 232 ++++++++++++++++++++++++++- Cargo.toml | 1 + crates/turbo-updater/Cargo.toml | 16 ++ crates/turbo-updater/src/lib.rs | 76 +++++++++ crates/turbo-updater/src/ui.rs | 61 +++++++ crates/turbo-updater/src/ui/utils.rs | 150 +++++++++++++++++ deny.toml | 10 ++ shim/Cargo.toml | 2 + shim/src/main.rs | 19 +++ 9 files changed, 564 insertions(+), 3 deletions(-) create mode 100644 crates/turbo-updater/Cargo.toml create mode 100644 crates/turbo-updater/src/lib.rs create mode 100644 crates/turbo-updater/src/ui.rs create mode 100644 crates/turbo-updater/src/ui/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 4120a88539b9e..681e7ba6c4e4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -838,6 +838,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + [[package]] name = "clang-sys" version = "1.4.0" @@ -951,6 +957,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi 0.3.9", +] + [[package]] name = "combine" version = "4.6.4" @@ -1511,6 +1528,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1712,6 +1738,27 @@ dependencies = [ "serde", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -2478,6 +2525,16 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + [[package]] name = "iovec" version = "0.1.4" @@ -2754,9 +2811,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.131" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c3b4822ccebfa39c02fc03d1534441b22ead323fa0f48bb7ddd8e6ba076a40" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libloading" @@ -2768,6 +2825,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + [[package]] name = "libmimalloc-sys" version = "0.1.28" @@ -2806,6 +2869,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" + [[package]] name = "lock_api" version = "0.4.9" @@ -2957,7 +3026,7 @@ dependencies = [ "supports-color", "supports-hyperlinks", "supports-unicode", - "terminal_size", + "terminal_size 0.1.17", "textwrap 0.15.0", "thiserror", "unicode-width", @@ -4349,6 +4418,21 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "rkyv" version = "0.7.37" @@ -4437,6 +4521,32 @@ dependencies = [ "semver 1.0.13", ] +[[package]] +name = "rustix" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.42.0", +] + +[[package]] +name = "rustls" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "rustversion" version = "1.0.9" @@ -4480,6 +4590,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "seahash" version = "4.1.0" @@ -4818,6 +4938,12 @@ dependencies = [ "url", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "st-map" version = "0.1.6" @@ -4949,6 +5075,15 @@ dependencies = [ "syn 1.0.99", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.10.0" @@ -6164,6 +6299,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "terminal_size" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb20089a8ba2b69debd491f8d2d023761cbf196e999218c591fa1e7e15a21907" +dependencies = [ + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "termtree" version = "0.4.0" @@ -6354,6 +6499,15 @@ dependencies = [ "syn 1.0.99", ] +[[package]] +name = "tiny-gradient" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8063c572fcc935676f1e01615f201f355a053e88525ec41c1b0c4884ce104847" +dependencies = [ + "libm", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -6697,6 +6851,8 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "tiny-gradient", + "turbo-updater", ] [[package]] @@ -6869,6 +7025,19 @@ dependencies = [ "turbo-tasks", ] +[[package]] +name = "turbo-updater" +version = "0.1.0" +dependencies = [ + "colored", + "serde", + "strip-ansi-escapes", + "terminal_size 0.2.3", + "thiserror", + "update-informer", + "ureq", +] + [[package]] name = "turbopack" version = "0.1.0" @@ -7230,6 +7399,44 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "update-informer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f154aee470c0882ea0f3b1cc2a46c5f4d24f282655f7b0cec065614fe24c447f" +dependencies = [ + "directories", + "semver 1.0.13", + "serde", + "serde_json", + "ureq", +] + +[[package]] +name = "ureq" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f" +dependencies = [ + "base64 0.13.0", + "chunked_transfer", + "flate2", + "log", + "once_cell", + "rustls", + "serde", + "serde_json", + "url", + "webpki", + "webpki-roots", +] + [[package]] name = "url" version = "2.3.0" @@ -7783,6 +7990,25 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +dependencies = [ + "webpki", +] + [[package]] name = "weezl" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 85c82d7f05090..f07570c1a0f4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "crates/turbopack-swc-utils", "crates/turbopack", "crates/turbopack-tests", + "crates/turbo-updater", "shim", "xtask", ] diff --git a/crates/turbo-updater/Cargo.toml b/crates/turbo-updater/Cargo.toml new file mode 100644 index 0000000000000..234382f1f29dc --- /dev/null +++ b/crates/turbo-updater/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "turbo-updater" +version = "0.1.0" +edition = "2021" +description = "Minimal wrapper around update-informer to provide npm registry support and consistent UI" +license = "MPL-2.0" +publish = false + +[dependencies] +colored = "2.0" +serde = { version = "1.0.126", features = ["derive"] } +strip-ansi-escapes = "0.1.1" +terminal_size = "0.2" +thiserror = "1.0" +update-informer = "0.5.0" +ureq = { version = "2.3.0" } diff --git a/crates/turbo-updater/src/lib.rs b/crates/turbo-updater/src/lib.rs new file mode 100644 index 0000000000000..1a98d4e1d2350 --- /dev/null +++ b/crates/turbo-updater/src/lib.rs @@ -0,0 +1,76 @@ +use std::time::Duration; + +use colored::*; +use serde::Deserialize; +use thiserror::Error as ThisError; +use update_informer::{Check, Package, Registry, Result as UpdateResult}; + +mod ui; + +const DEFAULT_TIMEOUT: Duration = Duration::from_millis(800); +const DEFAULT_INTERVAL: Duration = Duration::from_secs(60 * 60 * 24); + +#[derive(ThisError, Debug)] +pub enum UpdateNotifierError { + #[error("Failed to write to terminal")] + RenderError(#[from] ui::utils::GetDisplayLengthError), +} + +#[derive(Deserialize)] +struct NpmVersionData { + version: String, +} + +struct NPMRegistry; + +impl Registry for NPMRegistry { + const NAME: &'static str = "npm_registry"; + fn get_latest_version(pkg: &Package, _timeout: Duration) -> UpdateResult> { + let url = format!( + "https://turbo.build/api/binaries/version?name={name}", + name = pkg + ); + let resp = ureq::get(&url).timeout(_timeout).call()?; + let result = resp.into_json::().unwrap(); + Ok(Some(result.version)) + } +} + +pub fn check_for_updates( + package_name: &str, + github_repo: &str, + footer: Option<&str>, + current_version: &str, + timeout: Option, + interval: Option, +) -> Result<(), UpdateNotifierError> { + let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT); + let interval = interval.unwrap_or(DEFAULT_INTERVAL); + let informer = update_informer::new(NPMRegistry, package_name, current_version) + .timeout(timeout) + .interval(interval); + if let Ok(Some(version)) = informer.check_version() { + let latest_version = version.to_string(); + let msg = format!( + " + Update available {version_prefix}{current_version} ≫ {latest_version} + Changelog: {github_repo}/releases/tag/{latest_version} + Run \"{update_cmd}\" to update + ", + version_prefix = "v".dimmed(), + current_version = current_version.dimmed(), + latest_version = latest_version.green().bold(), + github_repo = github_repo, + // TODO: make this package manager aware + update_cmd = "npm i -g turbo".cyan().bold(), + ); + + if let Some(footer) = footer { + return ui::message(&format!("{}\n{}", msg, footer)); + } + + return ui::message(&msg); + } + + Ok(()) +} diff --git a/crates/turbo-updater/src/ui.rs b/crates/turbo-updater/src/ui.rs new file mode 100644 index 0000000000000..57fa2b29d426a --- /dev/null +++ b/crates/turbo-updater/src/ui.rs @@ -0,0 +1,61 @@ +use terminal_size::{terminal_size, Width}; + +use crate::UpdateNotifierError; +pub mod utils; + +const DEFAULT_PADDING: usize = 8; + +pub fn message(text: &str) -> Result<(), UpdateNotifierError> { + let size = terminal_size(); + let lines: Vec<&str> = text.split('\n').map(|line| line.trim()).collect(); + + // get the display width of each line so we can center it within the box later + let lines_display_width = lines + .iter() + .map(|line| utils::get_display_length(line)) + .collect::, _>>()?; + + // find the longest line to determine layout + let longest_line = lines_display_width + .iter() + .max() + .copied() + .unwrap_or_default(); + let full_message_width = longest_line + DEFAULT_PADDING; + + // create a curried render function to reduce verbosity when calling + let render_at_layout = |layout: utils::Layout, width: usize| { + utils::render_message( + layout, + width, + lines, + lines_display_width, + full_message_width, + ) + }; + + // render differently depending on viewport + if let Some((Width(term_width), _)) = size { + // if possible, pad this value slightly + let term_width = if term_width > 2 { + usize::from(term_width) - 2 + } else { + term_width.into() + }; + + let can_fit_box = term_width >= full_message_width; + let can_center_text = term_width >= longest_line; + + if can_fit_box { + render_at_layout(utils::Layout::Large, term_width); + } else if can_center_text { + render_at_layout(utils::Layout::Medium, term_width); + } else { + render_at_layout(utils::Layout::Small, term_width); + } + } else { + render_at_layout(utils::Layout::Unknown, 0); + } + + Ok(()) +} diff --git a/crates/turbo-updater/src/ui/utils.rs b/crates/turbo-updater/src/ui/utils.rs new file mode 100644 index 0000000000000..5028a5599eb4e --- /dev/null +++ b/crates/turbo-updater/src/ui/utils.rs @@ -0,0 +1,150 @@ +use std::{io::Error as IOError, string::FromUtf8Error}; + +use colored::*; +use strip_ansi_escapes::strip as strip_ansi_escapes; +use thiserror::Error as ThisError; + +pub enum BorderAlignment { + Divider, + Top, + Bottom, +} + +pub enum Layout { + Unknown, + Small, + Medium, + Large, +} + +const TOP_LEFT: &str = "╭"; +const TOP_RIGHT: &str = "╮"; +const BOTTOM_LEFT: &str = "╰"; +const BOTTOM_RIGHT: &str = "╯"; +const HORIZONTAL: &str = "─"; +const VERTICAL: &str = "│"; +const SPACE: &str = " "; + +#[derive(ThisError, Debug)] +pub enum GetDisplayLengthError { + #[error("Could not strip ANSI escape codes from string")] + StripError(#[from] IOError), + #[error("Could not convert to string")] + ConvertError(#[from] FromUtf8Error), +} + +pub fn get_display_length(line: &str) -> Result { + // strip any ansi escape codes (for color) + let stripped = strip_ansi_escapes(line)?; + let stripped = String::from_utf8(stripped)?; + // count the chars instead of the bytes (for unicode) + return Ok(stripped.chars().count()); +} + +pub fn x_border(width: usize, position: BorderAlignment) { + match position { + BorderAlignment::Top => { + println!( + "{}{}{}", + TOP_LEFT.yellow(), + HORIZONTAL.repeat(width).yellow(), + TOP_RIGHT.yellow() + ); + } + BorderAlignment::Bottom => { + println!( + "{}{}{}", + BOTTOM_LEFT.yellow(), + HORIZONTAL.repeat(width).yellow(), + BOTTOM_RIGHT.yellow() + ); + } + BorderAlignment::Divider => { + println!("{}", HORIZONTAL.repeat(width).yellow(),); + } + } +} + +pub fn render_message( + layout: Layout, + width: usize, + lines: Vec<&str>, + lines_display_width: Vec, + full_message_width: usize, +) { + match layout { + // Left aligned text with no border. + // Used when term width is unknown. + Layout::Unknown => { + for line in lines.iter() { + println!("{}", line); + } + } + + // Left aligned text with top and bottom border. + // Used when text cannot be centered without wrapping + Layout::Small => { + x_border(width, BorderAlignment::Divider); + for (line, line_display_width) in lines.iter().zip(lines_display_width.iter()) { + if *line_display_width == 0 { + println!("{}", SPACE.repeat(width)); + } else { + println!("{}", line); + } + } + x_border(width, BorderAlignment::Divider); + } + + // Centered text with top and bottom border. + // Used when text can be centered without wrapping, but + // there isn't enough room to include the box with padding. + Layout::Medium => { + x_border(width, BorderAlignment::Divider); + for (line, line_display_width) in lines.iter().zip(lines_display_width.iter()) { + if *line_display_width == 0 { + println!("{}", SPACE.repeat(width)); + } else { + let line_padding = (width - line_display_width) / 2; + // for lines of odd length, tack the reminder to the end + let line_padding_remainder = width - (line_padding * 2) - line_display_width; + println!( + "{}{}{}", + SPACE.repeat(line_padding), + line, + SPACE.repeat(line_padding + line_padding_remainder), + ); + } + } + x_border(width, BorderAlignment::Divider); + } + + // Centered text with border on all sides + Layout::Large => { + x_border(full_message_width, BorderAlignment::Top); + for (line, line_display_width) in lines.iter().zip(lines_display_width.iter()) { + if *line_display_width == 0 { + println!( + "{}{}{}", + VERTICAL.yellow(), + SPACE.repeat(full_message_width), + VERTICAL.yellow() + ); + } else { + let line_padding = (full_message_width - line_display_width) / 2; + // for lines of odd length, tack the reminder to the end + let line_padding_remainder = + full_message_width - (line_padding * 2) - line_display_width; + println!( + "{}{}{}{}{}", + VERTICAL.yellow(), + SPACE.repeat(line_padding), + line, + SPACE.repeat(line_padding + line_padding_remainder), + VERTICAL.yellow() + ); + } + } + x_border(full_message_width, BorderAlignment::Bottom); + } + } +} diff --git a/deny.toml b/deny.toml index a17703a5a6662..cd33815d4a5d9 100644 --- a/deny.toml +++ b/deny.toml @@ -23,4 +23,14 @@ allow = [ "Unicode-DFS-2016", # portpicker "Unlicense", + "OpenSSL", ] +[[licenses.clarify]] +name = "ring" +expression = "ISC AND MIT AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + +[[licenses.clarify]] +name = "webpki" +expression = "ISC AND MIT AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] diff --git a/shim/Cargo.toml b/shim/Cargo.toml index d5110caf175cb..4b55f0281b2bd 100644 --- a/shim/Cargo.toml +++ b/shim/Cargo.toml @@ -22,3 +22,5 @@ predicates = "2.1.1" serde = { version = "1.0.145", features = ["derive"] } serde_json = "1.0.86" serde_yaml = "0.8.26" +turbo-updater = { path = "../crates/turbo-updater" } +tiny-gradient = "0.1" diff --git a/shim/src/main.rs b/shim/src/main.rs index 94c2ed4856a4a..2c6c270cc64a3 100644 --- a/shim/src/main.rs +++ b/shim/src/main.rs @@ -16,6 +16,8 @@ use std::{ use anyhow::{anyhow, Result}; use clap::{CommandFactory, Parser, Subcommand}; use serde::Serialize; +use tiny_gradient::{GradientStr, RGB}; +use turbo_updater::check_for_updates; use crate::{ ffi::{nativeRunWithArgs, nativeRunWithTurboState, GoString}, @@ -446,6 +448,23 @@ fn get_version() -> &'static str { } fn main() -> Result<()> { + // custom footer for update message + let footer = format!( + "Follow {username} for updates: {url}", + username = "@turborepo".gradient([RGB::new(0, 153, 247), RGB::new(241, 23, 18)]), + url = "https://twitter.com/turborepo" + ); + + // check for updates + let _ = check_for_updates( + "turbo", + "https://github.com/vercel/turbo", + Some(&footer), + get_version(), + None, + None, + ); + let clap_args = Args::parse(); // --help doesn't work with ignore_errors in clap. if clap_args.help {