use super::command::Command;
use super::r#use::Use;
use crate::config::FnmConfig;
use crate::fs::symlink_dir;
use crate::outln;
use crate::path_ext::PathExt;
use crate::shell::{infer_shell, Shell, Shells};
use clap::ValueEnum;
use colored::Colorize;
use std::collections::HashMap;
use std::fmt::Debug;
use std::io::IsTerminal;
use thiserror::Error;
#[derive(clap::Parser, Debug, Default)]
pub struct Env {
#[clap(long)]
shell: Option<Shells>,
#[clap(long, conflicts_with = "shell")]
json: bool,
#[clap(long, hide = true)]
multi: bool,
#[clap(long)]
use_on_cd: bool,
}
fn generate_symlink_path() -> String {
format!(
"{}_{}",
std::process::id(),
chrono::Utc::now().timestamp_millis(),
)
}
fn make_symlink(config: &FnmConfig) -> Result<std::path::PathBuf, Error> {
let base_dir = config.multishell_storage().ensure_exists_silently();
let mut temp_dir = base_dir.join(generate_symlink_path());
while temp_dir.exists() {
temp_dir = base_dir.join(generate_symlink_path());
}
match symlink_dir(config.default_version_dir(), &temp_dir) {
Ok(()) => Ok(temp_dir),
Err(source) => Err(Error::CantCreateSymlink { source, temp_dir }),
}
}
#[inline]
fn bool_as_str(value: bool) -> &'static str {
if value {
"true"
} else {
"false"
}
}
fn set_path_for_multishell(multishell_path: &std::path::Path) {
let path_for_node = if cfg!(windows) {
multishell_path.to_path_buf()
} else {
multishell_path.join("bin")
};
let current_path = std::env::var_os("PATH").unwrap_or_default();
let mut split_paths: Vec<_> = std::env::split_paths(¤t_path).collect();
split_paths.insert(0, path_for_node);
if let Ok(new_path) = std::env::join_paths(split_paths) {
unsafe {
std::env::set_var("PATH", new_path);
}
}
}
impl Command for Env {
type Error = Error;
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
if self.multi {
outln!(
config,
Error,
"{} {} is deprecated. This is now the default.",
"warning:".yellow().bold(),
"--multi".italic()
);
}
let multishell_path = make_symlink(config)?;
let base_dir = config.base_dir_with_default();
let env_vars = [
("FNM_MULTISHELL_PATH", multishell_path.to_str().unwrap()),
(
"FNM_VERSION_FILE_STRATEGY",
config.version_file_strategy().as_str(),
),
("FNM_DIR", base_dir.to_str().unwrap()),
("FNM_LOGLEVEL", config.log_level().as_str()),
("FNM_NODE_DIST_MIRROR", config.node_dist_mirror.as_str()),
(
"FNM_COREPACK_ENABLED",
bool_as_str(config.corepack_enabled()),
),
("FNM_RESOLVE_ENGINES", bool_as_str(config.resolve_engines())),
("FNM_ARCH", config.arch.as_str()),
];
if self.json {
println!(
"{}",
serde_json::to_string(&HashMap::from(env_vars)).unwrap()
);
return Ok(());
}
let shell: Box<dyn Shell> = self
.shell
.map(Into::into)
.or_else(infer_shell)
.ok_or(Error::CantInferShell)?;
let binary_path = if cfg!(windows) {
shell.path(&multishell_path)
} else {
shell.path(&multishell_path.join("bin"))
};
println!("{}", binary_path?);
for (name, value) in &env_vars {
println!("{}", shell.set_env_var(name, value));
}
if self.use_on_cd {
set_path_for_multishell(&multishell_path);
let config_with_multishell =
config.clone().with_multishell_path(multishell_path.clone());
let use_cmd = Use {
version: None,
install_if_missing: false,
silent_if_unchanged: true,
info_to_stderr: true,
};
let should_force_stderr_color = !std::io::stdout().is_terminal()
&& std::io::stderr().is_terminal()
&& std::env::var_os("NO_COLOR").is_none();
if should_force_stderr_color {
colored::control::set_override(true);
}
let _ = use_cmd.apply(&config_with_multishell);
if should_force_stderr_color {
colored::control::unset_override();
}
println!("{}", shell.use_on_cd(config)?);
}
if let Some(v) = shell.rehash() {
println!("{v}");
}
Ok(())
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error(
"{}\n{}\n{}\n{}",
"Can't infer shell!",
"fnm can't infer your shell based on the process tree.",
"Maybe it is unsupported? we support the following shells:",
shells_as_string()
)]
CantInferShell,
#[error("Can't create the symlink for multishells at {temp_dir:?}. Maybe there are some issues with permissions for the directory? {source}")]
CantCreateSymlink {
#[source]
source: std::io::Error,
temp_dir: std::path::PathBuf,
},
#[error(transparent)]
ShellError {
#[from]
source: anyhow::Error,
},
}
fn shells_as_string() -> String {
Shells::value_variants()
.iter()
.map(|x| format!("* {x}"))
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_smoke() {
let config = FnmConfig::default();
Env {
#[cfg(windows)]
shell: Some(Shells::Cmd),
#[cfg(not(windows))]
shell: Some(Shells::Bash),
..Default::default()
}
.call(config);
}
}