`solana-install update` now updates a named release to the latest patch version
This commit is contained in:
parent
66b4124a68
commit
79280b304b
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
use crate::{
|
||||
use {
|
||||
crate::{
|
||||
config::{Config, ExplicitRelease},
|
||||
stop_process::stop_process,
|
||||
update_manifest::{SignedUpdateManifest, UpdateManifest},
|
||||
};
|
||||
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::{
|
||||
},
|
||||
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,
|
||||
};
|
||||
use std::{
|
||||
},
|
||||
std::{
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, Read},
|
||||
path::{Path, PathBuf},
|
||||
sync::mpsc,
|
||||
time::{Duration, Instant, SystemTime},
|
||||
},
|
||||
tempfile::TempDir,
|
||||
url::Url,
|
||||
};
|
||||
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<Option<UpdateManifest>, 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::<String>::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);
|
||||
}
|
||||
|
||||
print_release_version(&config);
|
||||
} else {
|
||||
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());
|
||||
println_name_value(
|
||||
&format!("{}download URL:", BULLET),
|
||||
&update_manifest.download_url,
|
||||
);
|
||||
}
|
||||
|
||||
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<bool, String> {
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct GithubReleases(Vec<GithubRelease>);
|
||||
|
||||
fn semver_of(string: &str) -> Result<semver::Version, String> {
|
||||
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<semver::VersionReq>,
|
||||
) -> reqwest::Result<Option<String>> {
|
||||
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::<GithubReleases>()?
|
||||
.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::<Vec<_>>();
|
||||
|
||||
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<bool, String> {
|
||||
init_or_update(config_file, false, check_only)
|
||||
}
|
||||
|
||||
pub fn init_or_update(config_file: &str, is_init: bool, check_only: bool) -> Result<bool, String> {
|
||||
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 = 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);
|
||||
let download_url_and_sha256 = if release_dir.exists() {
|
||||
// Release already present in the cache
|
||||
None
|
||||
} else {
|
||||
Some(download_url)
|
||||
Some((github_release_download_url(&release_semver), None))
|
||||
};
|
||||
(download_url, release_dir)
|
||||
(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<bool, String> {
|
|||
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
|
||||
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!(
|
||||
"Latest {} build ({}) found in cache",
|
||||
release_channel,
|
||||
¤t_release_version.commit[0..7],
|
||||
"Install is up to date. {} is the latest commit for {}",
|
||||
&active_release_version.commit[0..7],
|
||||
release_channel
|
||||
);
|
||||
(None, release_dir)
|
||||
} else {
|
||||
println_name_value(
|
||||
&format!("{}Release commit:", BULLET),
|
||||
&format!(
|
||||
"{} => {}:",
|
||||
¤t_release_version.commit[0..7],
|
||||
&update_release_version.commit[0..7],
|
||||
),
|
||||
);
|
||||
|
||||
(download_url, 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() {
|
||||
return Ok(false);
|
||||
}
|
||||
let update_manifest = update_manifest.unwrap();
|
||||
}
|
||||
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 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);
|
||||
}
|
||||
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<bool, String> {
|
|||
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<bool, String> {
|
|||
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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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::{
|
||||
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
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_config_program::ConfigState;
|
||||
use solana_sdk::{
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue