Skip to content

Commit

Permalink
fnm env --global: considering if this is a good idea
Browse files Browse the repository at this point in the history
technically i think that i can just mutate the 'default' alias instead. because now that i think about it,
it makes 0 sense to set current to 'default' every time a new shell is opened. so current _is_ default.
  • Loading branch information
Schniz committed Dec 25, 2024
1 parent c054f1a commit 880a296
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 28 deletions.
9 changes: 6 additions & 3 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,21 +401,24 @@ Options:
--json
Print JSON instead of shell commands
--global
Have a single Node.js version defined globally. This opts out of the "multishell" solution which allows a Node.js version per shell session. All your shell sessions will share a global Node.js version, and calling `fnm use` will update the global pointer
--log-level <LOG_LEVEL>
The log level of fnm commands
[env: FNM_LOGLEVEL]
[default: info]
[possible values: quiet, error, info]
--use-on-cd
Print the script to change Node versions every directory change
--arch <ARCH>
Override the architecture of the installed Node binary. Defaults to arch of fnm binary
[env: FNM_ARCH]
--use-on-cd
Print the script to change Node versions every directory change
--version-file-strategy <VERSION_FILE_STRATEGY>
A strategy for how to resolve the Node version. Used whenever `fnm use` or `fnm install` is called without a version, or when `--use-on-cd` is configured on evaluation
Expand Down
42 changes: 33 additions & 9 deletions src/commands/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::collections::HashMap;
use std::fmt::Debug;
use thiserror::Error;

#[allow(clippy::struct_excessive_bools)]
#[derive(clap::Parser, Debug, Default)]
pub struct Env {
/// The shell syntax to use. Infers when missing.
Expand All @@ -21,6 +22,11 @@ pub struct Env {
/// Deprecated. This is the default now.
#[clap(long, hide = true)]
multi: bool,
/// Have a single Node.js version defined globally. This opts out of the "multishell" solution
/// which allows a Node.js version per shell session. All your shell sessions will share a
/// global Node.js version, and calling `fnm use` will update the global pointer.
#[clap(long, conflicts_with = "multi")]
global: bool,
/// Print the script to change Node versions every directory change
#[clap(long)]
use_on_cd: bool,
Expand All @@ -36,15 +42,15 @@ fn generate_symlink_path() -> String {

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());
let mut path = base_dir.join(generate_symlink_path());

while temp_dir.exists() {
temp_dir = base_dir.join(generate_symlink_path());
while path.exists() {
path = 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 }),
match symlink_dir(config.default_version_dir(), &path) {
Ok(()) => Ok(path),
Err(source) => Err(Error::CantCreateSymlink { source, path }),
}
}

Expand All @@ -71,7 +77,19 @@ impl Command for Env {
);
}

let multishell_path = make_symlink(config)?;
let multishell_path = if self.global {
let current_path = config.current_global_version_path();
crate::fs::two_phase_symlink(&config.default_version_dir(), &current_path).map_err(
|source| Error::SettingGlobalVersionSymlink {
source,
path: current_path.clone(),
},
)?;
current_path
} else {
make_symlink(config)?
};

let base_dir = config.base_dir_with_default();

let env_vars = [
Expand Down Expand Up @@ -138,11 +156,17 @@ pub enum Error {
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}")]
#[error("Can't create the symlink for multishells at {path:?}. Maybe there are some issues with permissions for the directory? {source}")]
CantCreateSymlink {
#[source]
source: std::io::Error,
temp_dir: std::path::PathBuf,
path: std::path::PathBuf,
},
#[error("Can't assign global version symlink at {path:?}. Maybe there are some issues with permissions for the directory? {source}")]
SettingGlobalVersionSymlink {
#[source]
source: crate::fs::TwoPhaseSymlinkError,
path: std::path::PathBuf,
},
#[error(transparent)]
ShellError {
Expand Down
20 changes: 4 additions & 16 deletions src/commands/use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl Command for Use {
})?;
}

replace_symlink(&version_path, multishell_path)
crate::fs::two_phase_symlink(&version_path, multishell_path)
.map_err(|source| Error::SymlinkingCreationIssue { source })?;

Ok(())
Expand Down Expand Up @@ -150,20 +150,6 @@ fn install_new_version(
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::{IsTerminal, Write};

Expand Down Expand Up @@ -221,7 +207,9 @@ fn warn_if_multishell_path_not_in_path_env_var(
#[derive(Debug, Error)]
pub enum Error {
#[error("Can't create the symlink: {}", source)]
SymlinkingCreationIssue { source: std::io::Error },
SymlinkingCreationIssue {
source: crate::fs::TwoPhaseSymlinkError,
},
#[error(transparent)]
InstallError { source: <Install as Command>::Error },
#[error("Can't get locally installed versions: {}", source)]
Expand Down
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ impl FnmConfig {
self.directories.multishell_storage()
}

pub fn current_global_version_path(&self) -> std::path::PathBuf {
self.base_dir_with_default().join("current")
}

#[cfg(test)]
pub fn with_base_dir(mut self, base_dir: Option<std::path::PathBuf>) -> Self {
self.base_dir = base_dir;
Expand Down
25 changes: 25 additions & 0 deletions src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,28 @@ pub fn remove_symlink_dir<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
pub fn shallow_read_symlink<P: AsRef<Path>>(path: P) -> std::io::Result<std::path::PathBuf> {
std::fs::read_link(path)
}

/// Creates a symlink into a temporary directory,
/// then renames the temporary directory to the target.
///
/// This is required as symlinking doesn't override existing
/// symlinks.
/// The naive solution is to delete, and then symlink, but that can create
/// a race condition in the filesystem.
pub fn two_phase_symlink(
source: &std::path::Path,
target: &std::path::Path,
) -> Result<(), TwoPhaseSymlinkError> {
let temp = tempfile::tempdir().map_err(TwoPhaseSymlinkError::CreatingTempSymlink)?;
let temp_path = temp.path().join("temp");
symlink_dir(source, &temp_path).map_err(TwoPhaseSymlinkError::CreatingTempSymlink)?;
std::fs::rename(&temp_path, target).map_err(TwoPhaseSymlinkError::Renaming)
}

#[derive(Debug, thiserror::Error)]
pub enum TwoPhaseSymlinkError {
#[error("Can't create temporary symlink for two phase symlink: {0}")]
CreatingTempSymlink(#[source] std::io::Error),
#[error("Can't create a symlink in the target directory: {0}")]
Renaming(#[source] std::io::Error),
}

0 comments on commit 880a296

Please sign in to comment.