summaryrefslogtreecommitdiff
path: root/cli/tools/upgrade.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools/upgrade.rs')
-rw-r--r--cli/tools/upgrade.rs276
1 files changed, 276 insertions, 0 deletions
diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs
new file mode 100644
index 000000000..2bc28ec7d
--- /dev/null
+++ b/cli/tools/upgrade.rs
@@ -0,0 +1,276 @@
+// 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.
+
+use crate::http_util::fetch_once;
+use crate::http_util::FetchOnceResult;
+use crate::AnyError;
+use deno_core::error::custom_error;
+use deno_core::futures::FutureExt;
+use deno_core::url::Url;
+use deno_fetch::reqwest;
+use deno_fetch::reqwest::redirect::Policy;
+use deno_fetch::reqwest::Client;
+use regex::Regex;
+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;
+
+lazy_static! {
+ static ref ARCHIVE_NAME: String = format!("deno-{}.zip", env!("TARGET"));
+}
+
+async fn get_latest_version(client: &Client) -> Result<Version, AnyError> {
+ 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,
+ version: Option<String>,
+ output: Option<PathBuf>,
+ ca_file: Option<String>,
+) -> Result<(), AnyError> {
+ let mut client_builder = Client::builder().redirect(Policy::none());
+
+ // If we have been provided a CA Certificate, add it into the HTTP client
+ if let Some(ca_file) = ca_file {
+ let buf = std::fs::read(ca_file);
+ let cert = reqwest::Certificate::from_pem(&buf.unwrap())?;
+ client_builder = client_builder.add_root_certificate(cert);
+ }
+
+ let client = client_builder.build()?;
+
+ let current_version = semver_parse(crate::version::DENO).unwrap();
+
+ let install_version = match version {
+ Some(passed_version) => match semver_parse(&passed_version) {
+ Ok(ver) => {
+ if !force && current_version == ver {
+ println!("Version {} is already installed", &ver);
+ return Ok(());
+ } else {
+ ver
+ }
+ }
+ Err(_) => {
+ eprintln!("Invalid semver passed");
+ std::process::exit(1)
+ }
+ },
+ None => {
+ let latest_version = get_latest_version(&client).await?;
+
+ if !force && current_version >= latest_version {
+ println!(
+ "Local deno version {} is the most recent release",
+ &crate::version::DENO
+ );
+ return Ok(());
+ } else {
+ latest_version
+ }
+ }
+ };
+
+ let archive_data = download_package(
+ &compose_url_to_exec(&install_version)?,
+ client,
+ &install_version,
+ )
+ .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, &install_version)?;
+
+ if !dry_run {
+ match output {
+ Some(path) => {
+ fs::rename(&new_exe_path, &path)
+ .or_else(|_| fs::copy(&new_exe_path, &path).map(|_| ()))?;
+ }
+ None => replace_exe(&new_exe_path, &old_exe_path)?,
+ }
+ }
+
+ println!("Upgrade done successfully");
+
+ Ok(())
+}
+
+fn download_package(
+ url: &Url,
+ client: Client,
+ version: &Version,
+) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, AnyError>>>> {
+ println!("downloading {}", url);
+ let url = url.clone();
+ let version = version.clone();
+ let fut = async move {
+ match fetch_once(client.clone(), &url, None).await {
+ Ok(result) => {
+ println!(
+ "Version has been found\nDeno is upgrading to version {}",
+ &version
+ );
+ match result {
+ FetchOnceResult::Code(source, _) => Ok(source),
+ FetchOnceResult::NotModified => unreachable!(),
+ FetchOnceResult::Redirect(_url, _) => {
+ download_package(&_url, client, &version).await
+ }
+ }
+ }
+ Err(_) => {
+ println!("Version has not been found, aborting");
+ std::process::exit(1)
+ }
+ }
+ };
+ fut.boxed_local()
+}
+
+fn compose_url_to_exec(version: &Version) -> Result<Url, AnyError> {
+ let s = format!(
+ "https://github.com/denoland/deno/releases/download/v{}/{}",
+ version, *ARCHIVE_NAME
+ );
+ Url::parse(&s).map_err(AnyError::from)
+}
+
+fn find_version(text: &str) -> Result<String, AnyError> {
+ 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(custom_error("NotFound", "Cannot read latest tag version"))
+}
+
+fn unpack(archive_data: Vec<u8>) -> Result<PathBuf, std::io::Error> {
+ // 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("-NoLogo")
+ .arg("-NoProfile")
+ .arg("-NonInteractive")
+ .arg("-Command")
+ .arg(
+ "& {
+ param($Path, $DestinationPath)
+ trap { $host.ui.WriteErrorLine($_.Exception); exit 1 }
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ [System.IO.Compression.ZipFile]::ExtractToDirectory(
+ $Path,
+ $DestinationPath
+ );
+ }",
+ )
+ .arg("-Path")
+ .arg(format!("'{}'", &archive_path.to_str().unwrap()))
+ .arg("-DestinationPath")
+ .arg(format!("'{}'", &temp_dir.to_str().unwrap()))
+ .spawn()?
+ .wait()?
+ }
+ "zip" => {
+ 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<(), std::io::Error> {
+ 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<(), AnyError> {
+ 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());
+}