From 79280b304bbeeb3c8ce5308188678ab45f30f914 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Thu, 11 Mar 2021 17:52:16 -0800 Subject: [PATCH] `solana-install update` now updates a named release to the latest patch version --- Cargo.lock | 2 +- install/Cargo.toml | 4 +- install/src/command.rs | 431 ++++++++++++++++++++++----------- install/src/config.rs | 14 +- install/src/lib.rs | 16 +- install/src/update_manifest.rs | 18 +- 6 files changed, 319 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 721046721..7f5442648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4647,7 +4647,7 @@ dependencies = [ "reqwest 0.10.8", "semver 0.9.0", "serde", - "serde_derive", + "serde_json", "serde_yaml", "solana-clap-utils", "solana-client", diff --git a/install/Cargo.toml b/install/Cargo.toml index 3fa53a5e5..920ca9661 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -22,8 +22,8 @@ indicatif = "0.15.0" lazy_static = "1.4.0" nix = "0.19.0" reqwest = { version = "0.10.8", default-features = false, features = ["blocking", "rustls-tls", "json"] } -serde = "1.0.122" -serde_derive = "1.0.103" +serde = { version = "1.0.122", features = ["derive"] } +serde_json = "1.0.62" serde_yaml = "0.8.13" solana-clap-utils = { path = "../clap-utils", version = "1.6.0" } solana-client = { path = "../client", version = "1.6.0" } diff --git a/install/src/command.rs b/install/src/command.rs index 475d35887..e5669f9f1 100644 --- a/install/src/command.rs +++ b/install/src/command.rs @@ -1,30 +1,32 @@ -use crate::{ - config::{Config, ExplicitRelease}, - stop_process::stop_process, - update_manifest::{SignedUpdateManifest, UpdateManifest}, +use { + crate::{ + config::{Config, ExplicitRelease}, + stop_process::stop_process, + update_manifest::{SignedUpdateManifest, UpdateManifest}, + }, + chrono::{Local, TimeZone}, + console::{style, Emoji}, + indicatif::{ProgressBar, ProgressStyle}, + serde::{Deserialize, Serialize}, + solana_client::rpc_client::RpcClient, + solana_config_program::{config_instruction, get_config_data, ConfigState}, + solana_sdk::{ + hash::{Hash, Hasher}, + message::Message, + pubkey::Pubkey, + signature::{read_keypair_file, Keypair, Signable, Signer}, + transaction::Transaction, + }, + std::{ + fs::{self, File}, + io::{self, BufReader, Read}, + path::{Path, PathBuf}, + sync::mpsc, + time::{Duration, Instant, SystemTime}, + }, + tempfile::TempDir, + url::Url, }; -use chrono::{Local, TimeZone}; -use console::{style, Emoji}; -use indicatif::{ProgressBar, ProgressStyle}; -use serde_derive::Deserialize; -use solana_client::rpc_client::RpcClient; -use solana_config_program::{config_instruction, get_config_data, ConfigState}; -use solana_sdk::{ - hash::{Hash, Hasher}, - message::Message, - pubkey::Pubkey, - signature::{read_keypair_file, Keypair, Signable, Signer}, - transaction::Transaction, -}; -use std::{ - fs::{self, File}, - io::{self, BufReader, Read}, - path::{Path, PathBuf}, - sync::mpsc, - time::{Duration, Instant, SystemTime}, -}; -use tempfile::TempDir; -use url::Url; #[derive(Deserialize, Debug)] pub struct ReleaseVersion { @@ -37,6 +39,7 @@ static TRUCK: Emoji = Emoji("🚚 ", ""); static LOOKING_GLASS: Emoji = Emoji("🔍 ", ""); static BULLET: Emoji = Emoji("â€ĸ ", "* "); static SPARKLE: Emoji = Emoji("✨ ", ""); +static WRAPPED_PRESENT: Emoji = Emoji("🎁 ", ""); static PACKAGE: Emoji = Emoji("đŸ“Ļ ", ""); static INFORMATION: Emoji = Emoji("ℹī¸ ", ""); static RECYCLING: Emoji = Emoji("â™ģī¸ ", ""); @@ -544,7 +547,7 @@ pub fn init( config }; - update(config_file)?; + init_or_update(config_file, true, false)?; let path_modified = if !no_modify_path { add_to_path(&config.active_release_bin_dir().to_str().unwrap()) @@ -582,11 +585,16 @@ fn release_channel_version_url(release_channel: &str) -> String { ) } -pub fn info( - config_file: &str, - local_info_only: bool, - eval: bool, -) -> Result, String> { +fn print_update_manifest(update_manifest: &UpdateManifest) { + let when = Local.timestamp(update_manifest.timestamp_secs as i64, 0); + println_name_value(&format!("{}release date:", BULLET), &when.to_string()); + println_name_value( + &format!("{}download URL:", BULLET), + &update_manifest.download_url, + ); +} + +pub fn info(config_file: &str, local_info_only: bool, eval: bool) -> Result<(), String> { let config = Config::load(config_file)?; if eval { @@ -604,7 +612,7 @@ pub fn info( println!("SOLANA_INSTALL_ACTIVE_CHANNEL={}", channel,); Option::::None }); - return Ok(None); + return Ok(()); } println_name_value("Configuration:", &config_file); @@ -613,6 +621,17 @@ pub fn info( &config.active_release_dir().to_str().unwrap_or("?"), ); + fn print_release_version(config: &Config) { + if let Ok(release_version) = + load_release_version(&config.active_release_dir().join("version.yml")) + { + println_name_value( + &format!("{}Release commit:", BULLET), + &release_version.commit[0..7], + ); + } + } + if let Some(explicit_release) = &config.explicit_release { match explicit_release { ExplicitRelease::Semver(release_semver) => { @@ -630,51 +649,30 @@ pub fn info( ); } } - return Ok(None); - } - - println_name_value("JSON RPC URL:", &config.json_rpc_url); - println_name_value( - "Update manifest pubkey:", - &config.update_manifest_pubkey.to_string(), - ); - - fn print_update_manifest(update_manifest: &UpdateManifest) { - let when = Local.timestamp(update_manifest.timestamp_secs as i64, 0); - println_name_value(&format!("{}release date:", BULLET), &when.to_string()); + print_release_version(&config); + } else { + println_name_value("JSON RPC URL:", &config.json_rpc_url); println_name_value( - &format!("{}download URL:", BULLET), - &update_manifest.download_url, + "Update manifest pubkey:", + &config.update_manifest_pubkey.to_string(), ); - } - match config.current_update_manifest { - Some(ref update_manifest) => { - println_name_value("Installed version:", ""); - print_update_manifest(&update_manifest); - } - None => { - println_name_value("Installed version:", "None"); + match config.current_update_manifest { + Some(ref update_manifest) => { + println_name_value("Installed version:", ""); + print_release_version(&config); + print_update_manifest(&update_manifest); + } + None => { + println_name_value("Installed version:", "None"); + } } } if local_info_only { - Ok(None) + Ok(()) } else { - let progress_bar = new_spinner_progress_bar(); - progress_bar.set_message(&format!("{}Checking for updates...", LOOKING_GLASS)); - let rpc_client = RpcClient::new(config.json_rpc_url.clone()); - let manifest = get_update_manifest(&rpc_client, &config.update_manifest_pubkey)?; - progress_bar.finish_and_clear(); - - if Some(&manifest) == config.current_update_manifest.as_ref() { - println!("\n{}", style("Installation is up to date").italic()); - Ok(None) - } else { - println!("\n{}", style("An update is available:").bold()); - print_update_manifest(&manifest); - Ok(Some(manifest)) - } + update(config_file, true).map(|_| ()) } } @@ -845,24 +843,137 @@ pub fn gc(config_file: &str) -> Result<(), String> { Ok(()) } -pub fn update(config_file: &str) -> Result { - let mut config = Config::load(config_file)?; - let update_manifest = info(config_file, false, false)?; +#[derive(Debug, Deserialize, Serialize)] +pub struct GithubRelease { + pub tag_name: String, + pub prerelease: bool, +} - let release_dir = if let Some(explicit_release) = &config.explicit_release { - let (download_url, release_dir) = match explicit_release { - ExplicitRelease::Semver(release_semver) => { - let download_url = github_release_download_url(release_semver); - let release_dir = config.release_dir(&release_semver); - let download_url = if release_dir.exists() { - // If this release_semver has already been successfully downloaded, no update - // needed - println!("{} found in cache", release_semver); - None - } else { - Some(download_url) - }; - (download_url, release_dir) +#[derive(Debug, Deserialize, Serialize)] +pub struct GithubReleases(Vec); + +fn semver_of(string: &str) -> Result { + if string.starts_with('v') { + semver::Version::parse(string.split_at(1).1) + } else { + semver::Version::parse(string) + } + .map_err(|err| err.to_string()) +} + +fn check_for_newer_github_release( + version_filter: Option, +) -> reqwest::Result> { + let url = + reqwest::Url::parse("https://api.github.com/repos/solana-labs/solana/releases").unwrap(); + let client = reqwest::blocking::Client::builder() + .user_agent("solana-install") + .build()?; + let request = client.get(url).build()?; + let response = client.execute(request)?; + + let mut releases = response + .json::()? + .0 + .into_iter() + .filter_map( + |GithubRelease { + tag_name, + prerelease, + }| { + if let Ok(version) = semver_of(&tag_name) { + if !prerelease + && version_filter + .as_ref() + .map_or(true, |version_filter| version_filter.matches(&version)) + { + return Some(version); + } + } + None + }, + ) + .collect::>(); + + releases.sort(); + Ok(releases.pop().map(|r| r.to_string())) +} + +pub enum SemverUpdateType { + Fixed, + Patch, + _Minor, +} + +pub fn update(config_file: &str, check_only: bool) -> Result { + init_or_update(config_file, false, check_only) +} + +pub fn init_or_update(config_file: &str, is_init: bool, check_only: bool) -> Result { + let mut config = Config::load(config_file)?; + + let semver_update_type = if is_init { + SemverUpdateType::Fixed + } else { + SemverUpdateType::Patch + }; + + let (updated_version, download_url_and_sha256, release_dir) = if let Some(explicit_release) = + &config.explicit_release + { + match explicit_release { + ExplicitRelease::Semver(current_release_semver) => { + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message(&format!("{}Checking for updates...", LOOKING_GLASS)); + + let github_release = check_for_newer_github_release( + semver::VersionReq::parse(&format!( + "{}{}", + match semver_update_type { + SemverUpdateType::Fixed => "=", + SemverUpdateType::Patch => "~", + SemverUpdateType::_Minor => "^", + }, + current_release_semver + )) + .ok(), + ) + .map_err(|err| err.to_string())?; + progress_bar.finish_and_clear(); + + match github_release { + None => { + return Err(format!("Unknown release: {}", current_release_semver)); + } + Some(release_semver) => { + if release_semver == *current_release_semver { + if let Ok(active_release_version) = load_release_version( + &config.active_release_dir().join("version.yml"), + ) { + if format!("v{}", current_release_semver) + == active_release_version.channel + { + println!( + "Install is up to date. {} is the latest compatible release", + release_semver + ); + return Ok(false); + } + } + } + config.explicit_release = + Some(ExplicitRelease::Semver(release_semver.clone())); + + let release_dir = config.release_dir(&release_semver); + let download_url_and_sha256 = if release_dir.exists() { + // Release already present in the cache + None + } else { + Some((github_release_download_url(&release_semver), None)) + }; + (release_semver, download_url_and_sha256, release_dir) + } + } } ExplicitRelease::Channel(release_channel) => { let version_url = release_channel_version_url(release_channel); @@ -878,59 +989,73 @@ pub fn update(config_file: &str) -> Result { let current_release_version_yml = release_dir.join("solana-release").join("version.yml"); - let download_url = Some(release_channel_download_url(release_channel)); + let download_url = release_channel_download_url(release_channel); if !current_release_version_yml.exists() { - println_name_value( - &format!("{}Release commit:", BULLET), - &update_release_version.commit[0..7], - ); - (download_url, release_dir) + ( + format!( + "{} commit {}", + release_channel, + &update_release_version.commit[0..7] + ), + Some((download_url, None)), + release_dir, + ) } else { let current_release_version = load_release_version(¤t_release_version_yml)?; if update_release_version.commit == current_release_version.commit { - // Same commit, no update required - println!( - "Latest {} build ({}) found in cache", - release_channel, - ¤t_release_version.commit[0..7], - ); - (None, release_dir) - } else { - println_name_value( - &format!("{}Release commit:", BULLET), - &format!( - "{} => {}:", - ¤t_release_version.commit[0..7], - &update_release_version.commit[0..7], - ), - ); + if let Ok(active_release_version) = + load_release_version(&config.active_release_dir().join("version.yml")) + { + if current_release_version.commit == active_release_version.commit { + // Same version, no update required + println!( + "Install is up to date. {} is the latest commit for {}", + &active_release_version.commit[0..7], + release_channel + ); + return Ok(false); + } + } - (download_url, release_dir) + // Release already present in the cache + ( + format!( + "{} commit {}", + release_channel, + &update_release_version.commit[0..7] + ), + None, + release_dir, + ) + } else { + ( + format!( + "{} (from {})", + &update_release_version.commit[0..7], + ¤t_release_version.commit[0..7], + ), + Some((download_url, None)), + release_dir, + ) } } } - }; - - if let Some(download_url) = download_url { - let (_temp_dir, temp_archive, _temp_archive_sha256) = - download_to_temp(&download_url, None) - .map_err(|err| format!("Unable to download {}: {}", download_url, err))?; - extract_release_archive(&temp_archive, &release_dir).map_err(|err| { - format!( - "Unable to extract {:?} to {:?}: {}", - temp_archive, release_dir, err - ) - })?; } - - release_dir } else { - if update_manifest.is_none() { + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message(&format!("{}Checking for updates...", LOOKING_GLASS)); + let rpc_client = RpcClient::new(config.json_rpc_url.clone()); + let update_manifest = get_update_manifest(&rpc_client, &config.update_manifest_pubkey)?; + progress_bar.finish_and_clear(); + + if Some(&update_manifest) == config.current_update_manifest.as_ref() { + println!("Install is up to date"); return Ok(false); } - let update_manifest = update_manifest.unwrap(); + println!("\n{}", style("An update is available:").bold()); + print_update_manifest(&update_manifest); if timestamp_secs() < u64::from_str_radix(crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH, 10).unwrap() @@ -943,27 +1068,39 @@ pub fn update(config_file: &str) -> Result { return Err("Unable to update to an older version".to_string()); } } + config.current_update_manifest = Some(update_manifest.clone()); + let release_dir = config.release_dir(&update_manifest.download_sha256.to_string()); - let (_temp_dir, temp_archive, _temp_archive_sha256) = download_to_temp( - &update_manifest.download_url, - Some(&update_manifest.download_sha256), + + let download_url = update_manifest.download_url; + let archive_sha256 = Some(update_manifest.download_sha256); + ( + "latest manifest".to_string(), + Some((download_url, archive_sha256)), + release_dir, ) - .map_err(|err| { - format!( - "Unable to download {}: {}", - update_manifest.download_url, err - ) - })?; + }; + + if check_only { + println!( + " {}{}", + WRAPPED_PRESENT, + style(format!("Update available: {}", updated_version)).bold() + ); + return Ok(true); + } + + if let Some((download_url, archive_sha256)) = download_url_and_sha256 { + let (_temp_dir, temp_archive, _temp_archive_sha256) = + download_to_temp(&download_url, archive_sha256.as_ref()) + .map_err(|err| format!("Unable to download {}: {}", download_url, err))?; extract_release_archive(&temp_archive, &release_dir).map_err(|err| { format!( "Unable to extract {:?} to {:?}: {}", temp_archive, release_dir, err ) })?; - - config.current_update_manifest = Some(update_manifest); - release_dir - }; + } let release_target = load_release_target(&release_dir).map_err(|err| { format!( @@ -1000,7 +1137,19 @@ pub fn update(config_file: &str) -> Result { config.save(config_file)?; gc(config_file)?; - println!(" {}{}", SPARKLE, style("Update successful").bold()); + if is_init { + println!( + " {}{}", + SPARKLE, + style(format!("{} initialized", updated_version)).bold() + ); + } else { + println!( + " {}{}", + SPARKLE, + style(format!("Update successful to {}", updated_version)).bold() + ); + } Ok(true) } @@ -1063,7 +1212,7 @@ pub fn run( }; if config.explicit_release.is_none() && now.elapsed().as_secs() > config.update_poll_secs { - match update(config_file) { + match update(config_file, false) { Ok(true) => { // Update successful, kill current process so it will be restart if let Some(ref mut child) = child_option { diff --git a/install/src/config.rs b/install/src/config.rs index 4871d565a..db676eb74 100644 --- a/install/src/config.rs +++ b/install/src/config.rs @@ -1,9 +1,11 @@ -use crate::update_manifest::UpdateManifest; -use serde_derive::{Deserialize, Serialize}; -use solana_sdk::pubkey::Pubkey; -use std::fs::{create_dir_all, File}; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; +use { + crate::update_manifest::UpdateManifest, + serde::{Deserialize, Serialize}, + solana_sdk::pubkey::Pubkey, + std::fs::{create_dir_all, File}, + std::io::{self, Write}, + std::path::{Path, PathBuf}, +}; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub enum ExplicitRelease { diff --git a/install/src/lib.rs b/install/src/lib.rs index a8bcd66c4..188cdfcd0 100644 --- a/install/src/lib.rs +++ b/install/src/lib.rs @@ -2,10 +2,12 @@ #[macro_use] extern crate lazy_static; -use clap::{crate_description, crate_name, App, AppSettings, Arg, ArgMatches, SubCommand}; -use solana_clap_utils::{ - input_parsers::pubkey_of, - input_validators::{is_pubkey, is_url}, +use { + clap::{crate_description, crate_name, App, AppSettings, Arg, ArgMatches, SubCommand}, + solana_clap_utils::{ + input_parsers::pubkey_of, + input_validators::{is_pubkey, is_url}, + }, }; mod build_env; @@ -158,9 +160,7 @@ pub fn main() -> Result<(), String> { Arg::with_name("local_info_only") .short("l") .long("local") - .help( - "only display local information, don't check the cluster for new updates", - ), + .help("only display local information, don't check for updates"), ) .arg( Arg::with_name("eval") @@ -262,7 +262,7 @@ pub fn main() -> Result<(), String> { ) } ("gc", Some(_matches)) => command::gc(config_file), - ("update", Some(_matches)) => command::update(config_file).map(|_| ()), + ("update", Some(_matches)) => command::update(config_file, false).map(|_| ()), ("run", Some(matches)) => { let program_name = matches.value_of("program_name").unwrap(); let program_arguments = matches diff --git a/install/src/update_manifest.rs b/install/src/update_manifest.rs index 658b6c84e..e9de44bfc 100644 --- a/install/src/update_manifest.rs +++ b/install/src/update_manifest.rs @@ -1,14 +1,16 @@ -use serde_derive::{Deserialize, Serialize}; -use solana_config_program::ConfigState; -use solana_sdk::{ - hash::Hash, - pubkey::Pubkey, - signature::{Signable, Signature}, +use { + serde::{Deserialize, Serialize}, + solana_config_program::ConfigState, + solana_sdk::{ + hash::Hash, + pubkey::Pubkey, + signature::{Signable, Signature}, + }, + std::{borrow::Cow, error, io}, }; -use std::{borrow::Cow, error, io}; /// Information required to download and apply a given update -#[derive(Serialize, Deserialize, Default, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)] pub struct UpdateManifest { pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH pub download_url: String, // Download URL to the release tar.bz2