deno/cli/installer.rs

491 lines
13 KiB
Rust
Raw Normal View History

// 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",
2020-02-02 21:55:22 +00:00
"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" (
2020-02-02 21:55:22 +00:00
"%~dp0\deno.exe" "run" "http://localhost:4545/cli/tests/echo_server.ts" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.TS;=;%
2020-02-02 21:55:22 +00:00
"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
2020-02-02 21:55:22 +00:00
"$basedir/deno" "run" "http://localhost:4545/cli/tests/echo_server.ts" "$@"
ret=$?
else
2020-02-02 21:55:22 +00:00
"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",
2020-02-02 21:55:22 +00:00
"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" (
2020-02-02 21:55:22 +00:00
"%~dp0\deno.exe" "run" "http://localhost:4545/cli/tests/echo_server.ts" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.TS;=;%
2020-02-02 21:55:22 +00:00
"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
2020-02-02 21:55:22 +00:00
"$basedir/deno" "run" "http://localhost:4545/cli/tests/echo_server.ts" "$@"
ret=$?
else
2020-02-02 21:55:22 +00:00
"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",
2020-02-02 21:55:22 +00:00
"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" (
2020-02-02 21:55:22 +00:00
"%~dp0\deno.exe" "run" "--allow-read" "--allow-net" "http://localhost:4545/cli/tests/echo_server.ts" "--foobar" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.TS;=;%
2020-02-02 21:55:22 +00:00
"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
2020-02-02 21:55:22 +00:00
"$basedir/deno" "run" "--allow-read" "--allow-net" "http://localhost:4545/cli/tests/echo_server.ts" "--foobar" "$@"
ret=$?
else
2020-02-02 21:55:22 +00:00
"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);
}
}