Improve mapping and add refresh command for env

feature/lookup-installed
trivernis 2 years ago
parent 75e80ade99
commit 50a42df15b
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -18,6 +18,7 @@ color-eyre = "0.6.2"
crossterm = "0.25.0" crossterm = "0.25.0"
dialoguer = "0.10.3" dialoguer = "0.10.3"
dirs = "4.0.0" dirs = "4.0.0"
futures = "0.3.25"
futures-util = "0.3.25" futures-util = "0.3.25"
indicatif = "0.17.3" indicatif = "0.17.3"
lazy_static = "1.4.0" lazy_static = "1.4.0"
@ -26,6 +27,7 @@ miette = "5.5.0"
reqwest = { version = "0.11.14", features = ["json", "stream"] } reqwest = { version = "0.11.14", features = ["json", "stream"] }
semver = { version = "1.0.16", features = ["std", "serde"] } semver = { version = "1.0.16", features = ["std", "serde"] }
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"
set_env = "1.3.4" set_env = "1.3.4"
tar = "0.4.38" tar = "0.4.38"
thiserror = "1.0.38" thiserror = "1.0.38"

@ -12,34 +12,46 @@ pub struct Args {
#[derive(Clone, Debug, Subcommand)] #[derive(Clone, Debug, Subcommand)]
pub enum Command { pub enum Command {
/// Returns the nenv version
#[command(short_flag = 'v', aliases = &["--version"])]
Version,
/// Installs the given node version
#[command()] #[command()]
Install(InstallArgs), Install(InstallArgs),
/// Sets the specified version as the global default
#[command()] #[command()]
Use(UseArgs), Default(DefaultArgs),
#[command(short_flag = 'v', aliases = &["--version"])] /// Refreshes the node environment mappings and cache
Version, #[command()]
Refresh,
/// Executes the given version specific node executable
#[command()] #[command()]
Exec(ExecArgs), Exec(ExecArgs),
} }
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]
pub struct ExecArgs { pub struct ExecArgs {
/// The command to execute
#[arg()] #[arg()]
pub command: String, pub command: String,
/// The arguments for the command
#[arg(trailing_var_arg = true, allow_hyphen_values = true)] #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub args: Vec<OsString>, pub args: Vec<OsString>,
} }
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]
pub struct InstallArgs { pub struct InstallArgs {
/// the version to install
pub version: NodeVersion, pub version: NodeVersion,
} }
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]
pub struct UseArgs { pub struct DefaultArgs {
#[arg()] /// The version to set as default
pub version: NodeVersion, pub version: NodeVersion,
} }

@ -14,6 +14,7 @@ lazy_static! {
.unwrap_or_else(|| PathBuf::from(".cache")) .unwrap_or_else(|| PathBuf::from(".cache"))
.join(PathBuf::from("nenv")); .join(PathBuf::from("nenv"));
pub static ref CFG_FILE_PATH: PathBuf = CFG_DIR.join("config.toml"); 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 BIN_DIR: PathBuf = DATA_DIR.join("bin");
pub static ref NODE_VERSIONS_DIR: PathBuf = DATA_DIR.join("versions"); pub static ref NODE_VERSIONS_DIR: PathBuf = DATA_DIR.join("versions");
pub static ref NODE_ARCHIVE_SUFFIX: String = format!("-{OS}-{ARCH}.{ARCHIVE_TYPE}"); pub static ref NODE_ARCHIVE_SUFFIX: String = format!("-{OS}-{ARCH}.{ARCHIVE_TYPE}");

@ -44,6 +44,8 @@ pub enum Error {
#[diagnostic_source] #[diagnostic_source]
MapperError, MapperError,
), ),
#[error("Failed to work with json: {0}")]
Json(#[from] serde_json::Error),
#[error("IO Error: {0}")] #[error("IO Error: {0}")]
Io(#[from] io::Error), Io(#[from] io::Error),

@ -1,5 +1,6 @@
use std::ffi::OsString; use std::ffi::OsString;
use consts::{DATA_DIR, VERSION_FILE_PATH};
use crossterm::style::Stylize; use crossterm::style::Stylize;
use mapper::Mapper; use mapper::Mapper;
use repository::{config::Config, NodeVersion, Repository}; use repository::{config::Config, NodeVersion, Repository};
@ -12,8 +13,10 @@ mod utils;
mod web_api; mod web_api;
use dialoguer::Confirm; use dialoguer::Confirm;
use error::Result; use error::Result;
use tokio::fs;
pub async fn install_version(version: NodeVersion) -> Result<()> { pub async fn install_version(version: NodeVersion) -> Result<()> {
fs::remove_file(&*VERSION_FILE_PATH).await?;
let repo = get_repository().await?; let repo = get_repository().await?;
if repo.is_installed(&version).await? { if repo.is_installed(&version).await? {
@ -32,7 +35,7 @@ pub async fn install_version(version: NodeVersion) -> Result<()> {
Ok(()) Ok(())
} }
pub async fn use_version(version: NodeVersion) -> Result<()> { pub async fn set_default_version(version: NodeVersion) -> Result<()> {
let mut mapper = get_mapper().await?; let mut mapper = get_mapper().await?;
if !mapper.repository().is_installed(&version).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.repository().install_version(&version).await?;
} }
mapper.use_version(&version).await?; mapper.set_default_version(&version).await?;
println!("Now using {}", version.to_string().bold()); println!("Now using {}", version.to_string().bold());
Ok(()) Ok(())
@ -65,6 +68,13 @@ pub async fn exec(command: String, args: Vec<OsString>) -> Result<i32> {
Ok(exit_status.code().unwrap_or(0)) 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> { async fn get_repository() -> Result<Repository> {
Repository::init(Config::load().await?).await Repository::init(Config::load().await?).await
} }

@ -6,18 +6,20 @@ use clap::Parser;
mod args; mod args;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() -> nenv::error::Result<()> {
color_eyre::install().unwrap(); color_eyre::install().unwrap();
let args: Args = Args::parse(); let args: Args = Args::parse();
match args.commmand { match args.commmand {
args::Command::Install(v) => nenv::install_version(v.version).await.unwrap(), args::Command::Version => Ok(print_version()),
args::Command::Use(v) => nenv::use_version(v.version).await.unwrap(), args::Command::Install(v) => nenv::install_version(v.version).await,
args::Command::Version => print_version(), args::Command::Default(v) => nenv::set_default_version(v.version).await,
args::Command::Exec(args) => { 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); process::exit(exit_code);
} }
}; args::Command::Refresh => nenv::refresh().await,
}
} }
fn print_version() { fn print_version() {

@ -11,43 +11,31 @@ use super::error::MapperResult;
struct NodeApp { struct NodeApp {
info: DirEntry, info: DirEntry,
name: String,
} }
impl NodeApp { impl NodeApp {
pub fn new(info: DirEntry) -> Self { 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 Self { info, name }
pub fn name(&self) -> String {
let name = self.info.file_name();
name.to_string_lossy().into_owned()
} }
pub async fn unmap(&self) -> MapperResult<()> { pub fn name(&self) -> &String {
fs::remove_file(self.info.path()).await?; &self.name
Ok(())
} }
/// creates wrappers to map this application /// creates wrappers to map this application
pub async fn map_executable(&self) -> MapperResult<()> { pub async fn map_executable(&self) -> MapperResult<()> {
let src_path = BIN_DIR.join(self.info.file_name()); let src_path = BIN_DIR.join(self.info.file_name());
let name = self.info.file_name(); self.write_wrapper_script(&src_path).await
let name = name.to_string_lossy();
self.write_wrapper_script(&name, &src_path).await
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
async fn write_wrapper_script(&self, name: &str, path: &Path) -> MapperResult<()> { async fn write_wrapper_script(&self, path: &Path) -> MapperResult<()> {
fs::write( fs::write(path, format!("#!/bin/sh\nnenv exec {} \"$@\"", self.name)).await?;
path,
format!(
r#"#!/bin/sh
nenv exec {name} "$@""#
),
)
.await?;
let src_metadata = self.info.metadata().await?; let src_metadata = self.info.metadata().await?;
fs::set_permissions(&path, src_metadata.permissions()).await?; fs::set_permissions(&path, src_metadata.permissions()).await?;
@ -55,8 +43,13 @@ impl NodeApp {
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
async fn write_wrapper_script(&self, name: &str, path: &Path) -> MapperResult<()> { async fn write_wrapper_script(&self, path: &Path) -> MapperResult<()> {
fs::write(path, format!("nenv exec {name} %*")).await?; 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?; fs::set_permissions(&path, src_metadata.permissions()).await?;
Ok(()) Ok(())
@ -64,19 +57,17 @@ impl NodeApp {
} }
pub async fn map_node_bin(node_path: NodePath) -> MapperResult<()> { pub async fn map_node_bin(node_path: NodePath) -> MapperResult<()> {
let applications = get_applications(&node_path.bin()).await?; let mapped_app_names = get_applications(&*BIN_DIR)
let mapped_applications = get_applications(&*BIN_DIR).await?; .await?
let mut new_mapped = HashSet::new(); .iter()
.map(NodeApp::name)
.cloned()
.collect::<HashSet<_>>();
for application in applications { let mut applications = get_applications(&node_path.bin()).await?;
application.map_executable().await?; applications.retain(|app| !mapped_app_names.contains(app.name()));
new_mapped.insert(application.name());
} futures::future::join_all(applications.iter().map(NodeApp::map_executable)).await;
for app in mapped_applications {
if !new_mapped.contains(&app.name()) {
app.unmap().await?;
}
}
Ok(()) Ok(())
} }

@ -1,6 +1,9 @@
use std::{env, ffi::OsString, process::ExitStatus, str::FromStr}; use std::{env, ffi::OsString, process::ExitStatus, str::FromStr};
use tokio::fs;
use crate::{ use crate::{
consts::BIN_DIR,
error::LibResult, error::LibResult,
repository::{NodeVersion, Repository}, repository::{NodeVersion, Repository},
}; };
@ -32,7 +35,7 @@ impl Mapper {
} }
/// Sets the given version as the default one /// 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 self.repo
.config .config
.set_default_version(version.clone()) .set_default_version(version.clone())
@ -60,10 +63,20 @@ impl Mapper {
.run() .run()
.await .await
.map_err(MapperError::from)?; .map_err(MapperError::from)?;
self.map_active_version().await?;
Ok(exit_status) 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<NodeVersion> { fn get_version() -> Option<NodeVersion> {
env::var("NODE_VERSION") env::var("NODE_VERSION")
.ok() .ok()

@ -90,12 +90,20 @@ impl Repository {
pub async fn init(config: Config) -> LibResult<Self> { pub async fn init(config: Config) -> LibResult<Self> {
Self::create_folders().await?; Self::create_folders().await?;
let web_api = WebApi::new(&config.dist_base_url); let web_api = WebApi::new(&config.dist_base_url);
let versions = if let Some(v) = Versions::load().await {
v
} else {
let all_versions = web_api.get_versions().await?; let all_versions = web_api.get_versions().await?;
let v = Versions::new(all_versions);
v.save().await?;
v
};
Ok(Self { Ok(Self {
config, config,
web_api, web_api,
versions: Versions::new(all_versions), versions,
}) })
} }

@ -1,15 +1,29 @@
use std::collections::HashMap; use std::collections::HashMap;
use semver::{Version, VersionReq}; 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 { pub struct Versions {
lts_versions: HashMap<String, VersionReq>, lts_versions: HashMap<String, VersionReq>,
versions: HashMap<Version, VersionInfo>, versions: HashMap<Version, VersionInfo>,
} }
impl Versions { impl Versions {
/// Loads the versions from the cached versions.json file
pub(crate) async fn load() -> Option<Self> {
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 /// creates a new instance to access version information
pub fn new(all_versions: Vec<VersionInfo>) -> Self { pub fn new(all_versions: Vec<VersionInfo>) -> Self {
let lts_versions = all_versions 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 /// Returns the latest known node version
pub fn latest(&self) -> &VersionInfo { pub fn latest(&self) -> &VersionInfo {
let mut versions = self.versions.keys().collect::<Vec<_>>(); let mut versions = self.versions.keys().collect::<Vec<_>>();

@ -1,10 +1,10 @@
use std::borrow::Cow; use std::borrow::Cow;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer, Serialize};
/// Represents a single nodejs version info entry /// Represents a single nodejs version info entry
/// as retrieved from nodejs.org /// as retrieved from nodejs.org
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct VersionInfo { pub struct VersionInfo {
#[serde(deserialize_with = "deserialize_prefixed_version")] #[serde(deserialize_with = "deserialize_prefixed_version")]
pub version: semver::Version, pub version: semver::Version,
@ -19,7 +19,7 @@ pub struct VersionInfo {
pub files: Vec<String>, pub files: Vec<String>,
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ModuleVersions { pub struct ModuleVersions {
pub v8: String, pub v8: String,
pub npm: Option<String>, pub npm: Option<String>,

Loading…
Cancel
Save