diff --git a/src/args.rs b/src/args.rs index a375279..db633f3 100644 --- a/src/args.rs +++ b/src/args.rs @@ -10,6 +10,10 @@ pub struct Args { #[arg(long)] pub verbose: bool, + /// Overrides all versions found in the environment and uses this one instead + #[arg(long)] + pub use_version: Option, + #[command(subcommand)] pub command: Command, } @@ -34,7 +38,7 @@ pub enum Command { /// Sets the specified version as the global default #[command()] - Default(DefaultArgs), + SetDefault(DefaultArgs), /// Creates wrapper scripts for node binaries /// so they can be found in the path and are executed diff --git a/src/config/mod.rs b/src/config/mod.rs index d3bedeb..db84418 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -71,6 +71,7 @@ impl ConfigAccess { let cfg = toml::from_str(&cfg_string) .map_err(|e| ParseConfigError::new("config.toml", cfg_string, e))?; + tracing::debug!("{cfg:?}"); Ok(Self::new(cfg)) } diff --git a/src/config/value.rs b/src/config/value.rs index cc7583c..5b13388 100644 --- a/src/config/value.rs +++ b/src/config/value.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use crate::{consts::NODE_DIST_URL, repository::NodeVersion}; @@ -9,6 +11,10 @@ pub struct Config { /// Configuration for how to download node versions pub download: DownloadConfig, + + /// List of executables that are hardwired to a given node version + /// and can still be executed from other versions with this given version. + pub bins: HashMap, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -23,6 +29,14 @@ pub struct DownloadConfig { pub dist_base_url: String, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ExecutableConfig { + /// The node version to run this executable with. + /// This means that whatever the currently active version is + /// the given executable will always be executed with the configured one. + pub node_version: NodeVersion, +} + impl Default for NodeConfig { fn default() -> Self { Self { diff --git a/src/main.rs b/src/main.rs index 1ec8a0b..69a55f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ pub mod mapper; pub mod repository; mod utils; use miette::Result; +use repository::NodeVersion; use tracing::metadata::LevelFilter; use tracing_subscriber::fmt::format::FmtSpan; use xkcd_unreachable::xkcd_unreachable; @@ -35,12 +36,12 @@ async fn main() -> Result<()> { return Ok(()); } - let mut nenv = get_nenv().await?; + let mut nenv = get_nenv(args.use_version.as_ref()).await?; match args.command { args::Command::Install(v) => nenv.install(v.version).await, args::Command::Uninstall(v) => nenv.uninstall(v.version).await, - args::Command::Default(v) => nenv.set_system_default(v.version).await, + args::Command::SetDefault(v) => nenv.set_system_default(v.version).await, args::Command::Exec(args) => { let exit_code = nenv.exec(args.command, args.args).await?; @@ -62,8 +63,8 @@ fn print_version() { println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); } -async fn get_nenv() -> Result { - Nenv::init().await +async fn get_nenv(version_override: Option<&NodeVersion>) -> Result { + Nenv::init(version_override).await } fn init_tracing() { diff --git a/src/mapper/mapped_dir.rs b/src/mapper/mapped_dir.rs index d8acbae..185991d 100644 --- a/src/mapper/mapped_dir.rs +++ b/src/mapper/mapped_dir.rs @@ -1,22 +1,26 @@ -use std::{collections::HashSet, io, path::Path}; +use std::{ + collections::HashSet, + io, + path::{Path, PathBuf}, +}; -use tokio::fs::{self, DirEntry}; +use miette::miette; +use tokio::fs; use crate::{consts::BIN_DIR, error::MapDirError, repository::node_path::NodePath}; use miette::{Context, IntoDiagnostic, Result}; -struct NodeApp { - info: DirEntry, +pub struct NodeApp { + path: PathBuf, name: String, } impl NodeApp { - pub fn new(info: DirEntry) -> Self { - let path = info.path(); + pub fn new(path: PathBuf) -> Self { let name = path.file_stem().unwrap(); let name = name.to_string_lossy().into_owned(); - Self { info, name } + Self { path, name } } pub fn name(&self) -> &String { @@ -25,7 +29,11 @@ impl NodeApp { /// creates wrappers to map this application pub async fn map_executable(&self) -> Result<()> { - let src_path = BIN_DIR.join(self.info.file_name()); + let src_path = BIN_DIR.join( + self.path + .file_name() + .ok_or_else(|| miette!("The given path is not a file."))?, + ); self.write_wrapper_script(&src_path) .await .into_diagnostic() @@ -35,7 +43,7 @@ impl NodeApp { #[cfg(not(target_os = "windows"))] async fn write_wrapper_script(&self, path: &Path) -> Result<(), io::Error> { fs::write(path, format!("#!/bin/sh\nnenv exec {} \"$@\"", self.name)).await?; - let src_metadata = self.info.metadata().await?; + let src_metadata = self.path.metadata()?; fs::set_permissions(&path, src_metadata.permissions()).await?; Ok(()) @@ -48,13 +56,28 @@ impl NodeApp { format!("@echo off\nnenv exec {} %*", self.name), ) .await?; - let src_metadata = self.info.metadata().await?; + let src_metadata = self.path.metadata()?; fs::set_permissions(&path, src_metadata.permissions()).await?; Ok(()) } } +pub async fn map_direct(paths: Vec) -> Result<()> { + let results = futures::future::join_all( + paths + .into_iter() + .map(NodeApp::new) + .map(|n| async move { n.map_executable().await }), + ) + .await; + results + .into_iter() + .fold(Result::Ok(()), |acc, res| acc.and_then(|_| res))?; + + Ok(()) +} + pub async fn map_node_bin(node_path: &NodePath) -> Result<()> { let mapped_app_names = get_applications(&BIN_DIR) .await? @@ -87,7 +110,7 @@ async fn get_applications(path: &Path) -> Result> { let entry_path = entry.path(); if entry_path.is_file() && !exclude_path(&entry_path) { - files.push(NodeApp::new(entry)); + files.push(NodeApp::new(entry.path())); } } diff --git a/src/mapper/mod.rs b/src/mapper/mod.rs index abb4075..4b30e61 100644 --- a/src/mapper/mod.rs +++ b/src/mapper/mod.rs @@ -4,7 +4,10 @@ use tokio::fs; use crate::{consts::BIN_DIR, repository::node_path::NodePath}; -use self::{mapped_command::MappedCommand, mapped_dir::map_node_bin}; +use self::{ + mapped_command::MappedCommand, + mapped_dir::{map_direct, map_node_bin}, +}; use miette::{IntoDiagnostic, Result}; mod mapped_command; @@ -46,4 +49,15 @@ impl Mapper { Ok(()) } + + /// Maps all binaries + pub async fn map_bins(&self, binaries: Vec<(String, NodePath)>) -> Result<()> { + map_direct( + binaries + .into_iter() + .map(|(cmd, path)| path.bin().join(cmd)) + .collect(), + ) + .await + } } diff --git a/src/nenv.rs b/src/nenv.rs index c842973..0543f7d 100644 --- a/src/nenv.rs +++ b/src/nenv.rs @@ -5,7 +5,7 @@ use crate::{ consts::{BIN_DIR, CACHE_DIR, VERSION_FILE_PATH}, error::VersionError, mapper::Mapper, - repository::{NodeVersion, Repository}, + repository::{node_path::NodePath, NodeVersion, Repository}, utils::prompt, version_detection::{self, VersionDetector}, }; @@ -22,11 +22,16 @@ pub struct Nenv { impl Nenv { #[tracing::instrument(level = "debug")] - pub async fn init() -> Result { + pub async fn init(version_override: Option<&NodeVersion>) -> Result { let config = ConfigAccess::load().await?; let repo = Repository::init(config.clone()).await?; let default_version = { config.get().await.node.default_version.to_owned() }; - let active_version = Self::get_active_version().await.unwrap_or(default_version); + + let active_version = if let Some(version) = version_override { + version.to_owned() + } else { + Self::get_active_version().await.unwrap_or(default_version) + }; Ok(Self { config, @@ -55,7 +60,11 @@ impl Nenv { } else { self.repo.install_version(&version).await?; self.active_version = version.to_owned(); - self.get_mapper().await?.remap_additive().await?; + let mapper = self.get_mapper().await?; + mapper.remap_additive().await?; + mapper + .map_bins(self.get_binaries_with_path().await?) + .await?; println!("Installed {}", version.to_string().bold()); Ok(()) @@ -109,6 +118,9 @@ impl Nenv { /// Executes a given node executable for the currently active version #[tracing::instrument(skip(self))] pub async fn exec(&mut self, command: String, args: Vec) -> Result { + if let Some(cfg) = self.config.get().await.bins.get(&command) { + self.active_version = cfg.node_version.to_owned(); + } if !self.repo.is_installed(&self.active_version).await? { self.repo.install_version(&self.active_version).await?; } @@ -120,7 +132,9 @@ impl Nenv { /// Clears the version cache and remaps all executables #[tracing::instrument(skip(self))] pub async fn remap(&mut self) -> Result<()> { - self.get_mapper().await?.remap().await + let mapper = self.get_mapper().await?; + mapper.remap().await?; + mapper.map_bins(self.get_binaries_with_path().await?).await } /// Lists the currently installed versions @@ -241,4 +255,18 @@ impl Nenv { .ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?; Ok(Mapper::new(node_path)) } + + async fn get_binaries_with_path(&mut self) -> Result> { + let mut binaries_with_path = Vec::new(); + + for (bin, cfg) in &self.config.get().await.bins { + let path = self + .repo + .get_version_path(&cfg.node_version)? + .ok_or_else(|| VersionError::not_installed(&cfg.node_version))?; + binaries_with_path.push((bin.to_owned(), path)); + } + + Ok(binaries_with_path) + } } diff --git a/src/repository/mod.rs b/src/repository/mod.rs index f7d91e2..bd7d2ca 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -129,20 +129,18 @@ impl Repository { &*BIN_DIR, &*NODE_VERSIONS_DIR, ]; - for result in future::join_all(dirs.into_iter().map(|dir| async move { + future::join_all(dirs.into_iter().map(|dir| async move { if !dir.exists() { - fs::create_dir_all(dir).await.into_diagnostic()?; + fs::create_dir_all(dir).await?; } - Ok(()) + Result::<(), std::io::Error>::Ok(()) })) .await - { - #[allow(clippy::question_mark)] - if let Err(e) = result { - return Err(e); - } - } + .into_iter() + .fold(Result::Ok(()), |acc, res| acc.and_then(|_| res)) + .into_diagnostic() + .wrap_err("Failed to create application directory")?; Ok(()) }