From de5c099b47bd1d2e528f1a10179f130a02f26f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 31 Jan 2020 00:42:39 +0100 Subject: [PATCH] feat: deno install in Rust (#3806) //std/installer couldn't be removed due to bug, but it's now deprecated. --- cli/flags.rs | 317 +++++++++++++++----------- cli/installer.rs | 490 ++++++++++++++++++++++++++++++++++++++++ cli/lib.rs | 28 +++ std/examples/README.md | 8 +- std/examples/catj.ts | 2 +- std/http/file_server.ts | 2 +- std/installer/README.md | 90 +------- std/manual.md | 20 +- 8 files changed, 721 insertions(+), 236 deletions(-) create mode 100644 cli/installer.rs diff --git a/cli/flags.rs b/cli/flags.rs index d7a95d79..1eda8765 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -28,8 +28,6 @@ macro_rules! std_url { }; } -/// Used for `deno install...` subcommand -const INSTALLER_URL: &str = std_url!("installer/mod.ts"); /// Used for `deno test...` subcommand const TEST_RUNNER_URL: &str = std_url!("testing/runner.ts"); @@ -45,7 +43,12 @@ pub enum DenoSubcommand { }, Help, Info, - Install, + Install { + dir: Option, + exe_name: String, + module_url: String, + args: Vec, + }, Repl, Run, Types, @@ -92,6 +95,63 @@ pub struct DenoFlags { pub lock_write: bool, } +impl DenoFlags { + /// Return list of permission arguments that are equivalent + /// to the ones used to create `self`. + pub fn to_permission_args(&self) -> Vec { + let mut args = vec![]; + + if !self.read_whitelist.is_empty() { + let s = format!("--allow-read={}", self.read_whitelist.join(",")); + args.push(s); + } + + if self.allow_read { + args.push("--allow-read".to_string()); + } + + if !self.write_whitelist.is_empty() { + let s = format!("--allow-write={}", self.write_whitelist.join(",")); + args.push(s); + } + + if self.allow_write { + args.push("--allow-write".to_string()); + } + + if !self.net_whitelist.is_empty() { + let s = format!("--allow-net={}", self.net_whitelist.join(",")); + args.push(s); + } + + if self.allow_net { + args.push("--allow-net".to_string()); + } + + if self.allow_env { + args.push("--allow-env".to_string()); + } + + if self.allow_run { + args.push("--allow-run".to_string()); + } + + if self.allow_plugin { + args.push("--allow-plugin".to_string()); + } + + if self.allow_hrtime { + args.push("--allow-hrtime".to_string()); + } + + if self.allow_env { + args.push("--allow-env".to_string()); + } + + args + } +} + static ENV_VARIABLES_HELP: &str = "ENVIRONMENT VARIABLES: DENO_DIR Set deno's base directory NO_COLOR Set to disable color @@ -247,30 +307,32 @@ fn fmt_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { } fn install_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { - flags.subcommand = DenoSubcommand::Run; - flags.allow_read = true; - flags.allow_write = true; - flags.allow_net = true; - flags.allow_env = true; - flags.allow_run = true; - flags.argv.push(INSTALLER_URL.to_string()); + permission_args_parse(flags, matches); - if matches.is_present("dir") { + let dir = if matches.is_present("dir") { let install_dir = matches.value_of("dir").unwrap(); - flags.argv.push("--dir".to_string()); - println!("dir {}", install_dir); - flags.argv.push(install_dir.to_string()); + Some(install_dir.to_string()) } else { - println!("no dir"); - } + None + }; - let exe_name = matches.value_of("exe_name").unwrap(); - flags.argv.push(String::from(exe_name)); + let exe_name = matches.value_of("exe_name").unwrap().to_string(); + let cmd_values = matches.values_of("cmd").unwrap(); + let mut cmd_args = vec![]; - let cmd = matches.values_of("cmd").unwrap(); - for arg in cmd { - flags.argv.push(String::from(arg)); + for value in cmd_values { + cmd_args.push(value.to_string()); } + + let module_url = cmd_args[0].to_string(); + let args = cmd_args[1..].to_vec(); + + flags.subcommand = DenoSubcommand::Install { + dir, + exe_name, + module_url, + args, + }; } fn bundle_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { @@ -375,63 +437,8 @@ fn run_test_args_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { config_arg_parse(flags, matches); v8_flags_arg_parse(flags, matches); no_remote_arg_parse(flags, matches); + permission_args_parse(flags, matches); - if matches.is_present("allow-read") { - if matches.value_of("allow-read").is_some() { - let read_wl = matches.values_of("allow-read").unwrap(); - let raw_read_whitelist: Vec = - read_wl.map(std::string::ToString::to_string).collect(); - flags.read_whitelist = resolve_fs_whitelist(&raw_read_whitelist); - debug!("read whitelist: {:#?}", &flags.read_whitelist); - } else { - flags.allow_read = true; - } - } - if matches.is_present("allow-write") { - if matches.value_of("allow-write").is_some() { - let write_wl = matches.values_of("allow-write").unwrap(); - let raw_write_whitelist: Vec = - write_wl.map(std::string::ToString::to_string).collect(); - flags.write_whitelist = - resolve_fs_whitelist(raw_write_whitelist.as_slice()); - debug!("write whitelist: {:#?}", &flags.write_whitelist); - } else { - flags.allow_write = true; - } - } - if matches.is_present("allow-net") { - if matches.value_of("allow-net").is_some() { - let net_wl = matches.values_of("allow-net").unwrap(); - let raw_net_whitelist = - net_wl.map(std::string::ToString::to_string).collect(); - flags.net_whitelist = resolve_hosts(raw_net_whitelist); - debug!("net whitelist: {:#?}", &flags.net_whitelist); - } else { - flags.allow_net = true; - } - } - if matches.is_present("allow-env") { - flags.allow_env = true; - } - if matches.is_present("allow-run") { - flags.allow_run = true; - } - if matches.is_present("allow-plugin") { - flags.allow_plugin = true; - } - if matches.is_present("allow-hrtime") { - flags.allow_hrtime = true; - } - if matches.is_present("allow-all") { - flags.allow_read = true; - flags.allow_env = true; - flags.allow_net = true; - flags.allow_run = true; - flags.allow_read = true; - flags.allow_write = true; - flags.allow_plugin = true; - flags.allow_hrtime = true; - } if matches.is_present("cached-only") { flags.cached_only = true; } @@ -542,7 +549,7 @@ fn repl_subcommand<'a, 'b>() -> App<'a, 'b> { } fn install_subcommand<'a, 'b>() -> App<'a, 'b> { - SubCommand::with_name("install") + permission_args(SubCommand::with_name("install")) .setting(AppSettings::TrailingVarArg) .arg( Arg::with_name("dir") @@ -566,13 +573,13 @@ fn install_subcommand<'a, 'b>() -> App<'a, 'b> { "Installs a script as executable. The default installation directory is $HOME/.deno/bin and it must be added to the path manually. - deno install file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read + deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts deno install colors https://deno.land/std/examples/colors.ts To change installation directory use -d/--dir flag - deno install -d /usr/local/bin file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read") + deno install --allow-net --allow-read -d /usr/local/bin file_server https://deno.land/std/http/file_server.ts") } fn bundle_subcommand<'a, 'b>() -> App<'a, 'b> { @@ -681,15 +688,8 @@ Once cached, static imports no longer send network requests ) } -fn run_test_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { +fn permission_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { app - .arg(importmap_arg()) - .arg(reload_arg()) - .arg(config_arg()) - .arg(lock_arg()) - .arg(lock_write_arg()) - .arg(no_remote_arg()) - .arg(v8_flags_arg()) .arg( Arg::with_name("allow-read") .long("allow-read") @@ -743,6 +743,17 @@ fn run_test_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .long("allow-all") .help("Allow all permissions"), ) +} + +fn run_test_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + permission_args(app) + .arg(importmap_arg()) + .arg(reload_arg()) + .arg(config_arg()) + .arg(lock_arg()) + .arg(lock_write_arg()) + .arg(no_remote_arg()) + .arg(v8_flags_arg()) .arg( Arg::with_name("cached-only") .long("cached-only") @@ -952,6 +963,65 @@ fn no_remote_arg_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { } } +fn permission_args_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { + if matches.is_present("allow-read") { + if matches.value_of("allow-read").is_some() { + let read_wl = matches.values_of("allow-read").unwrap(); + let raw_read_whitelist: Vec = + read_wl.map(std::string::ToString::to_string).collect(); + flags.read_whitelist = resolve_fs_whitelist(&raw_read_whitelist); + debug!("read whitelist: {:#?}", &flags.read_whitelist); + } else { + flags.allow_read = true; + } + } + if matches.is_present("allow-write") { + if matches.value_of("allow-write").is_some() { + let write_wl = matches.values_of("allow-write").unwrap(); + let raw_write_whitelist: Vec = + write_wl.map(std::string::ToString::to_string).collect(); + flags.write_whitelist = + resolve_fs_whitelist(raw_write_whitelist.as_slice()); + debug!("write whitelist: {:#?}", &flags.write_whitelist); + } else { + flags.allow_write = true; + } + } + if matches.is_present("allow-net") { + if matches.value_of("allow-net").is_some() { + let net_wl = matches.values_of("allow-net").unwrap(); + let raw_net_whitelist = + net_wl.map(std::string::ToString::to_string).collect(); + flags.net_whitelist = resolve_hosts(raw_net_whitelist); + debug!("net whitelist: {:#?}", &flags.net_whitelist); + } else { + flags.allow_net = true; + } + } + if matches.is_present("allow-env") { + flags.allow_env = true; + } + if matches.is_present("allow-run") { + flags.allow_run = true; + } + if matches.is_present("allow-plugin") { + flags.allow_plugin = true; + } + if matches.is_present("allow-hrtime") { + flags.allow_hrtime = true; + } + if matches.is_present("allow-all") { + flags.allow_read = true; + flags.allow_env = true; + flags.allow_net = true; + flags.allow_run = true; + flags.allow_read = true; + flags.allow_write = true; + flags.allow_plugin = true; + flags.allow_hrtime = true; + } +} + // TODO(ry) move this to utility module and add test. /// Strips fragment part of URL. Panics on bad URL. pub fn resolve_urls(urls: Vec) -> Vec { @@ -1662,18 +1732,13 @@ mod tests { assert_eq!( r.unwrap(), DenoFlags { - subcommand: DenoSubcommand::Run, - argv: svec![ - "deno", - INSTALLER_URL, - "deno_colors", - "https://deno.land/std/examples/colors.ts" - ], - allow_write: true, - allow_net: true, - allow_read: true, - allow_env: true, - allow_run: true, + subcommand: DenoSubcommand::Install { + dir: None, + exe_name: "deno_colors".to_string(), + module_url: "https://deno.land/std/examples/colors.ts".to_string(), + args: vec![], + }, + argv: svec!["deno"], ..DenoFlags::default() } ); @@ -1684,28 +1749,23 @@ mod tests { let r = flags_from_vec_safe(svec![ "deno", "install", - "file_server", - "https://deno.land/std/http/file_server.ts", "--allow-net", - "--allow-read" + "--allow-read", + "file_server", + "https://deno.land/std/http/file_server.ts" ]); assert_eq!( r.unwrap(), DenoFlags { - subcommand: DenoSubcommand::Run, - argv: svec![ - "deno", - INSTALLER_URL, - "file_server", - "https://deno.land/std/http/file_server.ts", - "--allow-net", - "--allow-read" - ], - allow_write: true, + subcommand: DenoSubcommand::Install { + dir: None, + exe_name: "file_server".to_string(), + module_url: "https://deno.land/std/http/file_server.ts".to_string(), + args: vec![], + }, + argv: svec!["deno"], allow_net: true, allow_read: true, - allow_env: true, - allow_run: true, ..DenoFlags::default() } ); @@ -1718,30 +1778,25 @@ mod tests { "install", "-d", "/usr/local/bin", + "--allow-net", + "--allow-read", "file_server", "https://deno.land/std/http/file_server.ts", - "--allow-net", - "--allow-read" + "arg1", + "arg2" ]); assert_eq!( r.unwrap(), DenoFlags { - subcommand: DenoSubcommand::Run, - argv: svec![ - "deno", - INSTALLER_URL, - "--dir", - "/usr/local/bin", - "file_server", - "https://deno.land/std/http/file_server.ts", - "--allow-net", - "--allow-read" - ], - allow_write: true, + subcommand: DenoSubcommand::Install { + dir: Some("/usr/local/bin".to_string()), + exe_name: "file_server".to_string(), + module_url: "https://deno.land/std/http/file_server.ts".to_string(), + args: svec!["arg1", "arg2"], + }, + argv: svec!["deno"], allow_net: true, allow_read: true, - allow_env: true, - allow_run: true, ..DenoFlags::default() } ); diff --git a/cli/installer.rs b/cli/installer.rs new file mode 100644 index 00000000..2d47f6a0 --- /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, +) -> 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/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); + } +} diff --git a/cli/lib.rs b/cli/lib.rs index 655bd241..3a894e0d 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -35,6 +35,7 @@ mod global_state; mod global_timer; mod http_util; mod import_map; +mod installer; mod js; mod lockfile; mod metrics; @@ -280,6 +281,27 @@ async fn info_command(flags: DenoFlags) { js_check(result); } +async fn install_command( + flags: DenoFlags, + dir: Option, + exe_name: String, + module_url: String, + args: Vec, +) { + // Firstly fetch and compile module, this + // ensures the module exists. + let mut fetch_flags = flags.clone(); + fetch_flags.argv.push(module_url.to_string()); + fetch_flags.reload = true; + fetch_command(fetch_flags).await; + + let install_result = + installer::install(flags, dir, &exe_name, &module_url, args); + if let Err(e) = install_result { + print_msg_and_exit(&e.to_string()); + } +} + async fn fetch_command(flags: DenoFlags) { let (mut worker, state) = create_worker_and_state(flags); @@ -428,6 +450,12 @@ pub async fn main() { DenoSubcommand::Fetch => fetch_command(flags).await, DenoSubcommand::Format { check, files } => fmt_command(files, check).await, DenoSubcommand::Info => info_command(flags).await, + DenoSubcommand::Install { + dir, + exe_name, + module_url, + args, + } => install_command(flags, dir, exe_name, module_url, args).await, DenoSubcommand::Repl => run_repl(flags).await, DenoSubcommand::Run => run_script(flags).await, DenoSubcommand::Types => types_command(), diff --git a/std/examples/README.md b/std/examples/README.md index e2f6b2a0..351c4a6b 100644 --- a/std/examples/README.md +++ b/std/examples/README.md @@ -16,13 +16,13 @@ deno https://deno.land/std/examples/echo_server.ts --allow-net Or ```shell -deno install echo_server https://deno.land/std/examples/echo_server.ts --allow-net +deno install --allow-net echo_server https://deno.land/std/examples/echo_server.ts ``` ### cat - print file to standard output ```shell -deno install deno_cat https://deno.land/std/examples/cat.ts --allow-read +deno install --allow-read deno_cat https://deno.land/std/examples/cat.ts deno_cat file.txt ``` @@ -31,7 +31,7 @@ deno_cat file.txt A very useful command by Soheil Rashidi ported to Deno. ```shell -deno install catj https://deno.land/std/examples/catj.ts --allow-read +deno install --allow-read catj https://deno.land/std/examples/catj.ts catj example.json catj file1.json file2.json echo example.json | catj - @@ -47,7 +47,7 @@ deno --allow-net=deno.land https://deno.land/std/examples/curl.ts -- https://den ``` export GIST_TOKEN=ABC # Generate at https://github.com/settings/tokens -deno install gist https://deno.land/std/examples/gist.ts --allow-net --allow-env +deno install --allow-net --allow-env gist https://deno.land/std/examples/gist.ts gist --title "Example gist 1" script.ts gist --t "Example gist 2" script2.ts ``` diff --git a/std/examples/catj.ts b/std/examples/catj.ts index ef6fdb02..7951eeeb 100644 --- a/std/examples/catj.ts +++ b/std/examples/catj.ts @@ -4,7 +4,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Install using `deno install` -// $ deno install catj https://deno.land/std/examples/catj.ts --allow-read +// $ deno install --allow-read catj https://deno.land/std/examples/catj.ts /* eslint-disable @typescript-eslint/no-use-before-define */ import { parse } from "../flags/mod.ts"; diff --git a/std/http/file_server.ts b/std/http/file_server.ts index 4510023f..6ec10cd6 100755 --- a/std/http/file_server.ts +++ b/std/http/file_server.ts @@ -48,7 +48,7 @@ if (serverArgs.h || serverArgs.help) { Serves a local directory in HTTP. INSTALL: - deno install file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read + deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts USAGE: file_server [path] [options] diff --git a/std/installer/README.md b/std/installer/README.md index f78c86e9..2fc7654e 100644 --- a/std/installer/README.md +++ b/std/installer/README.md @@ -1,89 +1 @@ -# deno_installer - -Install remote or local script as executables. - -## Installation - -`installer` can be installed using itself: - -```sh -deno -A https://deno.land/std/installer/mod.ts deno_installer https://deno.land/std/installer/mod.ts -A -``` - -## Usage - -Install script - -```sh -# remote script -$ deno_installer file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read -> [1/1] Compiling https://deno.land/std/http/file_server.ts -> -> ✅ Successfully installed file_server. -> ~/.deno/bin/file_server - -# local script -$ deno_installer file_server ./deno_std/http/file_server.ts --allow-net --allow-read -> [1/1] Compiling file:///dev/deno_std/http/file_server.ts -> -> ✅ Successfully installed file_server. -> ~/.deno/bin/file_server -``` - -Run installed script: - -```sh -$ file_server -HTTP server listening on http://0.0.0.0:4500/ -``` - -## Custom installation directory - -By default installer uses `~/.deno/bin` to store installed scripts so make sure -it's in your `$PATH`. - -``` -echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc # change this to your shell -``` - -If you prefer to change installation directory use `-d` or `--dir` flag. - -``` -$ deno_installer --dir /usr/local/bin file_server ./deno_std/http/file_server.ts --allow-net --allow-read -> [1/1] Compiling file:///dev/deno_std/http/file_server.ts -> -> ✅ Successfully installed file_server. -> /usr/local/bin/file_server -``` - -## Update installed script - -```sh -$ deno_installer file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read -> ⚠️ file_server is already installed, do you want to overwrite it? [yN] -> y -> -> [1/1] Compiling file:///dev/deno_std/http/file_server.ts -> -> ✅ Successfully installed file_server. -``` - -Show help - -```sh -$ deno_installer --help -> deno installer - Install remote or local script as executables. - -USAGE: - deno -A https://deno.land/std/installer/mod.ts [OPTIONS] EXE_NAME SCRIPT_URL [FLAGS...] - -ARGS: - EXE_NAME Name for executable - SCRIPT_URL Local or remote URL of script to install - [FLAGS...] List of flags for script, both Deno permission and script specific - flag can be used. - -OPTIONS: - -d, --dir Installation directory path (defaults to ~/.deno/bin) -``` +WARNING: This code is deprecated and std/installer will be removed soon. diff --git a/std/manual.md b/std/manual.md index d97e4270..9ccd0a4f 100644 --- a/std/manual.md +++ b/std/manual.md @@ -285,7 +285,7 @@ await Deno.remove("request.log"); This one serves a local directory in HTTP. ```bash -deno install file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read +deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts ``` Run it: @@ -821,8 +821,8 @@ Or you could import it into another ES module to consume: Deno provides ability to easily install and distribute executable code via `deno install` command. -`deno install [EXE_NAME] [URL] [FLAGS...]` will install script available at -`URL` with name `EXE_NAME`. +`deno install [FLAGS...] [EXE_NAME] [URL] [SCRIPT_ARGS...]` will install script +available at `URL` with name `EXE_NAME`. This command is a thin wrapper that creates executable shell scripts which invoke `deno` with specified permissions and CLI flags. @@ -830,15 +830,16 @@ invoke `deno` with specified permissions and CLI flags. Example: ```shell -$ deno install file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read +$ deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts [1/1] Compiling https://deno.land/std/http/file_server.ts ✅ Successfully installed file_server. /Users/deno/.deno/bin/file_server ``` -By default scripts are installed at `$HOME/.deno/bin` and that directory must be -added to the path manually. +By default scripts are installed at `$HOME/.deno/bin` or +`$USERPROFILE/.deno/bin` and one of that directories must be added to the path +manually. ```shell $ echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc @@ -847,17 +848,16 @@ $ echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc Installation directory can be changed using `-d/--dir` flag: ```shell -$ deno install --dir /usr/local/bin file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read +$ deno install --allow-net --allow-read --dir /usr/local/bin file_server https://deno.land/std/http/file_server.ts ``` When installing a script you can specify permissions that will be used to run -the script. They are placed after the script URL and can be mixed with any -additional CLI flags you want to pass to the script. +the script. Example: ```shell -$ deno install file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read 8080 +$ deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts 8080 ``` Above command creates an executable called `file_server` that runs with write -- GitLab