diff options
Diffstat (limited to 'cli/tools/installer.rs')
-rw-r--r-- | cli/tools/installer.rs | 813 |
1 files changed, 813 insertions, 0 deletions
diff --git a/cli/tools/installer.rs b/cli/tools/installer.rs new file mode 100644 index 000000000..e0a99873a --- /dev/null +++ b/cli/tools/installer.rs @@ -0,0 +1,813 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::flags::Flags; +use crate::fs_util::canonicalize_path; +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::url::Url; +use log::Level; +use regex::{Regex, RegexBuilder}; +use std::env; +use std::fs; +use std::fs::File; +use std::io; +use std::io::Write; +#[cfg(not(windows))] +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; + +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<(), AnyError> { + if EXEC_NAME_RE.is_match(exec_name) { + Ok(()) + } else { + Err(generic_error(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<(), AnyError> { + 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<(), AnyError> { + use shell_escape::escape; + let args: Vec<String> = args + .into_iter() + .map(|c| escape(c.into()).into_owned()) + .collect(); + let template = format!( + r#"#!/bin/sh +# generated by deno install +exec 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(()) +} + +fn get_installer_root() -> Result<PathBuf, io::Error> { + if let Ok(env_dir) = env::var("DENO_INSTALL_ROOT") { + if !env_dir.is_empty() { + return canonicalize_path(&PathBuf::from(env_dir)); + } + } + // 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(|| { + io::Error::new( + io::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 fn install( + flags: Flags, + module_url: &str, + args: Vec<String>, + name: Option<String>, + root: Option<PathBuf>, + force: bool, +) -> Result<(), AnyError> { + let root = if let Some(root) = root { + canonicalize_path(&root)? + } 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(generic_error("Installation path is not a directory")); + } + } else { + fs::create_dir_all(&installation_dir)?; + }; + + // Check if module_url is remote + let module_url = if is_remote_url(module_url) { + Url::parse(module_url).expect("Should be valid url") + } else { + let module_path = PathBuf::from(module_url); + let module_path = if module_path.is_absolute() { + module_path + } else { + let cwd = env::current_dir().unwrap(); + cwd.join(module_path) + }; + Url::from_file_path(module_path).expect("Path should be absolute") + }; + + let name = name.or_else(|| infer_name_from_url(&module_url)); + + let name = match name { + Some(name) => name, + None => return Err(generic_error( + "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); + + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + if file_path.exists() && !force { + return Err(generic_error( + "Existing installation found. Aborting (Use -f to overwrite).", + )); + }; + + let mut extra_files: Vec<(PathBuf, String)> = vec![]; + + 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 { + 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(generic_error(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()); + } + + if flags.no_remote { + executable_args.push("--no-remote".to_string()); + } + + if flags.lock_write { + executable_args.push("--lock-write".to_string()); + } + + if flags.cached_only { + executable_args.push("--cached_only".to_string()); + } + + if let Some(v8_flags) = flags.v8_flags { + executable_args.push(format!("--v8-flags={}", v8_flags.join(","))); + } + + if let Some(seed) = flags.seed { + executable_args.push("--seed".to_string()); + executable_args.push(seed.to_string()); + } + + if let Some(inspect) = flags.inspect { + executable_args.push(format!("--inspect={}", inspect.to_string())); + } + + if let Some(inspect_brk) = flags.inspect_brk { + executable_args.push(format!("--inspect-brk={}", inspect_brk.to_string())); + } + + if let Some(import_map_path) = flags.import_map_path { + let mut copy_path = file_path.clone(); + copy_path.set_extension("import_map.json"); + executable_args.push("--import-map".to_string()); + executable_args.push(copy_path.to_str().unwrap().to_string()); + extra_files.push((copy_path, fs::read_to_string(import_map_path)?)); + } + + if let Some(config_path) = flags.config_path { + let mut copy_path = file_path.clone(); + copy_path.set_extension("tsconfig.json"); + executable_args.push("--config".to_string()); + executable_args.push(copy_path.to_str().unwrap().to_string()); + extra_files.push((copy_path, fs::read_to_string(config_path)?)); + } + + if let Some(lock_path) = flags.lock { + let mut copy_path = file_path.clone(); + copy_path.set_extension("lock.json"); + executable_args.push("--lock".to_string()); + executable_args.push(copy_path.to_str().unwrap().to_string()); + extra_files.push((copy_path, fs::read_to_string(lock_path)?)); + } + + executable_args.push(module_url.to_string()); + executable_args.extend_from_slice(&args); + + generate_executable_file(file_path.to_owned(), executable_args)?; + for (path, contents) in extra_files { + fs::write(path, contents)?; + } + + 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()) + ); + } + + #[test] + fn install_basic() { + let _guard = ENV_LOCK.lock().ok(); + 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, + ) + .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'); + + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); + } + 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); + } + } + + #[test] + fn install_unstable() { + 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, + ) + .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()); + + let content = fs::read_to_string(file_path).unwrap(); + println!("this is the file path {:?}", content); + if cfg!(windows) { + assert!(content.contains( + r#""run" "--unstable" "http://localhost:4545/cli/tests/echo_server.ts""# + )); + } else { + assert!(content.contains( + r#"run --unstable 'http://localhost:4545/cli/tests/echo_server.ts'"# + )); + } + } + + #[test] + fn install_inferred_name() { + 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, + ) + .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()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); + } + } + + #[test] + fn install_inferred_name_from_parent() { + 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, + ) + .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()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/subdir/main.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/subdir/main.ts'"#)); + } + } + + #[test] + fn install_custom_dir_option() { + 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, + ) + .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()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); + } + } + + #[test] + fn install_custom_dir_env_var() { + let _guard = ENV_LOCK.lock().ok(); + 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, + ) + .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()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); + } + if let Some(install_root) = original_install_root { + env::set_var("DENO_INSTALL_ROOT", install_root); + } + } + + #[test] + fn install_with_flags() { + 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, + ) + .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()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content.contains(r#""run" "--allow-read" "--allow-net" "--quiet" "--no-check" "http://localhost:4545/cli/tests/echo_server.ts" "--foobar""#)); + } else { + assert!(content.contains(r#"run --allow-read --allow-net --quiet --no-check 'http://localhost:4545/cli/tests/echo_server.ts' --foobar"#)); + } + } + + #[test] + 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("echo_server.ts"); + let local_module_url = Url::from_file_path(&local_module).unwrap(); + 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, + ) + .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()); + let content = fs::read_to_string(file_path).unwrap(); + assert!(content.contains(&local_module_url.to_string())); + } + + #[test] + fn install_force() { + 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, + ) + .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()); + + // No force. Install failed. + let no_force_result = install( + 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, + ); + 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("echo_server.ts")); + + // Force. Install success. + let force_result = install( + 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, + ); + assert!(force_result.is_ok()); + // Assert modified + let file_content_2 = fs::read_to_string(&file_path).unwrap(); + assert!(file_content_2.contains("cat.ts")); + } + + #[test] + fn install_with_config() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + let config_file_path = temp_dir.path().join("test_tsconfig.json"); + let config = "{}"; + let mut config_file = File::create(&config_file_path).unwrap(); + let result = config_file.write_all(config.as_bytes()); + assert!(result.is_ok()); + + let result = install( + Flags { + config_path: Some(config_file_path.to_string_lossy().to_string()), + ..Flags::default() + }, + "http://localhost:4545/cli/tests/cat.ts", + vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + true, + ); + eprintln!("result {:?}", result); + assert!(result.is_ok()); + + let config_file_name = "echo_test.tsconfig.json"; + + let file_path = bin_dir.join(config_file_name.to_string()); + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + assert!(content == "{}"); + } + + // TODO: enable on Windows after fixing batch escaping + #[cfg(not(windows))] + #[test] + fn install_shell_escaping() { + 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!["\"".to_string()], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + false, + ) + .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()); + let content = fs::read_to_string(file_path).unwrap(); + println!("{}", content); + if cfg!(windows) { + // TODO: see comment above this test + } else { + assert!(content.contains( + r#"run 'http://localhost:4545/cli/tests/echo_server.ts' '"'"# + )); + } + } +} |