1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-23 07:44:48 -05:00
denoland-deno/cli/installer.rs
2020-09-06 02:34:02 +02:00

706 lines
20 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::flags::Flags;
use crate::global_state::GlobalState;
use deno_core::ErrBox;
use deno_core::ModuleSpecifier;
use log::Level;
use regex::{Regex, RegexBuilder};
use std::env;
use std::fs;
use std::fs::File;
use std::io::Error;
use std::io::ErrorKind;
use std::io::Write;
#[cfg(not(windows))]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use url::Url;
lazy_static! {
static ref EXEC_NAME_RE: Regex = RegexBuilder::new(
r"^[a-z][\w-]*$"
).case_insensitive(true).build().unwrap();
// Regular expression to test disk driver letter. eg "C:\\User\username\path\to"
static ref DRIVE_LETTER_REG: Regex = RegexBuilder::new(
r"^[c-z]:"
).case_insensitive(true).build().unwrap();
}
pub fn is_remote_url(module_url: &str) -> bool {
let lower = module_url.to_lowercase();
lower.starts_with("http://") || lower.starts_with("https://")
}
fn validate_name(exec_name: &str) -> Result<(), Error> {
if EXEC_NAME_RE.is_match(exec_name) {
Ok(())
} else {
Err(Error::new(
ErrorKind::Other,
format!("Invalid executable name: {}", exec_name),
))
}
}
#[cfg(windows)]
/// On Windows if user is using Powershell .cmd extension is need to run the
/// installed module.
/// Generate batch script to satisfy that.
fn generate_executable_file(
file_path: PathBuf,
args: Vec<String>,
) -> Result<(), Error> {
let args: Vec<String> = args.iter().map(|c| format!("\"{}\"", c)).collect();
let template = format!(
"% generated by deno install %\n@deno.exe {} %*\n",
args.join(" ")
);
let mut file = File::create(&file_path)?;
file.write_all(template.as_bytes())?;
Ok(())
}
#[cfg(not(windows))]
fn generate_executable_file(
file_path: PathBuf,
args: Vec<String>,
) -> Result<(), Error> {
let args: Vec<String> = args.iter().map(|c| format!("\"{}\"", c)).collect();
let template = format!(
r#"#!/bin/sh
# generated by deno install
deno {} "$@"
"#,
args.join(" "),
);
let mut file = File::create(&file_path)?;
file.write_all(template.as_bytes())?;
let _metadata = fs::metadata(&file_path)?;
let mut permissions = _metadata.permissions();
permissions.set_mode(0o755);
fs::set_permissions(&file_path, permissions)?;
Ok(())
}
async fn generate_bundle(
flags: Flags,
module_specifier: ModuleSpecifier,
script_path: PathBuf,
) -> Result<(), ErrBox> {
let global_state = GlobalState::new(flags.clone())?;
let source = global_state
.ts_compiler
.bundle(&global_state, module_specifier)
.await?;
let mut file = File::create(&script_path)?;
file.write_all(source.as_bytes())?;
Ok(())
}
fn get_installer_root() -> Result<PathBuf, Error> {
if let Ok(env_dir) = env::var("DENO_INSTALL_ROOT") {
if !env_dir.is_empty() {
return PathBuf::from(env_dir).canonicalize();
}
}
// Note: on Windows, the $HOME environment variable may be set by users or by
// third party software, but it is non-standard and should not be relied upon.
let home_env_var = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
let mut home_path =
env::var_os(home_env_var)
.map(PathBuf::from)
.ok_or_else(|| {
Error::new(
ErrorKind::NotFound,
format!("${} is not defined", home_env_var),
)
})?;
home_path.push(".deno");
Ok(home_path)
}
fn infer_name_from_url(url: &Url) -> Option<String> {
let path = PathBuf::from(url.path());
let mut stem = match path.file_stem() {
Some(stem) => stem.to_string_lossy().to_string(),
None => return None,
};
if stem == "main" || stem == "mod" || stem == "index" || stem == "cli" {
if let Some(parent_name) = path.parent().and_then(|p| p.file_name()) {
stem = parent_name.to_string_lossy().to_string();
}
}
let stem = stem.splitn(2, '@').next().unwrap().to_string();
Some(stem)
}
pub async fn install(
flags: Flags,
module_url: &str,
args: Vec<String>,
name: Option<String>,
root: Option<PathBuf>,
force: bool,
) -> Result<(), ErrBox> {
let root = if let Some(root) = root {
root.canonicalize()?
} else {
get_installer_root()?
};
let installation_dir = root.join("bin");
// ensure directory exists
if let Ok(metadata) = fs::metadata(&installation_dir) {
if !metadata.is_dir() {
return Err(ErrBox::from(Error::new(
ErrorKind::Other,
"Installation path is not a directory",
)));
}
} else {
fs::create_dir_all(&installation_dir)?;
};
let module_specifier = ModuleSpecifier::resolve_url_or_path(module_url)?;
let module_specifier_ = module_specifier.clone();
let url = module_specifier_.as_url();
let name = name.or_else(|| infer_name_from_url(&url));
let name = match name {
Some(name) => name,
None => return Err(ErrBox::from(Error::new(
ErrorKind::Other,
"An executable name was not provided. One could not be inferred from the URL. Aborting.",
))),
};
validate_name(name.as_str())?;
let mut file_path = installation_dir.join(&name);
let script_path = file_path.with_extension("js");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
if file_path.exists() && !force {
return Err(ErrBox::from(Error::new(
ErrorKind::Other,
"Existing installation found. Aborting (Use -f to overwrite).",
)));
};
let mut executable_args = vec!["run".to_string()];
executable_args.extend_from_slice(&flags.to_permission_args());
if let Some(ca_file) = flags.ca_file.clone() {
executable_args.push("--cert".to_string());
executable_args.push(ca_file)
}
if let Some(log_level) = flags.log_level {
if log_level == Level::Error {
executable_args.push("--quiet".to_string());
} else {
executable_args.push("--log-level".to_string());
let log_level = match log_level {
Level::Debug => "debug",
Level::Info => "info",
_ => {
return Err(ErrBox::from(Error::new(
ErrorKind::Other,
format!("invalid log level {}", log_level),
)))
}
};
executable_args.push(log_level.to_string());
}
}
if flags.no_check {
executable_args.push("--no-check".to_string());
}
if flags.unstable {
executable_args.push("--unstable".to_string());
}
executable_args.push(script_path.to_string_lossy().to_string());
executable_args.extend_from_slice(&args);
generate_executable_file(file_path.to_owned(), executable_args)?;
generate_bundle(flags, module_specifier, script_path).await?;
println!("✅ Successfully installed {}", name);
println!("{}", file_path.to_string_lossy());
let installation_dir_str = installation_dir.to_string_lossy();
if !is_in_path(&installation_dir) {
println!(" Add {} to PATH", installation_dir_str);
if cfg!(windows) {
println!(" set PATH=%PATH%;{}", installation_dir_str);
} else {
println!(" export PATH=\"{}:$PATH\"", installation_dir_str);
}
}
Ok(())
}
fn is_in_path(dir: &PathBuf) -> bool {
if let Some(paths) = env::var_os("PATH") {
for p in env::split_paths(&paths) {
if *dir == p {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use tempfile::TempDir;
lazy_static! {
pub static ref ENV_LOCK: Mutex<()> = Mutex::new(());
}
#[test]
fn test_is_remote_url() {
assert!(is_remote_url("https://deno.land/std/http/file_server.ts"));
assert!(is_remote_url("http://deno.land/std/http/file_server.ts"));
assert!(is_remote_url("HTTP://deno.land/std/http/file_server.ts"));
assert!(is_remote_url("HTTp://deno.land/std/http/file_server.ts"));
assert!(!is_remote_url("file:///dev/deno_std/http/file_server.ts"));
assert!(!is_remote_url("./dev/deno_std/http/file_server.ts"));
}
#[test]
fn install_infer_name_from_url() {
assert_eq!(
infer_name_from_url(
&Url::parse("https://example.com/abc/server.ts").unwrap()
),
Some("server".to_string())
);
assert_eq!(
infer_name_from_url(
&Url::parse("https://example.com/abc/main.ts").unwrap()
),
Some("abc".to_string())
);
assert_eq!(
infer_name_from_url(
&Url::parse("https://example.com/abc/mod.ts").unwrap()
),
Some("abc".to_string())
);
assert_eq!(
infer_name_from_url(
&Url::parse("https://example.com/abc/index.ts").unwrap()
),
Some("abc".to_string())
);
assert_eq!(
infer_name_from_url(
&Url::parse("https://example.com/abc/cli.ts").unwrap()
),
Some("abc".to_string())
);
assert_eq!(
infer_name_from_url(&Url::parse("https://example.com/main.ts").unwrap()),
Some("main".to_string())
);
assert_eq!(
infer_name_from_url(&Url::parse("https://example.com").unwrap()),
None
);
assert_eq!(
infer_name_from_url(&Url::parse("file:///abc/server.ts").unwrap()),
Some("server".to_string())
);
assert_eq!(
infer_name_from_url(&Url::parse("file:///abc/main.ts").unwrap()),
Some("abc".to_string())
);
assert_eq!(
infer_name_from_url(&Url::parse("file:///main.ts").unwrap()),
Some("main".to_string())
);
assert_eq!(infer_name_from_url(&Url::parse("file:///").unwrap()), None);
assert_eq!(
infer_name_from_url(
&Url::parse("https://example.com/abc@0.1.0").unwrap()
),
Some("abc".to_string())
);
assert_eq!(
infer_name_from_url(
&Url::parse("https://example.com/abc@0.1.0/main.ts").unwrap()
),
Some("abc".to_string())
);
assert_eq!(
infer_name_from_url(
&Url::parse("https://example.com/abc@def@ghi").unwrap()
),
Some("abc".to_string())
);
}
#[tokio::test]
async fn install_basic() {
let _guard = ENV_LOCK.lock().ok();
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new().expect("tempdir fail");
let temp_dir_str = temp_dir.path().to_string_lossy().to_string();
// NOTE: this test overrides environmental variables
// don't add other tests in this file that mess with "HOME" and "USEPROFILE"
// otherwise transient failures are possible because tests are run in parallel.
// It means that other test can override env vars when this test is running.
let original_home = env::var_os("HOME");
let original_user_profile = env::var_os("HOME");
let original_install_root = env::var_os("DENO_INSTALL_ROOT");
env::set_var("HOME", &temp_dir_str);
env::set_var("USERPROFILE", &temp_dir_str);
env::set_var("DENO_INSTALL_ROOT", "");
install(
Flags::default(),
"http://localhost:4545/cli/tests/echo_server.ts",
vec![],
Some("echo_test".to_string()),
None,
false,
)
.await
.expect("Install failed");
let mut file_path = temp_dir.path().join(".deno/bin/echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
let content = fs::read_to_string(file_path).unwrap();
// It's annoying when shell scripts don't have NL at the end.
assert_eq!(content.chars().last().unwrap(), '\n');
assert!(content.contains(r#""run""#));
assert!(content.contains(r#"echo_test.js"#));
if let Some(home) = original_home {
env::set_var("HOME", home);
}
if let Some(user_profile) = original_user_profile {
env::set_var("USERPROFILE", user_profile);
}
if let Some(install_root) = original_install_root {
env::set_var("DENO_INSTALL_ROOT", install_root);
}
}
#[tokio::test]
async fn install_unstable() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new().expect("tempdir fail");
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
install(
Flags {
unstable: true,
..Flags::default()
},
"http://localhost:4545/cli/tests/echo_server.ts",
vec![],
Some("echo_test".to_string()),
Some(temp_dir.path().to_path_buf()),
false,
)
.await
.expect("Install failed");
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
assert!(file_path.with_extension("js").exists());
let content = fs::read_to_string(file_path).unwrap();
println!("this is the file path {:?}", content);
assert!(content.contains(r#""run" "--unstable""#));
assert!(content.contains(r#"echo_test.js"#));
}
#[tokio::test]
async fn install_inferred_name() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new().expect("tempdir fail");
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
install(
Flags::default(),
"http://localhost:4545/cli/tests/echo_server.ts",
vec![],
None,
Some(temp_dir.path().to_path_buf()),
false,
)
.await
.expect("Install failed");
let mut file_path = bin_dir.join("echo_server");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
assert!(file_path.with_extension("js").exists());
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains(r#""run""#));
assert!(content.contains(r#"echo_server.js"#));
}
#[tokio::test]
async fn install_inferred_name_from_parent() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new().expect("tempdir fail");
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
install(
Flags::default(),
"http://localhost:4545/cli/tests/subdir/main.ts",
vec![],
None,
Some(temp_dir.path().to_path_buf()),
false,
)
.await
.expect("Install failed");
let mut file_path = bin_dir.join("subdir");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
assert!(file_path.with_extension("js").exists());
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains(r#""run""#));
assert!(content.contains(r#"subdir.js"#));
}
#[tokio::test]
async fn install_custom_dir_option() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new().expect("tempdir fail");
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
install(
Flags::default(),
"http://localhost:4545/cli/tests/echo_server.ts",
vec![],
Some("echo_test".to_string()),
Some(temp_dir.path().to_path_buf()),
false,
)
.await
.expect("Install failed");
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
assert!(file_path.with_extension("js").exists());
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains(r#""run""#));
assert!(content.contains(r#"echo_test.js"#));
}
#[tokio::test]
async fn install_custom_dir_env_var() {
let _guard = ENV_LOCK.lock().ok();
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new().expect("tempdir fail");
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
let original_install_root = env::var_os("DENO_INSTALL_ROOT");
env::set_var("DENO_INSTALL_ROOT", temp_dir.path().to_path_buf());
install(
Flags::default(),
"http://localhost:4545/cli/tests/echo_server.ts",
vec![],
Some("echo_test".to_string()),
None,
false,
)
.await
.expect("Install failed");
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
assert!(file_path.with_extension("js").exists());
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains(r#""run""#));
assert!(content.contains(r#"echo_test.js"#));
if let Some(install_root) = original_install_root {
env::set_var("DENO_INSTALL_ROOT", install_root);
}
}
#[tokio::test]
async fn install_with_flags() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new().expect("tempdir fail");
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
install(
Flags {
allow_net: true,
allow_read: true,
no_check: true,
log_level: Some(Level::Error),
..Flags::default()
},
"http://localhost:4545/cli/tests/echo_server.ts",
vec!["--foobar".to_string()],
Some("echo_test".to_string()),
Some(temp_dir.path().to_path_buf()),
false,
)
.await
.expect("Install failed");
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
assert!(file_path.with_extension("js").exists());
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains(
r#""run" "--allow-read" "--allow-net" "--quiet" "--no-check""#
));
assert!(content.contains(r#"echo_test.js" "--foobar""#));
}
#[tokio::test]
async fn install_local_module() {
let temp_dir = TempDir::new().expect("tempdir fail");
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
let local_module = env::current_dir().unwrap().join("tests/echo_server.ts");
let local_module_str = local_module.to_string_lossy();
install(
Flags::default(),
&local_module_str,
vec![],
Some("echo_test".to_string()),
Some(temp_dir.path().to_path_buf()),
false,
)
.await
.expect("Install failed");
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
assert!(file_path.with_extension("js").exists());
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains("echo_test.js"));
}
#[tokio::test]
async fn install_force() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new().expect("tempdir fail");
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
install(
Flags::default(),
"http://localhost:4545/cli/tests/echo_server.ts",
vec![],
Some("echo_test".to_string()),
Some(temp_dir.path().to_path_buf()),
false,
)
.await
.expect("Install failed");
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
assert!(file_path.with_extension("js").exists());
// No force. Install failed.
let no_force_result = install(
Flags {
unstable: true,
..Flags::default()
},
"http://localhost:4545/cli/tests/cat.ts", // using a different URL
vec![],
Some("echo_test".to_string()),
Some(temp_dir.path().to_path_buf()),
false,
)
.await;
assert!(no_force_result.is_err());
assert!(no_force_result
.unwrap_err()
.to_string()
.contains("Existing installation found"));
// Assert not modified
let file_content = fs::read_to_string(&file_path).unwrap();
assert!(!file_content.contains("--unstable"));
// Force. Install success.
let force_result = install(
Flags {
unstable: true,
..Flags::default()
},
"http://localhost:4545/cli/tests/cat.ts", // using a different URL
vec![],
Some("echo_test".to_string()),
Some(temp_dir.path().to_path_buf()),
true,
)
.await;
assert!(force_result.is_ok());
// Assert modified
let file_content_2 = fs::read_to_string(&file_path).unwrap();
assert!(file_content_2.contains("--unstable"));
}
}