From 50a42df15bd2a9b48872be978c4ccdcaab543cd6 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 21 Jan 2023 18:08:28 +0100 Subject: [PATCH] Improve mapping and add refresh command for env --- Cargo.toml | 2 ++ src/args.rs | 22 ++++++++++--- src/consts.rs | 1 + src/error.rs | 2 ++ src/lib.rs | 14 +++++++-- src/main.rs | 14 +++++---- src/mapper/mapped_dir.rs | 63 ++++++++++++++++---------------------- src/mapper/mod.rs | 15 ++++++++- src/repository/mod.rs | 12 ++++++-- src/repository/versions.rs | 23 +++++++++++++- src/web_api/model.rs | 6 ++-- 11 files changed, 118 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1928f02..dfcd479 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ color-eyre = "0.6.2" crossterm = "0.25.0" dialoguer = "0.10.3" dirs = "4.0.0" +futures = "0.3.25" futures-util = "0.3.25" indicatif = "0.17.3" lazy_static = "1.4.0" @@ -26,6 +27,7 @@ miette = "5.5.0" reqwest = { version = "0.11.14", features = ["json", "stream"] } semver = { version = "1.0.16", features = ["std", "serde"] } serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.91" set_env = "1.3.4" tar = "0.4.38" thiserror = "1.0.38" diff --git a/src/args.rs b/src/args.rs index 0951ecb..d32112e 100644 --- a/src/args.rs +++ b/src/args.rs @@ -12,34 +12,46 @@ pub struct Args { #[derive(Clone, Debug, Subcommand)] pub enum Command { + /// Returns the nenv version + #[command(short_flag = 'v', aliases = &["--version"])] + Version, + + /// Installs the given node version #[command()] Install(InstallArgs), + /// Sets the specified version as the global default #[command()] - Use(UseArgs), + Default(DefaultArgs), - #[command(short_flag = 'v', aliases = &["--version"])] - Version, + /// Refreshes the node environment mappings and cache + #[command()] + Refresh, + /// Executes the given version specific node executable #[command()] Exec(ExecArgs), } #[derive(Clone, Debug, Parser)] pub struct ExecArgs { + /// The command to execute #[arg()] pub command: String, + + /// The arguments for the command #[arg(trailing_var_arg = true, allow_hyphen_values = true)] pub args: Vec, } #[derive(Clone, Debug, Parser)] pub struct InstallArgs { + /// the version to install pub version: NodeVersion, } #[derive(Clone, Debug, Parser)] -pub struct UseArgs { - #[arg()] +pub struct DefaultArgs { + /// The version to set as default pub version: NodeVersion, } diff --git a/src/consts.rs b/src/consts.rs index e55ca56..1331e37 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -14,6 +14,7 @@ lazy_static! { .unwrap_or_else(|| PathBuf::from(".cache")) .join(PathBuf::from("nenv")); pub static ref CFG_FILE_PATH: PathBuf = CFG_DIR.join("config.toml"); + pub static ref VERSION_FILE_PATH: PathBuf = DATA_DIR.join("versions.json"); pub static ref BIN_DIR: PathBuf = DATA_DIR.join("bin"); pub static ref NODE_VERSIONS_DIR: PathBuf = DATA_DIR.join("versions"); pub static ref NODE_ARCHIVE_SUFFIX: String = format!("-{OS}-{ARCH}.{ARCHIVE_TYPE}"); diff --git a/src/error.rs b/src/error.rs index 6131bfc..9d9b196 100644 --- a/src/error.rs +++ b/src/error.rs @@ -44,6 +44,8 @@ pub enum Error { #[diagnostic_source] MapperError, ), + #[error("Failed to work with json: {0}")] + Json(#[from] serde_json::Error), #[error("IO Error: {0}")] Io(#[from] io::Error), diff --git a/src/lib.rs b/src/lib.rs index ef3cb57..5cf9365 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use std::ffi::OsString; +use consts::{DATA_DIR, VERSION_FILE_PATH}; use crossterm::style::Stylize; use mapper::Mapper; use repository::{config::Config, NodeVersion, Repository}; @@ -12,8 +13,10 @@ mod utils; mod web_api; use dialoguer::Confirm; use error::Result; +use tokio::fs; pub async fn install_version(version: NodeVersion) -> Result<()> { + fs::remove_file(&*VERSION_FILE_PATH).await?; let repo = get_repository().await?; if repo.is_installed(&version).await? { @@ -32,7 +35,7 @@ pub async fn install_version(version: NodeVersion) -> Result<()> { Ok(()) } -pub async fn use_version(version: NodeVersion) -> Result<()> { +pub async fn set_default_version(version: NodeVersion) -> Result<()> { let mut mapper = get_mapper().await?; if !mapper.repository().is_installed(&version).await? @@ -47,7 +50,7 @@ pub async fn use_version(version: NodeVersion) -> Result<()> { mapper.repository().install_version(&version).await?; } - mapper.use_version(&version).await?; + mapper.set_default_version(&version).await?; println!("Now using {}", version.to_string().bold()); Ok(()) @@ -65,6 +68,13 @@ pub async fn exec(command: String, args: Vec) -> Result { Ok(exit_status.code().unwrap_or(0)) } +pub async fn refresh() -> Result<()> { + get_mapper().await?.remap().await?; + fs::remove_file(&*VERSION_FILE_PATH).await?; + + Ok(()) +} + async fn get_repository() -> Result { Repository::init(Config::load().await?).await } diff --git a/src/main.rs b/src/main.rs index f1b1e0a..6d10ff7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,18 +6,20 @@ use clap::Parser; mod args; #[tokio::main(flavor = "current_thread")] -async fn main() { +async fn main() -> nenv::error::Result<()> { color_eyre::install().unwrap(); let args: Args = Args::parse(); match args.commmand { - args::Command::Install(v) => nenv::install_version(v.version).await.unwrap(), - args::Command::Use(v) => nenv::use_version(v.version).await.unwrap(), - args::Command::Version => print_version(), + args::Command::Version => Ok(print_version()), + args::Command::Install(v) => nenv::install_version(v.version).await, + args::Command::Default(v) => nenv::set_default_version(v.version).await, args::Command::Exec(args) => { - let exit_code = nenv::exec(args.command, args.args).await.unwrap(); + let exit_code = nenv::exec(args.command, args.args).await?; + process::exit(exit_code); } - }; + args::Command::Refresh => nenv::refresh().await, + } } fn print_version() { diff --git a/src/mapper/mapped_dir.rs b/src/mapper/mapped_dir.rs index 2035f43..7775575 100644 --- a/src/mapper/mapped_dir.rs +++ b/src/mapper/mapped_dir.rs @@ -11,43 +11,31 @@ use super::error::MapperResult; struct NodeApp { info: DirEntry, + name: String, } impl NodeApp { pub fn new(info: DirEntry) -> Self { - Self { info } - } + let path = info.path(); + let name = path.file_stem().unwrap(); + let name = name.to_string_lossy().into_owned(); - /// returns the name of the application - pub fn name(&self) -> String { - let name = self.info.file_name(); - name.to_string_lossy().into_owned() + Self { info, name } } - pub async fn unmap(&self) -> MapperResult<()> { - fs::remove_file(self.info.path()).await?; - - Ok(()) + pub fn name(&self) -> &String { + &self.name } /// creates wrappers to map this application pub async fn map_executable(&self) -> MapperResult<()> { let src_path = BIN_DIR.join(self.info.file_name()); - let name = self.info.file_name(); - let name = name.to_string_lossy(); - self.write_wrapper_script(&name, &src_path).await + self.write_wrapper_script(&src_path).await } #[cfg(not(target_os = "windows"))] - async fn write_wrapper_script(&self, name: &str, path: &Path) -> MapperResult<()> { - fs::write( - path, - format!( - r#"#!/bin/sh - nenv exec {name} "$@""# - ), - ) - .await?; + async fn write_wrapper_script(&self, path: &Path) -> MapperResult<()> { + fs::write(path, format!("#!/bin/sh\nnenv exec {} \"$@\"", self.name)).await?; let src_metadata = self.info.metadata().await?; fs::set_permissions(&path, src_metadata.permissions()).await?; @@ -55,8 +43,13 @@ impl NodeApp { } #[cfg(target_os = "windows")] - async fn write_wrapper_script(&self, name: &str, path: &Path) -> MapperResult<()> { - fs::write(path, format!("nenv exec {name} %*")).await?; + async fn write_wrapper_script(&self, path: &Path) -> MapperResult<()> { + fs::write( + path.with_extension("bat"), + format!("nenv exec {} %*", self.name), + ) + .await?; + let src_metadata = self.info.metadata().await?; fs::set_permissions(&path, src_metadata.permissions()).await?; Ok(()) @@ -64,19 +57,17 @@ impl NodeApp { } pub async fn map_node_bin(node_path: NodePath) -> MapperResult<()> { - let applications = get_applications(&node_path.bin()).await?; - let mapped_applications = get_applications(&*BIN_DIR).await?; - let mut new_mapped = HashSet::new(); + let mapped_app_names = get_applications(&*BIN_DIR) + .await? + .iter() + .map(NodeApp::name) + .cloned() + .collect::>(); - for application in applications { - application.map_executable().await?; - new_mapped.insert(application.name()); - } - for app in mapped_applications { - if !new_mapped.contains(&app.name()) { - app.unmap().await?; - } - } + let mut applications = get_applications(&node_path.bin()).await?; + applications.retain(|app| !mapped_app_names.contains(app.name())); + + futures::future::join_all(applications.iter().map(NodeApp::map_executable)).await; Ok(()) } diff --git a/src/mapper/mod.rs b/src/mapper/mod.rs index 788d1e0..2961d28 100644 --- a/src/mapper/mod.rs +++ b/src/mapper/mod.rs @@ -1,6 +1,9 @@ use std::{env, ffi::OsString, process::ExitStatus, str::FromStr}; +use tokio::fs; + use crate::{ + consts::BIN_DIR, error::LibResult, repository::{NodeVersion, Repository}, }; @@ -32,7 +35,7 @@ impl Mapper { } /// Sets the given version as the default one - pub async fn use_version(&mut self, version: &NodeVersion) -> LibResult<()> { + pub async fn set_default_version(&mut self, version: &NodeVersion) -> LibResult<()> { self.repo .config .set_default_version(version.clone()) @@ -60,10 +63,20 @@ impl Mapper { .run() .await .map_err(MapperError::from)?; + self.map_active_version().await?; Ok(exit_status) } + /// Recreates all environment mappings + pub async fn remap(&self) -> LibResult<()> { + fs::remove_dir_all(&*BIN_DIR).await?; + fs::create_dir_all(&*BIN_DIR).await?; + self.map_active_version().await?; + + Ok(()) + } + fn get_version() -> Option { env::var("NODE_VERSION") .ok() diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 31deee0..9b25053 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -90,12 +90,20 @@ impl Repository { pub async fn init(config: Config) -> LibResult { Self::create_folders().await?; let web_api = WebApi::new(&config.dist_base_url); - let all_versions = web_api.get_versions().await?; + + let versions = if let Some(v) = Versions::load().await { + v + } else { + let all_versions = web_api.get_versions().await?; + let v = Versions::new(all_versions); + v.save().await?; + v + }; Ok(Self { config, web_api, - versions: Versions::new(all_versions), + versions, }) } diff --git a/src/repository/versions.rs b/src/repository/versions.rs index 513e26f..44d067f 100644 --- a/src/repository/versions.rs +++ b/src/repository/versions.rs @@ -1,15 +1,29 @@ use std::collections::HashMap; use semver::{Version, VersionReq}; +use serde::{Deserialize, Serialize}; +use tokio::fs; -use crate::web_api::VersionInfo; +use crate::{consts::VERSION_FILE_PATH, error::LibResult, web_api::VersionInfo}; +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Versions { lts_versions: HashMap, versions: HashMap, } impl Versions { + /// Loads the versions from the cached versions.json file + pub(crate) async fn load() -> Option { + if !VERSION_FILE_PATH.exists() { + return None; + } + let versions_string = fs::read_to_string(&*VERSION_FILE_PATH).await.ok()?; + let versions = serde_json::from_str(&versions_string).ok()?; + + Some(versions) + } + /// creates a new instance to access version information pub fn new(all_versions: Vec) -> Self { let lts_versions = all_versions @@ -32,6 +46,13 @@ impl Versions { } } + pub(crate) async fn save(&self) -> LibResult<()> { + let json_string = serde_json::to_string(&self)?; + fs::write(&*VERSION_FILE_PATH, json_string).await?; + + Ok(()) + } + /// Returns the latest known node version pub fn latest(&self) -> &VersionInfo { let mut versions = self.versions.keys().collect::>(); diff --git a/src/web_api/model.rs b/src/web_api/model.rs index 05284c6..d66f57f 100644 --- a/src/web_api/model.rs +++ b/src/web_api/model.rs @@ -1,10 +1,10 @@ use std::borrow::Cow; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize}; /// Represents a single nodejs version info entry /// as retrieved from nodejs.org -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct VersionInfo { #[serde(deserialize_with = "deserialize_prefixed_version")] pub version: semver::Version, @@ -19,7 +19,7 @@ pub struct VersionInfo { pub files: Vec, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ModuleVersions { pub v8: String, pub npm: Option,