diff options
Diffstat (limited to 'cli/installer.rs')
-rw-r--r-- | cli/installer.rs | 490 |
1 files changed, 490 insertions, 0 deletions
diff --git a/cli/installer.rs b/cli/installer.rs new file mode 100644 index 000000000..2d47f6a04 --- /dev/null +++ b/cli/installer.rs @@ -0,0 +1,490 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use crate::flags::DenoFlags; +use regex::{Regex, RegexBuilder}; +use std::env; +use std::fs; +use std::fs::File; +use std::io; +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(); +} + +fn yes_no_prompt(msg: &str) -> bool { + println!("{} [yN]", msg); + let mut buffer = String::new(); + io::stdin() + .read_line(&mut buffer) + .expect("Couldn't read from stdin"); + buffer.starts_with('y') | buffer.starts_with('Y') +} + +fn is_remote_url(module_url: &str) -> bool { + module_url.starts_with("http://") || module_url.starts_with("https://") +} + +fn validate_exec_name(exec_name: &str) -> Result<(), Error> { + if EXEC_NAME_RE.is_match(exec_name) { + Ok(()) + } else { + Err(Error::new( + ErrorKind::Other, + format!("Invalid module name: {}", exec_name), + )) + } +} + +#[cfg(windows)] +fn generate_executable_file( + file_path: PathBuf, + commands: Vec<String>, +) -> Result<(), Error> { + // On Windows if user is using Powershell .cmd extension is need to run the + // installed module. + // Generate batch script to satisfy that. + let template_header = "This executable is generated by Deno. Please don't modify it unless you know what it means."; + let commands: Vec<String> = + commands.iter().map(|c| format!("\"{}\"", c)).collect(); + // TODO: remove conditionals in generated scripts + let template = format!( + r#"% {} % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" {} %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + {} %* +) +"#, + template_header, + commands[1..].join(" "), + commands.join(" ") + ); + let file_path = file_path.with_extension(".cmd"); + let mut file = File::create(&file_path)?; + file.write_all(template.as_bytes())?; + Ok(()) +} + +#[cfg(not(windows))] +fn generate_executable_file( + file_path: PathBuf, + commands: Vec<String>, +) -> Result<(), Error> { + // On Windows if user is using Powershell .cmd extension is need to run the + // installed module. + // Generate batch script to satisfy that. + let template_header = "This executable is generated by Deno. Please don't modify it unless you know what it means."; + let commands: Vec<String> = + commands.iter().map(|c| format!("\"{}\"", c)).collect(); + + // TODO: remove conditionals in generated scripts + let template = format!( + r#"#!/bin/sh +# {} +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" {} "$@" + ret=$? +else + {} "$@" + ret=$? +fi +exit $ret +"#, + template_header, + commands[1..].join(" "), + commands.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_dir() -> Result<PathBuf, Error> { + // In Windows's Powershell $HOME environmental variable maybe null + // if so use $USERPROFILE instead. + let home = env::var("HOME") + .map(String::into) + .unwrap_or_else(|_| "".to_string()); + let user_profile = env::var("USERPROFILE") + .map(String::into) + .unwrap_or_else(|_| "".to_string()); + + if home.is_empty() && user_profile.is_empty() { + return Err(Error::new(ErrorKind::Other, "$HOME is not defined")); + } + + let home_path = if !home.is_empty() { home } else { user_profile }; + + let mut home_path = PathBuf::from(home_path); + home_path.push(".deno"); + home_path.push("bin"); + Ok(home_path) +} + +pub fn install( + flags: DenoFlags, + installation_dir: Option<String>, + exec_name: &str, + module_url: &str, + args: Vec<String>, +) -> Result<(), Error> { + let installation_dir = if let Some(dir) = installation_dir { + PathBuf::from(dir).canonicalize()? + } else { + get_installer_dir()? + }; + + // ensure directory exists + if let Ok(metadata) = fs::metadata(&installation_dir) { + if !metadata.is_dir() { + return Err(Error::new( + ErrorKind::Other, + "Insallation 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") + }; + + validate_exec_name(exec_name)?; + let file_path = installation_dir.join(exec_name); + + if file_path.exists() { + let msg = format!( + "⚠️ {} is already installed, do you want to overwrite it?", + exec_name + ); + if !yes_no_prompt(&msg) { + return Ok(()); + }; + }; + + let mut executable_args = vec!["deno".to_string(), "run".to_string()]; + executable_args.extend_from_slice(&flags.to_permission_args()); + executable_args.push(module_url.to_string()); + executable_args.extend_from_slice(&args); + + generate_executable_file(file_path.to_owned(), executable_args)?; + + println!("✅ Successfully installed {}", exec_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 tempfile::TempDir; + + #[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("file:///dev/deno_std/http/file_server.ts")); + assert!(!is_remote_url("./dev/deno_std/http/file_server.ts")); + } + + #[test] + fn install_basic() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let temp_dir_str = temp_dir.path().to_string_lossy().to_string(); + let original_home = env::var_os("HOME"); + let original_user_profile = env::var_os("HOME"); + env::set_var("HOME", &temp_dir_str); + env::set_var("USERPROFILE", &temp_dir_str); + + install( + DenoFlags::default(), + None, + "echo_test", + "http://localhost:4545/tests/echo_server.ts", + vec![], + ) + .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(); + + let expected_content = if cfg!(windows) { + r#"% This executable is generated by Deno. Please don't modify it unless you know what it means. % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" "run" "http://localhost:4545/tests/echo_server.ts" %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + "deno" "run" "http://localhost:4545/tests/echo_server.ts" %* +) +"# + } else { + r#"#!/bin/sh +# This executable is generated by Deno. Please don't modify it unless you know what it means. +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" "run" "http://localhost:4545/tests/echo_server.ts" "$@" + ret=$? +else + "deno" "run" "http://localhost:4545/tests/echo_server.ts" "$@" + ret=$? +fi +exit $ret +"# + }; + + assert_eq!(content, expected_content.to_string()); + + 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); + } + } + + #[test] + fn install_custom_dir() { + let temp_dir = TempDir::new().expect("tempdir fail"); + + install( + DenoFlags::default(), + Some(temp_dir.path().to_string_lossy().to_string()), + "echo_test", + "http://localhost:4545/tests/echo_server.ts", + vec![], + ) + .expect("Install failed"); + + let mut file_path = temp_dir.path().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(); + + let expected_content = if cfg!(windows) { + r#"% This executable is generated by Deno. Please don't modify it unless you know what it means. % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" "run" "http://localhost:4545/tests/echo_server.ts" %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + "deno" "run" "http://localhost:4545/tests/echo_server.ts" %* +) +"# + } else { + r#"#!/bin/sh +# This executable is generated by Deno. Please don't modify it unless you know what it means. +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" "run" "http://localhost:4545/tests/echo_server.ts" "$@" + ret=$? +else + "deno" "run" "http://localhost:4545/tests/echo_server.ts" "$@" + ret=$? +fi +exit $ret +"# + }; + + assert_eq!(content, expected_content.to_string()); + } + + #[test] + fn install_with_flags() { + let temp_dir = TempDir::new().expect("tempdir fail"); + + install( + DenoFlags { + allow_net: true, + allow_read: true, + ..DenoFlags::default() + }, + Some(temp_dir.path().to_string_lossy().to_string()), + "echo_test", + "http://localhost:4545/tests/echo_server.ts", + vec!["--foobar".to_string()], + ) + .expect("Install failed"); + + let mut file_path = temp_dir.path().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(); + + let expected_content = if cfg!(windows) { + r#"% This executable is generated by Deno. Please don't modify it unless you know what it means. % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" "run" "--allow-read" "--allow-net" "http://localhost:4545/tests/echo_server.ts" "--foobar" %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + "deno" "run" "--allow-read" "--allow-net" "http://localhost:4545/tests/echo_server.ts" "--foobar" %* +) +"# + } else { + r#"#!/bin/sh +# This executable is generated by Deno. Please don't modify it unless you know what it means. +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" "run" "--allow-read" "--allow-net" "http://localhost:4545/tests/echo_server.ts" "--foobar" "$@" + ret=$? +else + "deno" "run" "--allow-read" "--allow-net" "http://localhost:4545/tests/echo_server.ts" "--foobar" "$@" + ret=$? +fi +exit $ret +"# + }; + + assert_eq!(content, expected_content.to_string()); + } + + #[test] + fn install_local_module() { + let temp_dir = TempDir::new().expect("tempdir fail"); + 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( + DenoFlags::default(), + Some(temp_dir.path().to_string_lossy().to_string()), + "echo_test", + &local_module_str, + vec![], + ) + .expect("Install failed"); + + let mut file_path = temp_dir.path().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(); + + let expected_content = if cfg!(windows) { + format!( + r#"% This executable is generated by Deno. Please don't modify it unless you know what it means. % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" "run" "{}" %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + "deno" "run" "{}" %* +) +"#, + local_module_url.to_string(), + local_module_url.to_string() + ) + } else { + format!( + r#"#!/bin/sh +# This executable is generated by Deno. Please don't modify it unless you know what it means. +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" "run" "{}" "$@" + ret=$? +else + "deno" "run" "{}" "$@" + ret=$? +fi +exit $ret +"#, + local_module_url.to_string(), + local_module_url.to_string() + ) + }; + + assert_eq!(content, expected_content); + } +} |