You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
8.8 KiB
245 lines
8.8 KiB
use super::command::Command; |
|
use super::install::Install; |
|
use crate::current_version::current_version; |
|
use crate::fs; |
|
use crate::installed_versions; |
|
use crate::outln; |
|
use crate::shell; |
|
use crate::system_version; |
|
use crate::user_version::UserVersion; |
|
use crate::version::Version; |
|
use crate::version_file_strategy::VersionFileStrategy; |
|
use crate::{config::FnmConfig, user_version_reader::UserVersionReader}; |
|
use colored::Colorize; |
|
use std::path::Path; |
|
use thiserror::Error; |
|
|
|
#[derive(clap::Parser, Debug)] |
|
pub struct Use { |
|
version: Option<UserVersionReader>, |
|
/// Install the version if it isn't installed yet |
|
#[clap(long)] |
|
install_if_missing: bool, |
|
|
|
/// Don't output a message identifying the version being used |
|
/// if it will not change due to execution of this command |
|
#[clap(long)] |
|
silent_if_unchanged: bool, |
|
} |
|
|
|
impl Command for Use { |
|
type Error = Error; |
|
|
|
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> { |
|
let multishell_path = config.multishell_path().ok_or(Error::FnmEnvWasNotSourced)?; |
|
warn_if_multishell_path_not_in_path_env_var(multishell_path, config); |
|
|
|
let all_versions = installed_versions::list(config.installations_dir()) |
|
.map_err(|source| Error::VersionListingError { source })?; |
|
let requested_version = self |
|
.version |
|
.unwrap_or_else(|| { |
|
let current_dir = std::env::current_dir().unwrap(); |
|
UserVersionReader::Path(current_dir) |
|
}) |
|
.into_user_version(config) |
|
.ok_or_else(|| match config.version_file_strategy() { |
|
VersionFileStrategy::Local => InferVersionError::Local, |
|
VersionFileStrategy::Recursive => InferVersionError::Recursive, |
|
}) |
|
.map_err(|source| Error::CantInferVersion { source })?; |
|
|
|
let (message, version_path) = if let UserVersion::Full(Version::Bypassed) = |
|
requested_version |
|
{ |
|
let message = format!( |
|
"Bypassing fnm: using {} node", |
|
system_version::display_name().cyan() |
|
); |
|
(message, system_version::path()) |
|
} else if let Some(alias_name) = requested_version.alias_name() { |
|
let alias_path = config.aliases_dir().join(&alias_name); |
|
let system_path = system_version::path(); |
|
if matches!(fs::shallow_read_symlink(&alias_path), Ok(shallow_path) if shallow_path == system_path) |
|
{ |
|
let message = format!( |
|
"Bypassing fnm: using {} node", |
|
system_version::display_name().cyan() |
|
); |
|
(message, system_path) |
|
} else if alias_path.exists() { |
|
let message = format!("Using Node for alias {}", alias_name.cyan()); |
|
(message, alias_path) |
|
} else { |
|
install_new_version(requested_version, config, self.install_if_missing)?; |
|
return Ok(()); |
|
} |
|
} else { |
|
let current_version = requested_version.to_version(&all_versions, config); |
|
if let Some(version) = current_version { |
|
let version_path = config |
|
.installations_dir() |
|
.join(version.to_string()) |
|
.join("installation"); |
|
let message = format!("Using Node {}", version.to_string().cyan()); |
|
(message, version_path) |
|
} else { |
|
install_new_version(requested_version, config, self.install_if_missing)?; |
|
return Ok(()); |
|
} |
|
}; |
|
|
|
if !self.silent_if_unchanged || will_version_change(&version_path, config) { |
|
outln!(config, Info, "{}", message); |
|
} |
|
|
|
if let Some(multishells_path) = multishell_path.parent() { |
|
std::fs::create_dir_all(multishells_path).map_err(|_err| { |
|
Error::MultishellDirectoryCreationIssue { |
|
path: multishells_path.to_path_buf(), |
|
} |
|
})?; |
|
} |
|
|
|
replace_symlink(&version_path, multishell_path) |
|
.map_err(|source| Error::SymlinkingCreationIssue { source })?; |
|
|
|
Ok(()) |
|
} |
|
} |
|
|
|
fn will_version_change(resolved_path: &Path, config: &FnmConfig) -> bool { |
|
let current_version_path = current_version(config) |
|
.unwrap_or(None) |
|
.map(|v| v.installation_path(config)); |
|
|
|
current_version_path.as_deref() != Some(resolved_path) |
|
} |
|
|
|
fn install_new_version( |
|
requested_version: UserVersion, |
|
config: &FnmConfig, |
|
install_if_missing: bool, |
|
) -> Result<(), Error> { |
|
if !install_if_missing && !should_install_interactively(&requested_version) { |
|
return Err(Error::CantFindVersion { |
|
version: requested_version, |
|
}); |
|
} |
|
|
|
Install { |
|
version: Some(requested_version.clone()), |
|
..Install::default() |
|
} |
|
.apply(config) |
|
.map_err(|source| Error::InstallError { source })?; |
|
|
|
Use { |
|
version: Some(UserVersionReader::Direct(requested_version)), |
|
install_if_missing: true, |
|
silent_if_unchanged: false, |
|
} |
|
.apply(config)?; |
|
|
|
Ok(()) |
|
} |
|
|
|
/// Tries to delete `from`, and then tries to symlink `from` to `to` anyway. |
|
/// If the symlinking fails, it will return the errors in the following order: |
|
/// * The deletion error (if exists) |
|
/// * The creation error |
|
/// |
|
/// This way, we can create a symlink if it is missing. |
|
fn replace_symlink(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> { |
|
let symlink_deletion_result = fs::remove_symlink_dir(to); |
|
match fs::symlink_dir(from, to) { |
|
ok @ Ok(_) => ok, |
|
err @ Err(_) => symlink_deletion_result.and(err), |
|
} |
|
} |
|
|
|
fn should_install_interactively(requested_version: &UserVersion) -> bool { |
|
use std::io::Write; |
|
|
|
if !(atty::is(atty::Stream::Stdout) && atty::is(atty::Stream::Stdin)) { |
|
return false; |
|
} |
|
|
|
let error_message = format!( |
|
"Can't find an installed Node version matching {}.", |
|
requested_version.to_string().italic() |
|
); |
|
eprintln!("{}", error_message.red()); |
|
let do_you_want = format!("Do you want to install it? {} [y/N]:", "answer".bold()); |
|
eprint!("{} ", do_you_want.yellow()); |
|
std::io::stdout().flush().unwrap(); |
|
let mut s = String::new(); |
|
std::io::stdin() |
|
.read_line(&mut s) |
|
.expect("Can't read user input"); |
|
|
|
s.trim().to_lowercase() == "y" |
|
} |
|
|
|
fn warn_if_multishell_path_not_in_path_env_var( |
|
multishell_path: &std::path::Path, |
|
config: &FnmConfig, |
|
) { |
|
let bin_path = if cfg!(unix) { |
|
multishell_path.join("bin") |
|
} else { |
|
multishell_path.to_path_buf() |
|
}; |
|
|
|
let fixed_path = bin_path.to_str().and_then(shell::maybe_fix_windows_path); |
|
let fixed_path = fixed_path.as_ref().map(|x| &x[..]); |
|
|
|
for path in std::env::split_paths(&std::env::var("PATH").unwrap_or_default()) { |
|
if bin_path == path || fixed_path == path.to_str() { |
|
return; |
|
} |
|
} |
|
|
|
outln!( |
|
config, Error, |
|
"{} {}\n{}\n{}", |
|
"warning:".yellow().bold(), |
|
"The current Node.js path is not on your PATH environment variable.".yellow(), |
|
"You should setup your shell profile to evaluate `fnm env`, see https://github.com/Schniz/fnm#shell-setup on how to do this".yellow(), |
|
"Check out our documentation for more information: https://fnm.vercel.app".yellow() |
|
); |
|
} |
|
|
|
#[derive(Debug, Error)] |
|
pub enum Error { |
|
#[error("Can't create the symlink: {}", source)] |
|
SymlinkingCreationIssue { source: std::io::Error }, |
|
#[error(transparent)] |
|
InstallError { source: <Install as Command>::Error }, |
|
#[error("Can't get locally installed versions: {}", source)] |
|
VersionListingError { source: installed_versions::Error }, |
|
#[error("Requested version {} is not currently installed", version)] |
|
CantFindVersion { version: UserVersion }, |
|
#[error(transparent)] |
|
CantInferVersion { |
|
#[from] |
|
source: InferVersionError, |
|
}, |
|
#[error( |
|
"{}\n{}\n{}", |
|
"We can't find the necessary environment variables to replace the Node version.", |
|
"You should setup your shell profile to evaluate `fnm env`, see https://github.com/Schniz/fnm#shell-setup on how to do this", |
|
"Check out our documentation for more information: https://fnm.vercel.app" |
|
)] |
|
FnmEnvWasNotSourced, |
|
#[error("Can't create the multishell directory: {}", path.display())] |
|
MultishellDirectoryCreationIssue { path: std::path::PathBuf }, |
|
} |
|
|
|
#[derive(Debug, Error)] |
|
pub enum InferVersionError { |
|
#[error("Can't find version in dotfiles. Please provide a version manually to the command.")] |
|
Local, |
|
#[error("Could not find any version to use. Maybe you don't have a default version set?\nTry running `fnm default <VERSION>` to set one,\nor create a .node-version file inside your project to declare a Node.js version.")] |
|
Recursive, |
|
}
|
|
|