Add support for pinning binaries to specific node versions

main
trivernis 1 year ago
parent 494150370b
commit ccdfe83d8a
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -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<NodeVersion>,
#[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

@ -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))
}

@ -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<String, ExecutableConfig>,
}
#[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 {

@ -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> {
Nenv::init().await
async fn get_nenv(version_override: Option<&NodeVersion>) -> Result<Nenv> {
Nenv::init(version_override).await
}
fn init_tracing() {

@ -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<PathBuf>) -> 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<Vec<NodeApp>> {
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()));
}
}

@ -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
}
}

@ -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<Self> {
pub async fn init(version_override: Option<&NodeVersion>) -> 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);
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<OsString>) -> Result<i32> {
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<Vec<(String, NodePath)>> {
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)
}
}

@ -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(())
}

Loading…
Cancel
Save