From ec0738606753b34525988fd9f58a23cb300b7bc0 Mon Sep 17 00:00:00 2001 From: bartOssh Date: Mon, 23 Mar 2020 16:37:24 +0100 Subject: [PATCH] feat: first pass at "deno upgrade" (#4328) --- Cargo.lock | 9 +- cli/Cargo.toml | 1 + cli/flags.rs | 56 ++++++++- cli/lib.rs | 7 ++ cli/tests/integration_tests.rs | 24 ++++ cli/upgrade.rs | 224 +++++++++++++++++++++++++++++++++ 6 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 cli/upgrade.rs diff --git a/Cargo.lock b/Cargo.lock index 0105ea6a39..b658a2b6ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,6 +445,7 @@ dependencies = [ "reqwest", "ring", "rustyline", + "semver-parser 0.9.0", "serde", "serde_derive", "serde_json", @@ -1970,7 +1971,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser", + "semver-parser 0.7.0", ] [[package]] @@ -1979,6 +1980,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "semver-parser" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46e1121e8180c12ff69a742aabc4f310542b6ccb69f1691689ac17fdf8618aa" + [[package]] name = "serde" version = "1.0.104" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6dfa814771..c6b38de8be 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -61,6 +61,7 @@ utime = "0.2.1" webpki = "0.21.2" webpki-roots = "0.19.0" walkdir = "2.3.1" +semver-parser = "0.9.0" [target.'cfg(windows)'.dependencies] winapi = "0.3.8" diff --git a/cli/flags.rs b/cli/flags.rs index 9e1fbf5dfa..475172f0a7 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -63,6 +63,10 @@ pub enum DenoSubcommand { include: Option>, }, Types, + Upgrade { + dry_run: bool, + force: bool, + }, } impl Default for DenoSubcommand { @@ -250,6 +254,8 @@ pub fn flags_from_vec_safe(args: Vec) -> clap::Result { completions_parse(&mut flags, m); } else if let Some(m) = matches.subcommand_matches("test") { test_parse(&mut flags, m); + } else if let Some(m) = matches.subcommand_matches("upgrade") { + upgrade_parse(&mut flags, m); } else { unimplemented!(); } @@ -302,6 +308,7 @@ If the flag is set, restrict these messages to errors.", .subcommand(run_subcommand()) .subcommand(test_subcommand()) .subcommand(types_subcommand()) + .subcommand(upgrade_subcommand()) .long_about(DENO_HELP) .after_help(ENV_VARIABLES_HELP) } @@ -534,6 +541,12 @@ fn test_parse(flags: &mut Flags, matches: &clap::ArgMatches) { }; } +fn upgrade_parse(flags: &mut Flags, matches: &clap::ArgMatches) { + let dry_run = matches.is_present("dry-run"); + let force = matches.is_present("force"); + flags.subcommand = DenoSubcommand::Upgrade { dry_run, force }; +} + fn types_subcommand<'a, 'b>() -> App<'a, 'b> { SubCommand::with_name("types") .about("Print runtime TypeScript declarations") @@ -731,6 +744,29 @@ Future runs of this module will trigger no downloads or compilation unless ) } +fn upgrade_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("upgrade") + .about("Upgrade deno executable to newest version") + .long_about( + "Upgrade deno executable to newest available version. + +The latest version is downloaded from +https://github.com/denoland/deno/releases +and is used to replace the current executable.", + ) + .arg( + Arg::with_name("dry-run") + .long("dry-run") + .help("Perform all checks without replacing old exe"), + ) + .arg( + Arg::with_name("force") + .long("force") + .short("f") + .help("Replace current exe even if not out-of-date"), + ) +} + fn permission_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { app .arg( @@ -1142,7 +1178,8 @@ fn arg_hacks(mut args: Vec) -> Vec { "types", "install", "help", - "version" + "version", + "upgrade" ]; let modifier_flags = sset!["-h", "--help", "-V", "--version"]; // deno [subcommand|behavior modifier flags] -> do nothing @@ -1188,6 +1225,23 @@ mod tests { assert_eq!(args4, ["deno", "run", "-A", "script.js", "-L=info"]); } + #[test] + fn upgrade() { + let r = + flags_from_vec_safe(svec!["deno", "upgrade", "--dry-run", "--force"]); + let flags = r.unwrap(); + assert_eq!( + flags, + Flags { + subcommand: DenoSubcommand::Upgrade { + force: true, + dry_run: true, + }, + ..Flags::default() + } + ); + } + #[test] fn version() { let r = flags_from_vec_safe(svec!["deno", "--version"]); diff --git a/cli/lib.rs b/cli/lib.rs index c6bbe0b68f..ba5152bd62 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -14,6 +14,8 @@ extern crate indexmap; #[cfg(unix)] extern crate nix; extern crate rand; +extern crate regex; +extern crate reqwest; extern crate serde; extern crate serde_derive; extern crate tokio; @@ -52,6 +54,7 @@ pub mod state; mod test_runner; pub mod test_util; mod tokio_util; +mod upgrade; pub mod version; mod web_worker; pub mod worker; @@ -75,6 +78,7 @@ use log::Record; use std::env; use std::io::Write; use std::path::PathBuf; +use upgrade::upgrade_command; use url::Url; static LOGGER: Logger = Logger; @@ -487,6 +491,9 @@ pub fn main() { let _r = std::io::stdout().write_all(types.as_bytes()); return; } + DenoSubcommand::Upgrade { force, dry_run } => { + upgrade_command(dry_run, force).boxed_local() + } _ => unreachable!(), }; diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index ba1880b803..ed51605652 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -168,6 +168,30 @@ fn fmt_stdin_error() { assert!(!output.status.success()); } +// Warning: this test requires internet access. +#[test] +fn upgrade_in_tmpdir() { + let temp_dir = TempDir::new().unwrap(); + let exe_path = if cfg!(windows) { + temp_dir.path().join("deno") + } else { + temp_dir.path().join("deno.exe") + }; + let _ = std::fs::copy(util::deno_exe_path(), &exe_path).unwrap(); + assert!(exe_path.exists()); + let _mtime1 = std::fs::metadata(&exe_path).unwrap().modified().unwrap(); + let status = Command::new(&exe_path) + .arg("upgrade") + .arg("--force") + .spawn() + .unwrap() + .wait() + .unwrap(); + assert!(status.success()); + let _mtime2 = std::fs::metadata(&exe_path).unwrap().modified().unwrap(); + // TODO(ry) assert!(mtime1 < mtime2); +} + #[test] fn installer_test_local_module_run() { let temp_dir = TempDir::new().expect("tempdir fail"); diff --git a/cli/upgrade.rs b/cli/upgrade.rs new file mode 100644 index 0000000000..519f8d6bc1 --- /dev/null +++ b/cli/upgrade.rs @@ -0,0 +1,224 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +//! This module provides feature to upgrade deno executable +//! +//! At the moment it is only consumed using CLI but in +//! the future it can be easily extended to provide +//! the same functions as ops available in JS runtime. + +extern crate semver_parser; +use crate::futures::FutureExt; +use crate::http_util::fetch_once; +use crate::http_util::FetchOnceResult; +use crate::op_error::OpError; +use crate::ErrBox; +use regex::Regex; +use reqwest::{redirect::Policy, Client}; +use semver_parser::version::parse as semver_parse; +use semver_parser::version::Version; +use std::fs; +use std::future::Future; +use std::io::prelude::*; +use std::path::Path; +use std::path::PathBuf; +use std::pin::Pin; +use std::process::Command; +use std::process::Stdio; +use std::string::String; +use tempfile::TempDir; +use url::Url; + +// TODO(ry) Auto detect target triples for the uploaded files. +#[cfg(windows)] +const ARCHIVE_NAME: &str = "deno-x86_64-pc-windows-msvc.zip"; +#[cfg(target_os = "macos")] +const ARCHIVE_NAME: &str = "deno-x86_64-apple-darwin.zip"; +#[cfg(target_os = "linux")] +const ARCHIVE_NAME: &str = "deno-x86_64-unknown-linux-gnu.zip"; + +async fn get_latest_version(client: &Client) -> Result { + println!("Checking for latest version"); + let body = client + .get(Url::parse( + "https://github.com/denoland/deno/releases/latest", + )?) + .send() + .await? + .text() + .await?; + let v = find_version(&body)?; + Ok(semver_parse(&v).unwrap()) +} + +/// Asynchronously updates deno executable to greatest version +/// if greatest version is available. +pub async fn upgrade_command(dry_run: bool, force: bool) -> Result<(), ErrBox> { + let client = Client::builder().redirect(Policy::none()).build()?; + let latest_version = get_latest_version(&client).await?; + let current_version = semver_parse(crate::version::DENO).unwrap(); + + if !force && current_version >= latest_version { + println!( + "Local deno version {} is the most recent release", + &crate::version::DENO + ); + } else { + println!( + "New version has been found\nDeno is upgrading to version {}", + &latest_version + ); + let archive_data = + download_package(&compose_url_to_exec(&latest_version)?, client).await?; + + let old_exe_path = std::env::current_exe()?; + let new_exe_path = unpack(archive_data)?; + let permissions = fs::metadata(&old_exe_path)?.permissions(); + fs::set_permissions(&new_exe_path, permissions)?; + check_exe(&new_exe_path, &latest_version)?; + + if !dry_run { + replace_exe(&new_exe_path, &old_exe_path)?; + } + + println!("Upgrade done successfully") + } + Ok(()) +} + +fn download_package( + url: &Url, + client: Client, +) -> Pin, ErrBox>>>> { + println!("downloading {}", url); + let url = url.clone(); + let fut = async move { + match fetch_once(client.clone(), &url, None).await? { + FetchOnceResult::Code(source, _) => Ok(source), + FetchOnceResult::NotModified => unreachable!(), + FetchOnceResult::Redirect(_url, _) => { + download_package(&_url, client).await + } + } + }; + fut.boxed_local() +} + +fn compose_url_to_exec(version: &Version) -> Result { + let s = format!( + "https://github.com/denoland/deno/releases/download/v{}/{}", + version, ARCHIVE_NAME + ); + Ok(Url::parse(&s)?) +} + +fn find_version(text: &str) -> Result { + let re = Regex::new(r#"v([^\?]+)?""#)?; + if let Some(_mat) = re.find(text) { + let mat = _mat.as_str(); + return Ok(mat[1..mat.len() - 1].to_string()); + } + Err(OpError::other("Cannot read latest tag version".to_string()).into()) +} + +fn unpack(archive_data: Vec) -> Result { + // We use into_path so that the tempdir is not automatically deleted. This is + // useful for debugging upgrade, but also so this function can return a path + // to the newly uncompressed file without fear of the tempdir being deleted. + let temp_dir = TempDir::new()?.into_path(); + let exe_ext = if cfg!(windows) { "exe" } else { "" }; + let exe_path = temp_dir.join("deno").with_extension(exe_ext); + assert!(!exe_path.exists()); + + let archive_ext = Path::new(ARCHIVE_NAME) + .extension() + .and_then(|ext| ext.to_str()) + .unwrap(); + let unpack_status = match archive_ext { + "gz" => { + let exe_file = fs::File::create(&exe_path)?; + let mut cmd = Command::new("gunzip") + .arg("-c") + .stdin(Stdio::piped()) + .stdout(Stdio::from(exe_file)) + .spawn()?; + cmd.stdin.as_mut().unwrap().write_all(&archive_data)?; + cmd.wait()? + } + "zip" => { + if cfg!(windows) { + let archive_path = temp_dir.join("deno.zip"); + fs::write(&archive_path, &archive_data)?; + Command::new("powershell.exe") + .arg("-Command") + .arg("Expand-Archive") + .arg("-Path") + .arg(&archive_path) + .arg("-DestinationPath") + .arg(&temp_dir) + .spawn()? + .wait()? + } else { + let archive_path = temp_dir.join("deno.zip"); + fs::write(&archive_path, &archive_data)?; + Command::new("unzip") + .current_dir(&temp_dir) + .arg(archive_path) + .spawn()? + .wait()? + } + } + ext => panic!("Unsupported archive type: '{}'", ext), + }; + assert!(unpack_status.success()); + assert!(exe_path.exists()); + Ok(exe_path) +} + +fn replace_exe(new: &Path, old: &Path) -> Result<(), ErrBox> { + if cfg!(windows) { + // On windows you cannot replace the currently running executable. + // so first we rename it to deno.old.exe + fs::rename(old, old.with_extension("old.exe"))?; + } else { + fs::remove_file(old)?; + } + // Windows cannot rename files across device boundaries, so if rename fails, + // we try again with copy. + fs::rename(new, old).or_else(|_| fs::copy(new, old).map(|_| ()))?; + Ok(()) +} + +fn check_exe( + exe_path: &Path, + expected_version: &Version, +) -> Result<(), ErrBox> { + let output = Command::new(exe_path) + .arg("-V") + .stderr(std::process::Stdio::inherit()) + .output()?; + let stdout = String::from_utf8(output.stdout)?; + assert!(output.status.success()); + assert_eq!(stdout.trim(), format!("deno {}", expected_version)); + Ok(()) +} + +#[test] +fn test_find_version() { + let url = "You are being redirected."; + assert_eq!(find_version(url).unwrap(), "0.36.0".to_string()); +} + +#[test] +fn test_compose_url_to_exec() { + let v = semver_parse("0.0.1").unwrap(); + let url = compose_url_to_exec(&v).unwrap(); + #[cfg(windows)] + assert_eq!(url.as_str(), "https://github.com/denoland/deno/releases/download/v0.0.1/deno-x86_64-pc-windows-msvc.zip"); + #[cfg(target_os = "macos")] + assert_eq!( + url.as_str(), + "https://github.com/denoland/deno/releases/download/v0.0.1/deno-x86_64-apple-darwin.zip" + ); + #[cfg(target_os = "linux")] + assert_eq!(url.as_str(), "https://github.com/denoland/deno/releases/download/v0.0.1/deno-x86_64-unknown-linux-gnu.zip"); +}