Add support for pinning binaries to specific node versions

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

@ -10,6 +10,10 @@ pub struct Args {
#[arg(long)] #[arg(long)]
pub verbose: bool, pub verbose: bool,
/// Overrides all versions found in the environment and uses this one instead
#[arg(long)]
pub use_version: Option<NodeVersion>,
#[command(subcommand)] #[command(subcommand)]
pub command: Command, pub command: Command,
} }
@ -34,7 +38,7 @@ pub enum Command {
/// Sets the specified version as the global default /// Sets the specified version as the global default
#[command()] #[command()]
Default(DefaultArgs), SetDefault(DefaultArgs),
/// Creates wrapper scripts for node binaries /// Creates wrapper scripts for node binaries
/// so they can be found in the path and are executed /// so they can be found in the path and are executed

@ -71,6 +71,7 @@ impl ConfigAccess {
let cfg = toml::from_str(&cfg_string) let cfg = toml::from_str(&cfg_string)
.map_err(|e| ParseConfigError::new("config.toml", cfg_string, e))?; .map_err(|e| ParseConfigError::new("config.toml", cfg_string, e))?;
tracing::debug!("{cfg:?}");
Ok(Self::new(cfg)) Ok(Self::new(cfg))
} }

@ -1,3 +1,5 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{consts::NODE_DIST_URL, repository::NodeVersion}; use crate::{consts::NODE_DIST_URL, repository::NodeVersion};
@ -9,6 +11,10 @@ pub struct Config {
/// Configuration for how to download node versions /// Configuration for how to download node versions
pub download: DownloadConfig, 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)] #[derive(Serialize, Deserialize, Clone, Debug)]
@ -23,6 +29,14 @@ pub struct DownloadConfig {
pub dist_base_url: String, 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 { impl Default for NodeConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {

@ -11,6 +11,7 @@ pub mod mapper;
pub mod repository; pub mod repository;
mod utils; mod utils;
use miette::Result; use miette::Result;
use repository::NodeVersion;
use tracing::metadata::LevelFilter; use tracing::metadata::LevelFilter;
use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::fmt::format::FmtSpan;
use xkcd_unreachable::xkcd_unreachable; use xkcd_unreachable::xkcd_unreachable;
@ -35,12 +36,12 @@ async fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
let mut nenv = get_nenv().await?; let mut nenv = get_nenv(args.use_version.as_ref()).await?;
match args.command { match args.command {
args::Command::Install(v) => nenv.install(v.version).await, args::Command::Install(v) => nenv.install(v.version).await,
args::Command::Uninstall(v) => nenv.uninstall(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) => { args::Command::Exec(args) => {
let exit_code = nenv.exec(args.command, args.args).await?; 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")); println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
} }
async fn get_nenv() -> Result<Nenv> { async fn get_nenv(version_override: Option<&NodeVersion>) -> Result<Nenv> {
Nenv::init().await Nenv::init(version_override).await
} }
fn init_tracing() { 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 crate::{consts::BIN_DIR, error::MapDirError, repository::node_path::NodePath};
use miette::{Context, IntoDiagnostic, Result}; use miette::{Context, IntoDiagnostic, Result};
struct NodeApp { pub struct NodeApp {
info: DirEntry, path: PathBuf,
name: String, name: String,
} }
impl NodeApp { impl NodeApp {
pub fn new(info: DirEntry) -> Self { pub fn new(path: PathBuf) -> Self {
let path = info.path();
let name = path.file_stem().unwrap(); let name = path.file_stem().unwrap();
let name = name.to_string_lossy().into_owned(); let name = name.to_string_lossy().into_owned();
Self { info, name } Self { path, name }
} }
pub fn name(&self) -> &String { pub fn name(&self) -> &String {
@ -25,7 +29,11 @@ impl NodeApp {
/// creates wrappers to map this application /// creates wrappers to map this application
pub async fn map_executable(&self) -> Result<()> { 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) self.write_wrapper_script(&src_path)
.await .await
.into_diagnostic() .into_diagnostic()
@ -35,7 +43,7 @@ impl NodeApp {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
async fn write_wrapper_script(&self, path: &Path) -> Result<(), io::Error> { async fn write_wrapper_script(&self, path: &Path) -> Result<(), io::Error> {
fs::write(path, format!("#!/bin/sh\nnenv exec {} \"$@\"", self.name)).await?; 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?; fs::set_permissions(&path, src_metadata.permissions()).await?;
Ok(()) Ok(())
@ -48,13 +56,28 @@ impl NodeApp {
format!("@echo off\nnenv exec {} %*", self.name), format!("@echo off\nnenv exec {} %*", self.name),
) )
.await?; .await?;
let src_metadata = self.info.metadata().await?; let src_metadata = self.path.metadata()?;
fs::set_permissions(&path, src_metadata.permissions()).await?; fs::set_permissions(&path, src_metadata.permissions()).await?;
Ok(()) 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<()> { 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?
@ -87,7 +110,7 @@ async fn get_applications(path: &Path) -> Result<Vec<NodeApp>> {
let entry_path = entry.path(); let entry_path = entry.path();
if entry_path.is_file() && !exclude_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 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}; use miette::{IntoDiagnostic, Result};
mod mapped_command; mod mapped_command;
@ -46,4 +49,15 @@ impl Mapper {
Ok(()) 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}, consts::{BIN_DIR, CACHE_DIR, VERSION_FILE_PATH},
error::VersionError, error::VersionError,
mapper::Mapper, mapper::Mapper,
repository::{NodeVersion, Repository}, repository::{node_path::NodePath, NodeVersion, Repository},
utils::prompt, utils::prompt,
version_detection::{self, VersionDetector}, version_detection::{self, VersionDetector},
}; };
@ -22,11 +22,16 @@ pub struct Nenv {
impl Nenv { impl Nenv {
#[tracing::instrument(level = "debug")] #[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 config = ConfigAccess::load().await?;
let repo = Repository::init(config.clone()).await?; let repo = Repository::init(config.clone()).await?;
let default_version = { config.get().await.node.default_version.to_owned() }; 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 { Ok(Self {
config, config,
@ -55,7 +60,11 @@ impl Nenv {
} else { } else {
self.repo.install_version(&version).await?; self.repo.install_version(&version).await?;
self.active_version = version.to_owned(); 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()); println!("Installed {}", version.to_string().bold());
Ok(()) Ok(())
@ -109,6 +118,9 @@ impl Nenv {
/// Executes a given node executable for the currently active version /// Executes a given node executable for the currently active version
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub async fn exec(&mut self, command: String, args: Vec<OsString>) -> Result<i32> { 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? { if !self.repo.is_installed(&self.active_version).await? {
self.repo.install_version(&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 /// Clears the version cache and remaps all executables
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub async fn remap(&mut self) -> Result<()> { 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 /// Lists the currently installed versions
@ -241,4 +255,18 @@ impl Nenv {
.ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?; .ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?;
Ok(Mapper::new(node_path)) 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, &*BIN_DIR,
&*NODE_VERSIONS_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() { if !dir.exists() {
fs::create_dir_all(dir).await.into_diagnostic()?; fs::create_dir_all(dir).await?;
} }
Ok(()) Result::<(), std::io::Error>::Ok(())
})) }))
.await .await
{ .into_iter()
#[allow(clippy::question_mark)] .fold(Result::Ok(()), |acc, res| acc.and_then(|_| res))
if let Err(e) = result { .into_diagnostic()
return Err(e); .wrap_err("Failed to create application directory")?;
}
}
Ok(()) Ok(())
} }

Loading…
Cancel
Save