Move commands to nenv instance

All commands are moved to the nenv instance. This
also changes responsibilities slighly and introduces
a shared config object
feature/lookup-installed
trivernis 1 year ago
parent 9c7b588865
commit ad1afd3173
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -33,5 +33,6 @@ tokio = { version = "1.24.2", features = ["rt", "macros", "tracing", "net", "fs"
toml = "0.5.11" toml = "0.5.11"
tracing = "0.1.37" tracing = "0.1.37"
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"
xkcd_unreachable = "0.1.1"
zip = "0.6.3" zip = "0.6.3"

@ -7,7 +7,7 @@ use clap::{Parser, Subcommand};
#[clap(infer_subcommands = true)] #[clap(infer_subcommands = true)]
pub struct Args { pub struct Args {
#[command(subcommand)] #[command(subcommand)]
pub commmand: Command, pub command: Command,
} }
#[derive(Clone, Debug, Subcommand)] #[derive(Clone, Debug, Subcommand)]
@ -24,7 +24,8 @@ pub enum Command {
#[command()] #[command()]
Default(DefaultArgs), Default(DefaultArgs),
/// Refreshes the node environment mappings and cache /// Refreshes the node environment mappings and cache.
/// This will erase all binary mappings not relevant to the current node version.
#[command()] #[command()]
Refresh, Refresh,

@ -0,0 +1,111 @@
use std::ops::{Deref, DerefMut};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use miette::Context;
use miette::{IntoDiagnostic, Result};
use tokio::fs;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::error::SerializeTomlError;
use crate::{
consts::{CFG_DIR, CFG_FILE_PATH},
error::ParseConfigError,
};
use self::value::Config;
mod value;
#[derive(Clone)]
pub struct ConfigAccess {
dirty: Arc<AtomicBool>,
config: Arc<RwLock<Config>>,
}
pub struct ModifyGuard<'a, T>(ConfigAccess, RwLockWriteGuard<'a, T>);
impl<'a, T> Deref for ModifyGuard<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.1
}
}
impl<'a, T> DerefMut for ModifyGuard<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.1
}
}
impl<'a, T> Drop for ModifyGuard<'a, T> {
fn drop(&mut self) {
self.0
.dirty
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
impl ConfigAccess {
/// Loads the config file from the default config path
pub async fn load() -> Result<Self> {
if !CFG_FILE_PATH.exists() {
if !CFG_DIR.exists() {
fs::create_dir_all(&*CFG_DIR)
.await
.into_diagnostic()
.context("creating config dir")?;
}
let cfg = Config::default();
let access = Self::new(cfg);
access.save().await?;
Ok(access)
} else {
let cfg_string = fs::read_to_string(&*CFG_FILE_PATH)
.await
.into_diagnostic()
.context("reading config file")?;
let cfg = toml::from_str(&cfg_string)
.map_err(|e| ParseConfigError::new("config.toml", cfg_string, e))?;
Ok(Self::new(cfg))
}
}
pub async fn get(&self) -> RwLockReadGuard<Config> {
if self.dirty.swap(false, std::sync::atomic::Ordering::Relaxed) {
self.save().await.expect("Failed so save config");
}
self.config.read().await
}
pub async fn get_mut(&self) -> ModifyGuard<Config> {
if self.dirty.swap(false, std::sync::atomic::Ordering::Relaxed) {
self.save().await.expect("Failed so save config");
}
ModifyGuard(self.clone(), self.config.write().await)
}
fn new(config: Config) -> Self {
Self {
dirty: Arc::new(AtomicBool::new(false)),
config: Arc::new(RwLock::new(config)),
}
}
pub async fn save(&self) -> Result<()> {
fs::write(
&*CFG_FILE_PATH,
toml::to_string_pretty(&*self.config.read().await).map_err(SerializeTomlError::from)?,
)
.await
.into_diagnostic()
.context("writing config file")?;
Ok(())
}
}

@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
use crate::{consts::NODE_DIST_URL, repository::NodeVersion};
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct Config {
/// Node execution related config
pub node: NodeConfig,
/// Configuration for how to download node versions
pub download: DownloadConfig,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NodeConfig {
/// The default version if no version is specified
/// in the `package.json` file or `NODE_VERSION` environment variable
#[serde(with = "NodeVersion")]
pub default_version: NodeVersion,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DownloadConfig {
pub dist_base_url: String,
}
impl Default for NodeConfig {
fn default() -> Self {
Self {
default_version: NodeVersion::LatestLts,
}
}
}
impl Default for DownloadConfig {
fn default() -> Self {
Self {
dist_base_url: String::from(NODE_DIST_URL),
}
}
}

@ -2,12 +2,8 @@ use std::process;
use args::Args; use args::Args;
use clap::Parser; use clap::Parser;
use std::ffi::OsString;
use consts::VERSION_FILE_PATH; use nenv::Nenv;
use crossterm::style::Stylize;
use mapper::Mapper;
use repository::{config::Config, NodeVersion, Repository};
mod consts; mod consts;
pub mod error; pub mod error;
@ -15,35 +11,41 @@ pub mod mapper;
pub mod repository; pub mod repository;
mod utils; mod utils;
mod web_api; mod web_api;
use dialoguer::Confirm; use miette::Result;
use miette::{IntoDiagnostic, Result}; use xkcd_unreachable::xkcd_unreachable;
use tokio::fs;
use crate::error::VersionError;
mod args; mod args;
mod config;
mod nenv;
mod version_detection;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> { async fn main() -> Result<()> {
miette::set_panic_hook(); miette::set_panic_hook();
let args: Args = Args::parse(); let args: Args = Args::parse();
match args.commmand { if let args::Command::Version = &args.command {
args::Command::Version => { print_version();
print_version(); return Ok(());
Ok(()) }
}
args::Command::Install(v) => install_version(v.version).await, let mut nenv = get_nenv().await?;
args::Command::Default(v) => set_default_version(v.version).await,
match args.command {
args::Command::Install(v) => nenv.install(v.version).await,
args::Command::Default(v) => nenv.set_system_default(v.version).await,
args::Command::Exec(args) => { args::Command::Exec(args) => {
let exit_code = exec(args.command, args.args).await?; let exit_code = nenv.exec(args.command, args.args).await?;
process::exit(exit_code); process::exit(exit_code);
} }
args::Command::Refresh => refresh().await, args::Command::Refresh => nenv.refresh().await,
args::Command::ListVersions => list_versions().await, args::Command::ListVersions => nenv.list_versions().await,
_ => xkcd_unreachable!(),
}?; }?;
nenv.persist().await?;
Ok(()) Ok(())
} }
@ -51,116 +53,6 @@ fn print_version() {
println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
} }
/// Installs a given node version async fn get_nenv() -> Result<Nenv> {
async fn install_version(version: NodeVersion) -> Result<()> { Nenv::init().await
if VERSION_FILE_PATH.exists() {
fs::remove_file(&*VERSION_FILE_PATH)
.await
.into_diagnostic()?;
}
let repo = get_repository().await?;
if repo.is_installed(&version)?
&& !Confirm::new()
.with_prompt(format!(
"The version {} is already installed. Reinstall?",
version.to_string().bold()
))
.default(false)
.interact()
.unwrap()
{
return Ok(());
}
repo.install_version(&version).await?;
println!("Installed {}", version.to_string().bold());
Ok(())
}
/// Sets a default system wide node version
async fn set_default_version(version: NodeVersion) -> Result<()> {
let mut mapper = get_mapper().await?;
if !mapper.repository().is_installed(&version)?
&& Confirm::new()
.with_prompt(format!(
"The version {version} is not installed. Do you want to install it?"
))
.default(false)
.interact()
.unwrap()
{
mapper.repository().install_version(&version).await?;
}
mapper.set_default_version(&version).await?;
println!("Now using {}", version.to_string().bold());
Ok(())
}
/// Exectues a given command
#[inline]
async fn exec(command: String, args: Vec<OsString>) -> Result<i32> {
let mapper = get_mapper().await?;
let active_version = mapper.active_version();
if !mapper.repository().is_installed(active_version)? {
mapper.repository().install_version(active_version).await?;
}
let exit_status = mapper.exec(command, args).await?;
Ok(exit_status.code().unwrap_or(0))
}
/// Refreshes the version cache and mapped binaries
async fn refresh() -> Result<()> {
get_mapper().await?.remap().await?;
fs::remove_file(&*VERSION_FILE_PATH)
.await
.into_diagnostic()?;
println!("Remapped binaries and cleared version cache");
Ok(())
}
/// Lists all available node versions
async fn list_versions() -> Result<()> {
let mapper = get_mapper().await?;
let versions = mapper.repository().installed_versions().await?;
let active_version = mapper
.repository()
.lookup_version(mapper.active_version())?;
println!("{}", "Installed versions:".bold());
for version in versions {
let info = mapper
.repository()
.all_versions()
.get(&version)
.ok_or_else(|| VersionError::unknown_version(version.to_string()))?;
let lts = info
.lts
.as_ref()
.map(|l| format!(" ({})", l.to_owned().green()))
.unwrap_or_default();
if version == active_version.version {
println!(" {}{} [current]", version.to_string().blue().bold(), lts)
} else {
println!(" {}{}", version.to_string().blue(), lts)
}
}
Ok(())
}
async fn get_repository() -> Result<Repository> {
Repository::init(Config::load().await?).await
}
async fn get_mapper() -> Result<Mapper> {
Ok(Mapper::load(get_repository().await?).await)
} }

@ -55,7 +55,7 @@ impl NodeApp {
} }
} }
pub async fn map_node_bin(node_path: NodePath) -> Result<()> { pub async fn map_node_bin(node_path: &NodePath) -> Result<()> {
let mapped_app_names = get_applications(&BIN_DIR) let mapped_app_names = get_applications(&BIN_DIR)
.await? .await?
.iter() .iter()

@ -2,70 +2,29 @@ use std::{ffi::OsString, process::ExitStatus};
use tokio::fs; use tokio::fs;
use crate::{ use crate::{consts::BIN_DIR, repository::node_path::NodePath};
consts::BIN_DIR,
error::VersionError,
repository::{NodeVersion, Repository},
};
use self::{ use self::{mapped_command::MappedCommand, mapped_dir::map_node_bin};
mapped_command::MappedCommand,
mapped_dir::map_node_bin,
version_detection::{ParallelDetector, VersionDetector},
};
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
mod mapped_command; mod mapped_command;
mod mapped_dir; mod mapped_dir;
mod version_detection;
/// Responsible for mapping to node executables /// Responsible for mapping to node executables
/// and managing node versions /// and managing node versions
pub struct Mapper { pub struct Mapper {
repo: Repository, node_path: NodePath,
active_version: NodeVersion,
} }
impl Mapper { impl Mapper {
pub async fn load(repository: Repository) -> Self { pub fn new(node_path: NodePath) -> Self {
let version = Self::get_version() Self { node_path }
.await
.unwrap_or_else(|| repository.config.node.default_version.to_owned());
Self {
repo: repository,
active_version: version,
}
} }
pub fn repository(&self) -> &Repository {
&self.repo
}
/// Sets the given version as the default one
pub async fn set_default_version(&mut self, version: &NodeVersion) -> Result<()> {
self.repo
.config
.set_default_version(version.clone())
.await?;
self.active_version = version.clone();
self.map_active_version().await?;
Ok(())
}
pub fn active_version(&self) -> &NodeVersion {
&self.active_version
}
/// Executes a mapped command with the given node environment /// Executes a mapped command with the given node environment
pub async fn exec(&self, command: String, args: Vec<OsString>) -> Result<ExitStatus> { pub async fn exec(&self, command: String, args: Vec<OsString>) -> Result<ExitStatus> {
let node_path = self let executable = self.node_path.bin().join(&command);
.repo
.get_version_path(&self.active_version)?
.ok_or_else(|| VersionError::not_installed(&self.active_version))?;
let executable = node_path.bin().join(&command);
let exit_status = MappedCommand::new(command, executable, args).run().await?; let exit_status = MappedCommand::new(command, executable, args).run().await?;
self.map_active_version().await?; self.remap_additive().await?;
Ok(exit_status) Ok(exit_status)
} }
@ -74,25 +33,13 @@ impl Mapper {
pub async fn remap(&self) -> Result<()> { pub async fn remap(&self) -> Result<()> {
fs::remove_dir_all(&*BIN_DIR).await.into_diagnostic()?; fs::remove_dir_all(&*BIN_DIR).await.into_diagnostic()?;
fs::create_dir_all(&*BIN_DIR).await.into_diagnostic()?; fs::create_dir_all(&*BIN_DIR).await.into_diagnostic()?;
self.map_active_version().await?; self.remap_additive().await?;
Ok(()) Ok(())
} }
async fn get_version() -> Option<NodeVersion> { pub async fn remap_additive(&self) -> Result<()> {
ParallelDetector::detect_version() map_node_bin(&self.node_path).await?;
.await
.ok()
.and_then(|v| v)
}
/// creates wrapper scripts for the current version
async fn map_active_version(&self) -> Result<()> {
let dir = self
.repo
.get_version_path(&self.active_version)?
.ok_or_else(|| VersionError::not_installed(self.active_version.to_string()))?;
map_node_bin(dir).await?;
Ok(()) Ok(())
} }

@ -0,0 +1,161 @@
use std::ffi::OsString;
use crate::{
config::ConfigAccess,
consts::VERSION_FILE_PATH,
error::VersionError,
mapper::Mapper,
repository::{NodeVersion, Repository},
utils::prompt,
version_detection::{self, VersionDetector},
};
use crossterm::style::Stylize;
use miette::{IntoDiagnostic, Result};
use tokio::fs;
pub struct Nenv {
config: ConfigAccess,
repo: Repository,
active_version: NodeVersion,
}
impl Nenv {
pub async fn init() -> Result<Self> {
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);
Ok(Self {
config,
repo,
active_version,
})
}
/// Installs the given node version.
/// Prompts if that version already exists
pub async fn install(&mut self, version: NodeVersion) -> Result<()> {
Self::clear_version_cache().await?;
if self.repo.is_installed(&version)?
&& !prompt(
false,
format!(
"The version {} is already installed. Reinstall?",
version.to_string().bold()
),
)
{
println!("Nothing changed.");
Ok(())
} else {
self.repo.install_version(&version).await?;
self.active_version = version.to_owned();
self.get_mapper()?.remap_additive().await?;
println!("Installed {}", version.to_string().bold());
Ok(())
}
}
/// Sets the system-wide default version
pub async fn set_system_default(&mut self, version: NodeVersion) -> Result<()> {
self.active_version = version.to_owned();
if !self.repo.is_installed(&version)? {
if prompt(
false,
format!("The version {version} is not installed. Do you want to install it?"),
) {
self.repo.install_version(&version).await?;
self.config.get_mut().await.node.default_version = version.to_owned();
self.get_mapper()?.remap_additive().await?;
println!("Now using {}", version.to_string().bold());
}
Ok(())
} else {
self.get_mapper()?.remap_additive().await?;
self.config.get_mut().await.node.default_version = version.to_owned();
println!("Now using {}", version.to_string().bold());
Ok(())
}
}
/// Executes a given node executable for the currently active version
pub async fn exec(&self, command: String, args: Vec<OsString>) -> Result<i32> {
if !self.repo.is_installed(&self.active_version)? {
self.repo.install_version(&self.active_version).await?;
}
let exit_status = self.get_mapper()?.exec(command, args).await?;
Ok(exit_status.code().unwrap_or(0))
}
/// Persits all changes made that aren't written to the disk yet
pub async fn persist(&self) -> Result<()> {
self.config.save().await
}
/// Clears the version cache and remaps all executables
pub async fn refresh(&self) -> Result<()> {
Self::clear_version_cache().await?;
self.get_mapper()?.remap().await
}
/// Lists the currently installed versions
pub async fn list_versions(&self) -> Result<()> {
let versions = self.repo.installed_versions().await?;
let active_version = self.repo.lookup_version(&self.active_version)?;
println!("{}", "Installed versions:".bold());
for version in versions {
let info = self
.repo
.all_versions()
.get(&version)
.ok_or_else(|| VersionError::unknown_version(version.to_string()))?;
let lts = info
.lts
.as_ref()
.map(|l| format!(" ({})", l.to_owned().green()))
.unwrap_or_default();
if version == active_version.version {
println!(" {}{} [current]", version.to_string().blue().bold(), lts)
} else {
println!(" {}{}", version.to_string().blue(), lts)
}
}
Ok(())
}
async fn get_active_version() -> Option<NodeVersion> {
version_detection::ParallelDetector::detect_version()
.await
.ok()
.and_then(|v| v)
}
async fn clear_version_cache() -> Result<()> {
if VERSION_FILE_PATH.exists() {
fs::remove_file(&*VERSION_FILE_PATH)
.await
.into_diagnostic()?;
}
Ok(())
}
fn get_mapper(&self) -> Result<Mapper> {
let node_path = self
.repo
.get_version_path(&self.active_version)?
.ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?;
Ok(Mapper::new(node_path))
}
}

@ -1,95 +0,0 @@
use miette::Context;
use miette::{IntoDiagnostic, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::error::SerializeTomlError;
use crate::{
consts::{CFG_DIR, CFG_FILE_PATH, NODE_DIST_URL},
error::ParseConfigError,
};
use super::NodeVersion;
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct Config {
/// Node execution related config
pub node: NodeConfig,
/// Configuration for how to download node versions
pub download: DownloadConfig,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NodeConfig {
/// The default version if no version is specified
/// in the `package.json` file or `NODE_VERSION` environment variable
#[serde(with = "NodeVersion")]
pub default_version: NodeVersion,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DownloadConfig {
pub dist_base_url: String,
}
impl Default for NodeConfig {
fn default() -> Self {
Self {
default_version: NodeVersion::LatestLts,
}
}
}
impl Default for DownloadConfig {
fn default() -> Self {
Self {
dist_base_url: String::from(NODE_DIST_URL),
}
}
}
impl Config {
/// Loads the config file from the default config path
pub async fn load() -> Result<Self> {
if !CFG_FILE_PATH.exists() {
if !CFG_DIR.exists() {
fs::create_dir_all(&*CFG_DIR)
.await
.into_diagnostic()
.context("creating config dir")?;
}
let cfg = Config::default();
cfg.save().await?;
Ok(cfg)
} else {
let cfg_string = fs::read_to_string(&*CFG_FILE_PATH)
.await
.into_diagnostic()
.context("reading config file")?;
let cfg = toml::from_str(&cfg_string)
.map_err(|e| ParseConfigError::new("config.toml", cfg_string, e))?;
Ok(cfg)
}
}
pub async fn save(&self) -> Result<()> {
fs::write(
&*CFG_FILE_PATH,
toml::to_string_pretty(&self).map_err(SerializeTomlError::from)?,
)
.await
.into_diagnostic()
.context("writing config file")?;
Ok(())
}
pub async fn set_default_version(&mut self, default_version: NodeVersion) -> Result<()> {
self.node.default_version = default_version;
self.save().await
}
}

@ -12,6 +12,7 @@ use tokio::{
}; };
use crate::{ use crate::{
config::ConfigAccess,
consts::{ consts::{
ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR, OS, ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR, OS,
}, },
@ -21,9 +22,8 @@ use crate::{
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use self::{config::Config, node_path::NodePath, versions::Versions}; use self::{node_path::NodePath, versions::Versions};
pub mod config;
pub(crate) mod extract; pub(crate) mod extract;
pub(crate) mod node_path; pub(crate) mod node_path;
pub mod versions; pub mod versions;
@ -86,21 +86,16 @@ impl fmt::Display for NodeVersion {
pub struct Repository { pub struct Repository {
versions: Versions, versions: Versions,
web_api: WebApi, web_api: WebApi,
pub config: Config,
} }
impl Repository { impl Repository {
/// Initializes a new repository with the given confi /// Initializes a new repository with the given confi
pub async fn init(config: Config) -> Result<Self> { pub async fn init(config: ConfigAccess) -> Result<Self> {
Self::create_folders().await?; Self::create_folders().await?;
let web_api = WebApi::new(&config.download.dist_base_url); let web_api = WebApi::new(&config.get().await.download.dist_base_url);
let versions = load_versions(&web_api).await?; let versions = load_versions(&web_api).await?;
Ok(Self { Ok(Self { web_api, versions })
config,
web_api,
versions,
})
} }
async fn create_folders() -> Result<()> { async fn create_folders() -> Result<()> {

@ -1,5 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct NodePath { pub struct NodePath {
base: PathBuf, base: PathBuf,
} }

@ -3,6 +3,7 @@ use std::{
time::Duration, time::Duration,
}; };
use dialoguer::Confirm;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
pub fn progress_bar(total: u64) -> ProgressBar { pub fn progress_bar(total: u64) -> ProgressBar {
@ -30,6 +31,14 @@ pub fn progress_spinner() -> ProgressBar {
pb pb
} }
pub fn prompt<S: ToString>(default: bool, prompt: S) -> bool {
Confirm::new()
.with_prompt(prompt.to_string())
.default(default)
.interact()
.unwrap()
}
pub fn find_in_parents<P: AsRef<Path>>(origin: PathBuf, name: P) -> Option<PathBuf> { pub fn find_in_parents<P: AsRef<Path>>(origin: PathBuf, name: P) -> Option<PathBuf> {
for part in dir_parts(origin) { for part in dir_parts(origin) {
let file = part.join(&name); let file = part.join(&name);

Loading…
Cancel
Save