summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbartOssh <lenart.consulting@gmail.com>2020-03-23 16:37:24 +0100
committerGitHub <noreply@github.com>2020-03-23 11:37:24 -0400
commitec0738606753b34525988fd9f58a23cb300b7bc0 (patch)
tree4a0bdc3585b2e64f50c739d49eba1e5cd9c3ee1d
parenta0ba476feef9dcc1b3bbd5144d2e48ef5053ee09 (diff)
feat: first pass at "deno upgrade" (#4328)
-rw-r--r--Cargo.lock9
-rw-r--r--cli/Cargo.toml1
-rw-r--r--cli/flags.rs56
-rw-r--r--cli/lib.rs7
-rw-r--r--cli/tests/integration_tests.rs24
-rw-r--r--cli/upgrade.rs224
6 files changed, 319 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0105ea6a3..b658a2b6a 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]]
@@ -1980,6 +1981,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 6dfa81477..c6b38de8b 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 9e1fbf5df..475172f0a 100644
--- a/cli/flags.rs
+++ b/cli/flags.rs
@@ -63,6 +63,10 @@ pub enum DenoSubcommand {
include: Option<Vec<String>>,
},
Types,
+ Upgrade {
+ dry_run: bool,
+ force: bool,
+ },
}
impl Default for DenoSubcommand {
@@ -250,6 +254,8 @@ pub fn flags_from_vec_safe(args: Vec<String>) -> clap::Result<Flags> {
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<String>) -> Vec<String> {
"types",
"install",
"help",
- "version"
+ "version",
+ "upgrade"
];
let modifier_flags = sset!["-h", "--help", "-V", "--version"];
// deno [subcommand|behavior modifier flags] -> do nothing
@@ -1189,6 +1226,23 @@ mod tests {
}
#[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"]);
assert_eq!(r.unwrap_err().kind, clap::ErrorKind::VersionDisplayed);
diff --git a/cli/lib.rs b/cli/lib.rs
index c6bbe0b68..ba5152bd6 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 ba1880b80..ed5160565 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 000000000..519f8d6bc
--- /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<Version, ErrBox> {
+ 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<Box<dyn Future<Output = Result<Vec<u8>, 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<Url, ErrBox> {
+ let s = format!(
+ "https://github.com/denoland/deno/releases/download/v{}/{}",
+ version, ARCHIVE_NAME
+ );
+ Ok(Url::parse(&s)?)
+}
+
+fn find_version(text: &str) -> Result<String, ErrBox> {
+ 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<u8>) -> Result<PathBuf, ErrBox> {
+ // 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 = "<html><body>You are being <a href=\"https://github.com/denoland/deno/releases/tag/v0.36.0\">redirected</a>.</body></html>";
+ 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");
+}