deno/cli/installer.rs
2020-02-02 16:55:22 -05:00

490 lines
13 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::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/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);
}
}