mirror of
https://github.com/denoland/deno.git
synced 2024-12-23 07:44:48 -05:00
feat: first pass at "deno upgrade" (#4328)
This commit is contained in:
parent
a0ba476fee
commit
ec07386067
6 changed files with 319 additions and 2 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
56
cli/flags.rs
56
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
|
||||
|
@ -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"]);
|
||||
|
|
|
@ -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!(),
|
||||
};
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
224
cli/upgrade.rs
Normal file
224
cli/upgrade.rs
Normal file
|
@ -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");
|
||||
}
|
Loading…
Reference in a new issue