diff --git a/src/commands/env.rs b/src/commands/env.rs index c19198b..277e496 100644 --- a/src/commands/env.rs +++ b/src/commands/env.rs @@ -1,11 +1,11 @@ use super::command::Command; use crate::config::FnmConfig; use crate::fs::symlink_dir; -use crate::outln; use crate::path_ext::PathExt; use crate::shell::{infer_shell, Shell, AVAILABLE_SHELLS}; +use crate::{outln, shims}; use colored::Colorize; -use snafu::{OptionExt, Snafu}; +use snafu::{OptionExt, ResultExt, Snafu}; use std::fmt::Debug; use structopt::StructOpt; @@ -21,6 +21,12 @@ pub struct Env { /// Print the script to change Node versions every directory change #[structopt(long)] use_on_cd: bool, + + /// Adds `node` shims to your PATH environment variable + /// to allow you to use `node` commands in your shell + /// without rehashing. + #[structopt(long)] + with_shims: bool, } fn generate_symlink_path() -> String { @@ -61,7 +67,9 @@ impl Command for Env { let shell: Box = self.shell.or_else(&infer_shell).context(CantInferShell)?; let multishell_path = make_symlink(config); - let binary_path = if cfg!(windows) { + let binary_path = if self.with_shims { + shims::store_shim(config).context(CantCreateShims)? + } else if cfg!(windows) { multishell_path.clone() } else { multishell_path.join("bin") @@ -107,6 +115,8 @@ pub enum Error { shells_as_string() ))] CantInferShell, + #[snafu(display("Can't create Node.js shims: {}", source))] + CantCreateShims { source: std::io::Error }, } fn shells_as_string() -> String { diff --git a/src/commands/exec.rs b/src/commands/exec.rs index f2e8215..cfb5246 100644 --- a/src/commands/exec.rs +++ b/src/commands/exec.rs @@ -3,10 +3,12 @@ use crate::choose_version_for_user_input::{ choose_version_for_user_input, Error as UserInputError, }; use crate::config::FnmConfig; +use crate::current_version::{self, current_version}; use crate::outln; use crate::user_version::UserVersion; use crate::user_version_reader::UserVersionReader; use colored::Colorize; +use log::debug; use snafu::{OptionExt, ResultExt, Snafu}; use std::process::{Command, Stdio}; use structopt::StructOpt; @@ -20,6 +22,8 @@ pub struct Exec { /// Deprecated. This is the default now. #[structopt(long = "using-file", hidden = true)] using_file: bool, + #[structopt(long = "using-current", hidden = true, conflicts_with = "using")] + using_current: bool, /// The command to run arguments: Vec, } @@ -40,14 +44,21 @@ impl Cmd for Exec { let (binary, arguments) = self.arguments.split_first().context(NoBinaryProvided)?; - let version = self - .version - .unwrap_or_else(|| { - let current_dir = std::env::current_dir().unwrap(); - UserVersionReader::Path(current_dir) - }) - .into_user_version() - .context(CantInferVersion)?; + let version = if self.using_current { + let version = current_version(config) + .context(CantGetCurrentVersion)? + .context(NoCurrentVersion)?; + UserVersion::Full(version) + } else { + self.version + .unwrap_or_else(|| { + debug!("no version provided, falling back to current directory"); + let current_dir = std::env::current_dir().unwrap(); + UserVersionReader::Path(current_dir) + }) + .into_user_version() + .context(CantInferVersion)? + }; let applicable_version = choose_version_for_user_input(&version, config) .context(ApplicableVersionError)? @@ -59,6 +70,8 @@ impl Cmd for Exec { #[cfg(unix)] let bin_path = applicable_version.path().join("bin"); + debug!("Using Node.js from {}", bin_path.display()); + let path_env = { let paths_env = std::env::var_os("PATH").context(CantReadPathVariable)?; let mut paths: Vec<_> = std::env::split_paths(&paths_env).collect(); @@ -105,6 +118,12 @@ pub enum Error { "Can't read exit code from process.\nMaybe the process was killed using a signal?" ))] CantReadProcessExitCode, + #[snafu(display("{}", source))] + CantGetCurrentVersion { + source: current_version::Error, + }, + #[snafu(display("No current version. Please run `fnm use ` and retry."))] + NoCurrentVersion, #[snafu(display("command not provided. Please provide a command to run as an argument, like {} or {}.\n{} {}", "node".italic(), "bash".italic(), "example:".yellow().bold(), "fnm exec --using=12 node --version".italic().yellow()))] NoBinaryProvided, } diff --git a/src/main.rs b/src/main.rs index 8efefb7..53003e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod lts; mod path_ext; mod remote_node_index; mod shell; +mod shims; mod system_info; mod system_version; mod user_version; diff --git a/src/shims/mod.rs b/src/shims/mod.rs new file mode 100644 index 0000000..9b92933 --- /dev/null +++ b/src/shims/mod.rs @@ -0,0 +1,62 @@ +use crate::config::FnmConfig; +use log::info; +use std::io::Write; +use std::path::PathBuf; + +#[cfg(not(windows))] +const SHIM_CONTENT: &[u8] = include_bytes!("./unix.sh"); + +#[cfg(windows)] +const SHIM_CONTENT: &[u8] = include_bytes!("./windows.cmd"); + +pub type Error = std::io::Error; + +/// Creates shims for a path and returns the shim directory +pub fn store_shim(config: &FnmConfig) -> Result { + let dir = shims_dir(config)?; + let executable_path = dir.join(if cfg!(not(windows)) { + "node" + } else { + "node.cmd" + }); + + if executable_path.exists() { + return Ok(dir); + } + + info!("Creating a shim at {}", executable_path.display()); + let mut file = std::fs::File::create(&executable_path)?; + + #[cfg(not(windows))] + { + use std::os::unix::prelude::PermissionsExt; + info!("Setting file permissions to 777"); + let mut perm = file.metadata()?.permissions(); + perm.set_mode(0o777); + file.set_permissions(perm)?; + }; + + file.write_all(SHIM_CONTENT)?; + + Ok(dir) +} + +fn shims_dir(config: &FnmConfig) -> Result { + let path = config.base_dir_with_default().join("shims"); + std::fs::create_dir_all(&path)?; + Ok(path) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn test_shim() { + let base_dir = tempdir().unwrap(); + let config = FnmConfig::default().with_base_dir(Some(base_dir.into_path())); + let dir = shims_dir(&config).unwrap(); + assert!(dir.exists()); + } +} diff --git a/src/shims/unix.sh b/src/shims/unix.sh new file mode 100644 index 0000000..83c1476 --- /dev/null +++ b/src/shims/unix.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +fnm exec --using-current -- node "$@" diff --git a/src/shims/windows.cmd b/src/shims/windows.cmd new file mode 100644 index 0000000..ff1ed43 --- /dev/null +++ b/src/shims/windows.cmd @@ -0,0 +1,5 @@ +@echo off + +fnm exec --using-current -- node %* + +@echo on diff --git a/tests/feature_tests/mod.rs b/tests/feature_tests/mod.rs index f9a2796..c319dfc 100644 --- a/tests/feature_tests/mod.rs +++ b/tests/feature_tests/mod.rs @@ -13,6 +13,16 @@ mod basic { }); } +mod basic_with_shims { + test_shell!(Zsh, Bash, Fish, PowerShell, WinCmd; { + EvalFnmEnv::default() + .with_shims(true) + .then(Call::new("fnm", vec!["install", "v8.11.3"])) + .then(Call::new("fnm", vec!["use", "v8.11.3"])) + .then(test_node_version("v8.11.3")) + }); +} + mod nvmrc { test_shell!(Zsh, Bash, Fish, PowerShell, WinCmd; { EvalFnmEnv::default() diff --git a/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__Bash.snap b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__Bash.snap new file mode 100644 index 0000000..551aa6c --- /dev/null +++ b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__Bash.snap @@ -0,0 +1,15 @@ +--- +source: tests/feature_tests/mod.rs +expression: "&source.trim()" + +--- +set -e +shopt -s expand_aliases + +eval "$(fnm env --with-shims)" +fnm install v8.11.3 +fnm use v8.11.3 +if [ "$(node -v)" != "v8.11.3" ]; then + echo 'Expected Node version to be "v8.11.3", Got: '"$(node -v)" + exit 1 +fi diff --git a/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__Fish.snap b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__Fish.snap new file mode 100644 index 0000000..4ad9e4b --- /dev/null +++ b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__Fish.snap @@ -0,0 +1,12 @@ +--- +source: tests/feature_tests/mod.rs +expression: "&source.trim()" + +--- +fnm env --with-shims | source +fnm install v8.11.3 +fnm use v8.11.3 +if test (node -v) != "v8.11.3" + echo 'Expected Node version to be "v8.11.3", Got: '(node -v) + exit 1 +end diff --git a/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__PowerShell.snap b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__PowerShell.snap new file mode 100644 index 0000000..7d846ef --- /dev/null +++ b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__PowerShell.snap @@ -0,0 +1,13 @@ +--- +source: tests/feature_tests/mod.rs +expression: "&source.trim()" + +--- +$ErrorActionPreference = "Stop" +fnm env --with-shims | Out-String | Invoke-Expression +fnm install v8.11.3 +fnm use v8.11.3 +If ("$(node -v)" -ne "v8.11.3") { + Write-Output ('Expected Node version to be "v8.11.3", Got: ' + $(node -v)) + exit 1 +} diff --git a/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__WinCmd.snap b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__WinCmd.snap new file mode 100644 index 0000000..e6f667e --- /dev/null +++ b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__WinCmd.snap @@ -0,0 +1,13 @@ +--- +source: tests/feature_tests/mod.rs +expression: "&source.trim()" + +--- +FOR /f "tokens=*" %i IN ('fnm env --with-shims') DO CALL %i +fnm install v8.11.3 +fnm use v8.11.3 +node -v | findstr v8.11.3 +if %errorlevel% neq 0 ( + echo Node version does not match "v8.11.3" + exit 1 +) diff --git a/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__Zsh.snap b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__Zsh.snap new file mode 100644 index 0000000..28298e1 --- /dev/null +++ b/tests/feature_tests/snapshots/e2e__feature_tests__basic_with_shims__Zsh.snap @@ -0,0 +1,13 @@ +--- +source: tests/feature_tests/mod.rs +expression: "&source.trim()" + +--- +set -e +eval "$(fnm env --with-shims)" +fnm install v8.11.3 +fnm use v8.11.3 +if [ "$(node -v)" != "v8.11.3" ]; then + echo 'Expected Node version to be "v8.11.3", Got: '"$(node -v)" + exit 1 +fi diff --git a/tests/shellcode/eval_fnm_env.rs b/tests/shellcode/eval_fnm_env.rs index fd1d654..2adbd49 100644 --- a/tests/shellcode/eval_fnm_env.rs +++ b/tests/shellcode/eval_fnm_env.rs @@ -6,6 +6,7 @@ use std::fmt::Write; pub(crate) struct EvalFnmEnv { use_on_cd: bool, log_level: Option<&'static str>, + with_shims: bool, } impl EvalFnmEnv { @@ -16,6 +17,10 @@ impl EvalFnmEnv { pub(crate) fn log_level(self, log_level: Option<&'static str>) -> Self { Self { log_level, ..self } } + + pub(crate) fn with_shims(self, with_shims: bool) -> Self { + Self { with_shims, ..self } + } } impl std::fmt::Display for EvalFnmEnv { @@ -28,6 +33,9 @@ impl std::fmt::Display for EvalFnmEnv { if self.use_on_cd { write!(f, " --use-on-cd")?; } + if self.with_shims { + write!(f, " --with-shims")?; + } Ok(()) } }