summaryrefslogtreecommitdiff
path: root/cli/installer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/installer.rs')
-rw-r--r--cli/installer.rs490
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);
+ }
+}