Merge pull request #1 from Trivernis/feature/lookup-installed

Feature/lookup installed
main
Julius Riegel 1 year ago committed by GitHub
commit 494150370b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -21,7 +21,6 @@ path = "src/main.rs"
async-trait = "0.1.62"
bincode = "1.3.3"
clap = { version = "4.1.1", features = ["derive"] }
color-eyre = "0.6.2"
crossterm = "0.25.0"
dialoguer = "0.10.3"
dirs = "4.0.0"
@ -35,7 +34,6 @@ 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"
tokio = { version = "1.24.2", features = ["rt", "macros", "tracing", "net", "fs", "time", "process"] }

@ -28,6 +28,10 @@ pub enum Command {
#[command()]
Install(InstallArgs),
/// Uninstalls the given node version
#[command()]
Uninstall(UninstallArgs),
/// Sets the specified version as the global default
#[command()]
Default(DefaultArgs),
@ -69,6 +73,12 @@ pub struct InstallArgs {
pub version: NodeVersion,
}
#[derive(Clone, Debug, Parser)]
pub struct UninstallArgs {
/// the version to install
pub version: NodeVersion,
}
#[derive(Clone, Debug, Parser)]
pub struct DefaultArgs {
/// The version to set as default

@ -15,6 +15,7 @@ lazy_static! {
.join(PathBuf::from("nenv"));
pub static ref CFG_FILE_PATH: PathBuf = CFG_DIR.join("config.toml");
pub static ref VERSION_FILE_PATH: PathBuf = CACHE_DIR.join("versions.cache");
pub static ref INSTALLED_VERSION_FILE: PathBuf = DATA_DIR.join("installed_versions");
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}");

@ -39,6 +39,7 @@ async fn main() -> Result<()> {
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::Exec(args) => {
let exit_code = nenv.exec(args.command, args.args).await?;

@ -62,6 +62,24 @@ impl Nenv {
}
}
#[tracing::instrument(skip(self))]
pub async fn uninstall(&mut self, version: NodeVersion) -> Result<()> {
if prompt(
false,
format!(
"Do you really want to uninstall node {}?",
version.to_string().bold()
),
) {
self.repo.uninstall(&version).await?;
println!("Node {} has been removed.", version.to_string().bold())
} else {
println!("Nothing changed.");
}
Ok(())
}
/// Sets the system-wide default version
#[tracing::instrument(skip(self))]
pub async fn set_system_default(&mut self, version: NodeVersion) -> Result<()> {
@ -108,8 +126,11 @@ impl Nenv {
/// Lists the currently installed versions
#[tracing::instrument(skip(self))]
pub async fn list_versions(&mut self) -> Result<()> {
let versions = self.repo.installed_versions().await?;
let active_version = self.repo.lookup_version(&self.active_version).await?;
let versions = self.repo.installed_versions();
let active_version = self
.repo
.lookup_remote_version(&self.active_version)
.await?;
let active_version = active_version.version.into();
println!("{}", "Installed versions:".bold());
@ -216,8 +237,7 @@ impl Nenv {
async fn get_mapper(&mut self) -> Result<Mapper> {
let node_path = self
.repo
.get_version_path(&self.active_version)
.await?
.get_version_path(&self.active_version)?
.ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?;
Ok(Mapper::new(node_path))
}

@ -16,7 +16,7 @@ use super::VersionInfo;
#[derive(Clone, Serialize, Deserialize)]
pub struct Versions {
lts_versions: HashMap<String, u16>,
lts_versions: HashMap<String, u8>,
versions: HashMap<SimpleVersion, VersionMetadata>,
// as this field is not serialized
// it needs to be calculated after serialization
@ -53,7 +53,7 @@ impl Versions {
pub fn new(all_versions: Vec<VersionInfo>) -> Self {
let lts_versions = all_versions
.iter()
.filter_map(|v| Some((v.lts.lts_ref()?.to_lowercase(), v.version.major as u16)))
.filter_map(|v| Some((v.lts.lts_ref()?.to_lowercase(), v.version.major as u8)))
.collect::<HashMap<_, _>>();
let mut sorted_versions = all_versions
.iter()
@ -133,7 +133,7 @@ impl Versions {
/// Returns any version that fulfills the given requirement
#[tracing::instrument(level = "debug", skip(self))]
fn get_latest_for_major(&self, major: u16) -> Option<&VersionMetadata> {
fn get_latest_for_major(&self, major: u8) -> Option<&VersionMetadata> {
let fulfilling_versions = self
.sorted_versions
.iter()

@ -0,0 +1,108 @@
use std::{fs::File, io::Write};
use semver::VersionReq;
use serde::{Deserialize, Serialize};
use crate::{
consts::INSTALLED_VERSION_FILE,
versioning::{SimpleVersion, VersionMetadata},
};
use miette::{Context, IntoDiagnostic, Result};
#[derive(Serialize, Deserialize, Default)]
pub struct InstalledVersions {
ordered_versions: Vec<(SimpleVersion, VersionMetadata)>,
}
impl InstalledVersions {
pub fn new(mut versions: Vec<(SimpleVersion, VersionMetadata)>) -> Self {
versions.sort_by_key(|e| e.0);
versions.dedup_by_key(|e| e.0);
Self {
ordered_versions: versions,
}
}
/// Loads the local versions
pub fn load() -> Result<Self> {
let reader = File::open(&*INSTALLED_VERSION_FILE)
.into_diagnostic()
.context("Opening local versions file")?;
let versions = bincode::deserialize_from(reader)
.into_diagnostic()
.context("Deserializing local versions")?;
Ok(versions)
}
/// Saves the local versions
pub fn save(&self) -> Result<()> {
let mut file = File::create(&*INSTALLED_VERSION_FILE)
.into_diagnostic()
.context("Opening local versions file")?;
bincode::serialize_into(&mut file, &self)
.into_diagnostic()
.context("Serializing local versions")?;
file.flush()
.into_diagnostic()
.context("Flushing local versions to file")?;
Ok(())
}
/// Inserts a new version. This requires reordering the list
pub fn insert(&mut self, version: (SimpleVersion, VersionMetadata)) {
self.ordered_versions.push(version);
self.ordered_versions.sort_by_key(|e| e.0);
self.ordered_versions.dedup_by_key(|e| e.0);
}
/// Removes a version. This keeps the order intact
pub fn remove(&mut self, version: &SimpleVersion) {
self.ordered_versions.retain(|(v, _)| v != version)
}
pub fn all(&self) -> Vec<&SimpleVersion> {
self.ordered_versions.iter().map(|(v, _)| v).collect()
}
pub fn latest(&self) -> Option<&VersionMetadata> {
self.ordered_versions.last().map(|(_, m)| m)
}
pub fn latest_lts(&self) -> Option<&VersionMetadata> {
self.ordered_versions
.iter()
.filter(|(_, m)| m.lts.is_some())
.last()
.map(|(_, m)| m)
}
pub fn lts<S: AsRef<str>>(&self, lts: S) -> Option<&VersionMetadata> {
self.ordered_versions
.iter()
.filter_map(|(v, m)| Some((v, m.lts.clone()?, m)))
.filter(|(_, n, _)| n == lts.as_ref())
.last()
.map(|(_, _, m)| m)
}
pub fn fulfilling(&self, req: &VersionReq) -> Option<&VersionMetadata> {
self.ordered_versions
.iter()
.filter(|(v, _)| req.matches(&v.to_owned().into()))
.last()
.map(|(_, m)| m)
}
}
impl From<Vec<VersionMetadata>> for InstalledVersions {
fn from(versions: Vec<VersionMetadata>) -> Self {
let versions = versions
.into_iter()
.map(|v| (v.version.to_owned(), v))
.collect::<Vec<_>>();
Self::new(versions)
}
}

@ -13,10 +13,11 @@ use crate::{
versioning::{SimpleVersion, VersionMetadata},
};
use miette::{IntoDiagnostic, Result};
use miette::{Context, IntoDiagnostic, Result};
use self::{
downloader::{versions::Versions, NodeDownloader},
local_versions::InstalledVersions,
node_path::NodePath,
};
@ -91,6 +92,7 @@ impl fmt::Display for NodeVersion {
pub struct Repository {
downloader: NodeDownloader,
installed_versions: InstalledVersions,
}
impl Repository {
@ -98,9 +100,24 @@ impl Repository {
#[tracing::instrument(level = "debug", skip_all)]
pub async fn init(config: ConfigAccess) -> Result<Self> {
Self::create_folders().await?;
let downloader = NodeDownloader::new(config.clone());
let mut downloader = NodeDownloader::new(config.clone());
let installed_versions = match InstalledVersions::load() {
Ok(v) => v,
Err(_) => {
let installed: InstalledVersions =
load_installed_versions_info(downloader.versions().await?)
.await?
.into();
installed.save()?;
installed
}
};
Ok(Self { downloader })
Ok(Self {
downloader,
installed_versions,
})
}
#[tracing::instrument(level = "debug")]
@ -132,8 +149,8 @@ impl Repository {
/// Returns the path for the given node version
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_version_path(&mut self, version: &NodeVersion) -> Result<Option<NodePath>> {
let info = self.lookup_version(version).await?;
pub fn get_version_path(&mut self, version: &NodeVersion) -> Result<Option<NodePath>> {
let info = self.lookup_local_version(version)?;
let path = build_version_path(&info.version);
Ok(if path.exists() {
@ -144,24 +161,22 @@ impl Repository {
}
/// Returns a list of installed versions
#[tracing::instrument(level = "debug", skip(self))]
pub async fn installed_versions(&self) -> Result<Vec<Version>> {
let mut versions = Vec::new();
let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await.into_diagnostic()?;
while let Some(entry) = iter.next_entry().await.into_diagnostic()? {
if let Ok(version) = Version::parse(entry.file_name().to_string_lossy().as_ref()) {
versions.push(version);
};
}
Ok(versions)
pub fn installed_versions(&self) -> Vec<Version> {
self.installed_versions
.all()
.into_iter()
.map(|v| v.clone().into())
.collect()
}
/// Returns if the given version is installed
#[tracing::instrument(level = "debug", skip(self))]
pub async fn is_installed(&mut self, version: &NodeVersion) -> Result<bool> {
let info = self.lookup_version(version).await?;
let info = if let Ok(v) = self.lookup_local_version(version) {
v
} else {
self.lookup_remote_version(version).await?
};
Ok(build_version_path(&info.version).exists())
}
@ -169,15 +184,40 @@ impl Repository {
/// Installs the given node version
#[tracing::instrument(level = "debug", skip(self))]
pub async fn install_version(&mut self, version: &NodeVersion) -> Result<()> {
let info = self.lookup_version(version).await?.to_owned();
let info = self.lookup_remote_version(version).await?.to_owned();
self.downloader.download(&info.version).await?;
self.installed_versions.insert((info.version, info));
self.installed_versions.save()?;
Ok(())
}
/// Uninstalls the given node version by deleting the versions directory
#[tracing::instrument(level = "debug", skip(self))]
pub async fn uninstall(&mut self, version: &NodeVersion) -> Result<()> {
let info = self.lookup_local_version(version)?.clone();
let version_dir = NODE_VERSIONS_DIR.join(info.version.to_string());
if !version_dir.exists() {
return Err(VersionError::not_installed(version).into());
}
fs::remove_dir_all(version_dir)
.await
.into_diagnostic()
.context("Deleting node version")?;
self.installed_versions.remove(&info.version);
self.installed_versions.save()?;
Ok(())
}
/// Performs a lookup for the given node version
#[tracing::instrument(level = "debug", skip(self))]
pub async fn lookup_version(&mut self, version_req: &NodeVersion) -> Result<&VersionMetadata> {
pub async fn lookup_remote_version(
&mut self,
version_req: &NodeVersion,
) -> Result<&VersionMetadata> {
let versions = self.downloader.versions().await?;
let version = match version_req {
@ -194,6 +234,28 @@ impl Repository {
Ok(version)
}
/// Performs a lookup for the given node version
#[tracing::instrument(level = "debug", skip(self))]
pub fn lookup_local_version(&self, version_req: &NodeVersion) -> Result<&VersionMetadata> {
let versions = &self.installed_versions;
let version = match version_req {
NodeVersion::Latest => versions
.latest()
.ok_or_else(|| VersionError::not_installed("latest"))?,
NodeVersion::LatestLts => versions
.latest_lts()
.ok_or_else(|| VersionError::not_installed("lts"))?,
NodeVersion::Lts(lts) => versions
.lts(lts)
.ok_or_else(|| VersionError::unknown_version(lts.to_owned()))?,
NodeVersion::Req(req) => versions
.fulfilling(req)
.ok_or_else(|| VersionError::unfulfillable_version(req.to_owned()))?,
};
Ok(version)
}
/// Returns the reference to all known versions
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all_versions(&mut self) -> Result<&Versions> {
@ -206,3 +268,21 @@ fn build_version_path(version: &SimpleVersion) -> PathBuf {
.join(version.to_string())
.join(format!("node-v{}-{}-{}", version, OS, ARCH))
}
async fn load_installed_versions_info(versions: &Versions) -> Result<Vec<VersionMetadata>> {
let mut installed_versions = Vec::new();
let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await.into_diagnostic()?;
while let Some(entry) = iter.next_entry().await.into_diagnostic()? {
if let Ok(version) = Version::parse(entry.file_name().to_string_lossy().as_ref()) {
installed_versions.push(version);
};
}
let versions = installed_versions
.into_iter()
.filter_map(|v| versions.get(&v))
.cloned()
.collect();
Ok(versions)
}

@ -3,7 +3,7 @@ use std::{
time::Duration,
};
use dialoguer::Confirm;
use dialoguer::{theme::ColorfulTheme, Confirm};
use indicatif::{ProgressBar, ProgressStyle};
pub fn progress_bar(total: u64) -> ProgressBar {
@ -32,7 +32,7 @@ pub fn progress_spinner() -> ProgressBar {
}
pub fn prompt<S: ToString>(default: bool, prompt: S) -> bool {
Confirm::new()
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(prompt.to_string())
.default(default)
.interact()

@ -6,9 +6,9 @@ use crate::repository::downloader::VersionInfo;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize, Hash)]
pub struct SimpleVersion {
pub major: u16,
pub minor: u16,
pub patch: u32,
pub major: u8,
pub minor: u8,
pub patch: u16,
}
#[derive(Clone, Serialize, Deserialize)]
@ -22,9 +22,9 @@ pub struct VersionMetadata {
impl From<semver::Version> for SimpleVersion {
fn from(value: semver::Version) -> Self {
Self {
major: value.major as u16,
minor: value.minor as u16,
patch: value.patch as u32,
major: value.major as u8,
minor: value.minor as u8,
patch: value.patch as u16,
}
}
}

Loading…
Cancel
Save