// 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, ) -> 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 = 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, ) -> 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 = 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 { // 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, exec_name: &str, module_url: &str, args: Vec, ) -> 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/cli/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/cli/tests/echo_server.ts" %* ) ELSE ( @SETLOCAL @SET PATHEXT=%PATHEXT:;.TS;=;% "deno" "run" "http://localhost:4545/cli/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/cli/tests/echo_server.ts" "$@" ret=$? else "deno" "run" "http://localhost:4545/cli/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/cli/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/cli/tests/echo_server.ts" %* ) ELSE ( @SETLOCAL @SET PATHEXT=%PATHEXT:;.TS;=;% "deno" "run" "http://localhost:4545/cli/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/cli/tests/echo_server.ts" "$@" ret=$? else "deno" "run" "http://localhost:4545/cli/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/cli/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/cli/tests/echo_server.ts" "--foobar" %* ) ELSE ( @SETLOCAL @SET PATHEXT=%PATHEXT:;.TS;=;% "deno" "run" "--allow-read" "--allow-net" "http://localhost:4545/cli/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/cli/tests/echo_server.ts" "--foobar" "$@" ret=$? else "deno" "run" "--allow-read" "--allow-net" "http://localhost:4545/cli/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); } }