diff --git a/Cargo.toml b/Cargo.toml index 97f9707..2e46180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,9 @@ path = "src/main.rs" [dependencies] 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" @@ -39,7 +41,7 @@ thiserror = "1.0.38" tokio = { version = "1.24.2", features = ["rt", "macros", "tracing", "net", "fs", "time", "process"] } toml = "0.5.11" tracing = "0.1.37" -tracing-subscriber = "0.3.16" +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } xkcd_unreachable = "0.1.1" zip = "0.6.3" diff --git a/src/args.rs b/src/args.rs index 716615e..41980e8 100644 --- a/src/args.rs +++ b/src/args.rs @@ -6,6 +6,10 @@ use clap::{Parser, Subcommand}; #[derive(Clone, Debug, Parser)] #[clap(infer_subcommands = true)] pub struct Args { + /// Prints verbose logs + #[arg(long)] + pub verbose: bool, + #[command(subcommand)] pub command: Command, } diff --git a/src/consts.rs b/src/consts.rs index 9aa99af..78765d4 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -14,7 +14,7 @@ lazy_static! { .unwrap_or_else(|| PathBuf::from(".cache")) .join(PathBuf::from("nenv")); 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 VERSION_FILE_PATH: PathBuf = DATA_DIR.join("versions.cache"); 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}"); diff --git a/src/error.rs b/src/error.rs index 86356e8..dec3728 100644 --- a/src/error.rs +++ b/src/error.rs @@ -70,11 +70,11 @@ pub struct ParseJsonError { } #[derive(Debug, Error, Diagnostic)] -#[diagnostic(code(nenv::json::serialize))] -#[error("failed to serialize value to json string")] -pub struct SerializeJsonError { +#[diagnostic(code(nenv::bincode::serialize))] +#[error("failed to serialize value to bincode")] +pub struct SerializeBincodeError { #[from] - caused_by: serde_json::Error, + caused_by: bincode::Error, } #[derive(Debug, Error, Diagnostic)] diff --git a/src/main.rs b/src/main.rs index 42b9fbe..3569127 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,8 @@ pub mod repository; mod utils; mod web_api; use miette::Result; +use tracing::metadata::LevelFilter; +use tracing_subscriber::fmt::format::FmtSpan; use xkcd_unreachable::xkcd_unreachable; mod args; @@ -24,6 +26,10 @@ async fn main() -> Result<()> { miette::set_panic_hook(); let args: Args = Args::parse(); + if args.verbose { + init_tracing(); + } + if let args::Command::Version = &args.command { print_version(); return Ok(()); @@ -56,3 +62,12 @@ fn print_version() { async fn get_nenv() -> Result { Nenv::init().await } + +fn init_tracing() { + tracing_subscriber::fmt::SubscriberBuilder::default() + .with_max_level(LevelFilter::DEBUG) + .with_writer(std::io::stderr) + .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .compact() + .init(); +} diff --git a/src/mapper/mod.rs b/src/mapper/mod.rs index bffa3d0..abb4075 100644 --- a/src/mapper/mod.rs +++ b/src/mapper/mod.rs @@ -21,6 +21,7 @@ impl Mapper { Self { node_path } } /// Executes a mapped command with the given node environment + #[tracing::instrument(level = "debug", skip(self))] pub async fn exec(&self, command: String, args: Vec) -> Result { let executable = self.node_path.bin().join(&command); let exit_status = MappedCommand::new(command, executable, args).run().await?; @@ -30,6 +31,7 @@ impl Mapper { } /// Recreates all environment mappings + #[tracing::instrument(level = "debug", skip(self))] pub async fn remap(&self) -> Result<()> { fs::remove_dir_all(&*BIN_DIR).await.into_diagnostic()?; fs::create_dir_all(&*BIN_DIR).await.into_diagnostic()?; @@ -38,6 +40,7 @@ impl Mapper { Ok(()) } + #[tracing::instrument(level = "debug", skip(self))] pub async fn remap_additive(&self) -> Result<()> { map_node_bin(&self.node_path).await?; diff --git a/src/nenv.rs b/src/nenv.rs index e5b80d0..640508f 100644 --- a/src/nenv.rs +++ b/src/nenv.rs @@ -20,6 +20,7 @@ pub struct Nenv { } impl Nenv { + #[tracing::instrument(level = "debug")] pub async fn init() -> Result { let config = ConfigAccess::load().await?; let repo = Repository::init(config.clone()).await?; @@ -35,6 +36,7 @@ impl Nenv { /// Installs the given node version. /// Prompts if that version already exists + #[tracing::instrument(skip(self))] pub async fn install(&mut self, version: NodeVersion) -> Result<()> { Self::clear_version_cache().await?; @@ -60,6 +62,7 @@ impl Nenv { } /// Sets the system-wide default version + #[tracing::instrument(skip(self))] pub async fn set_system_default(&mut self, version: NodeVersion) -> Result<()> { self.active_version = version.to_owned(); @@ -85,6 +88,7 @@ impl Nenv { } /// Executes a given node executable for the currently active version + #[tracing::instrument(skip(self))] pub async fn exec(&self, command: String, args: Vec) -> Result { if !self.repo.is_installed(&self.active_version)? { self.repo.install_version(&self.active_version).await?; @@ -94,18 +98,15 @@ impl Nenv { 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 + #[tracing::instrument(skip(self))] pub async fn refresh(&self) -> Result<()> { Self::clear_version_cache().await?; self.get_mapper()?.remap().await } /// Lists the currently installed versions + #[tracing::instrument(skip(self))] pub async fn list_versions(&self) -> Result<()> { let versions = self.repo.installed_versions().await?; let active_version = self.repo.lookup_version(&self.active_version)?; @@ -134,6 +135,13 @@ impl Nenv { Ok(()) } + /// Persits all changes made that aren't written to the disk yet + #[tracing::instrument(level = "debug", skip(self))] + pub async fn persist(&self) -> Result<()> { + self.config.save().await + } + + #[tracing::instrument(level = "debug")] async fn get_active_version() -> Option { version_detection::ParallelDetector::detect_version() .await @@ -141,6 +149,7 @@ impl Nenv { .and_then(|v| v) } + #[tracing::instrument(level = "debug")] async fn clear_version_cache() -> Result<()> { if VERSION_FILE_PATH.exists() { fs::remove_file(&*VERSION_FILE_PATH) @@ -151,6 +160,7 @@ impl Nenv { Ok(()) } + #[tracing::instrument(level = "debug", skip(self))] fn get_mapper(&self) -> Result { let node_path = self .repo diff --git a/src/repository/mod.rs b/src/repository/mod.rs index ba520b2..2e28cb4 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -17,12 +17,15 @@ use crate::{ ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR, OS, }, error::VersionError, - web_api::{VersionInfo, WebApi}, + web_api::WebApi, }; use miette::{IntoDiagnostic, Result}; -use self::{node_path::NodePath, versions::Versions}; +use self::{ + node_path::NodePath, + versions::{SimpleVersionInfo, Versions}, +}; pub(crate) mod extract; pub(crate) mod node_path; @@ -90,6 +93,7 @@ pub struct Repository { impl Repository { /// Initializes a new repository with the given confi + #[tracing::instrument(level = "debug", skip_all)] pub async fn init(config: ConfigAccess) -> Result { Self::create_folders().await?; let web_api = WebApi::new(&config.get().await.download.dist_base_url); @@ -98,6 +102,7 @@ impl Repository { Ok(Self { web_api, versions }) } + #[tracing::instrument(level = "debug")] async fn create_folders() -> Result<()> { let dirs = vec![ &*CFG_DIR, @@ -116,6 +121,7 @@ impl Repository { } /// Returns the path for the given node version + #[tracing::instrument(level = "debug", skip(self))] pub fn get_version_path(&self, version: &NodeVersion) -> Result> { let info = self.lookup_version(version)?; let path = build_version_path(&info.version); @@ -128,6 +134,7 @@ impl Repository { } /// Returns a list of installed versions + #[tracing::instrument(level = "debug", skip(self))] pub async fn installed_versions(&self) -> Result> { let mut versions = Vec::new(); let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await.into_diagnostic()?; @@ -142,6 +149,7 @@ impl Repository { } /// Returns if the given version is installed + #[tracing::instrument(level = "debug", skip(self))] pub fn is_installed(&self, version: &NodeVersion) -> Result { let info = self.lookup_version(version)?; @@ -149,7 +157,11 @@ impl Repository { } /// Performs a lookup for the given node version - pub fn lookup_version(&self, version_req: &NodeVersion) -> Result<&VersionInfo, VersionError> { + #[tracing::instrument(level = "debug", skip(self))] + pub fn lookup_version( + &self, + version_req: &NodeVersion, + ) -> Result<&SimpleVersionInfo, VersionError> { let version = match version_req { NodeVersion::Latest => self.versions.latest(), NodeVersion::LatestLts => self.versions.latest_lts(), @@ -167,19 +179,22 @@ impl Repository { } /// Returns the reference to all known versions + #[tracing::instrument(level = "debug", skip(self))] pub fn all_versions(&self) -> &Versions { &self.versions } /// Installs a specified node version + #[tracing::instrument(level = "debug", skip(self))] pub async fn install_version(&self, version_req: &NodeVersion) -> Result<()> { let info = self.lookup_version(version_req)?; let archive_path = self.download_version(&info.version).await?; - self.extract_archive(info, &archive_path)?; + self.extract_archive(&info.version, &archive_path)?; Ok(()) } + #[tracing::instrument(level = "debug", skip(self))] async fn download_version(&self, version: &Version) -> Result { let download_path = CACHE_DIR.join(format!("node-v{}{}", version, *NODE_ARCHIVE_SUFFIX)); @@ -195,8 +210,9 @@ impl Repository { Ok(download_path) } - fn extract_archive(&self, info: &VersionInfo, archive_path: &Path) -> Result<()> { - let dst_path = NODE_VERSIONS_DIR.join(info.version.to_string()); + #[tracing::instrument(level = "debug", skip(self))] + fn extract_archive(&self, version: &Version, archive_path: &Path) -> Result<()> { + let dst_path = NODE_VERSIONS_DIR.join(version.to_string()); extract::extract_file(archive_path, &dst_path)?; Ok(()) @@ -204,6 +220,7 @@ impl Repository { } #[inline] +#[tracing::instrument(level = "debug", skip_all)] async fn load_versions(web_api: &WebApi) -> Result { let versions = if let Some(v) = Versions::load().await { v diff --git a/src/repository/versions.rs b/src/repository/versions.rs index 6edb555..a5b4fbd 100644 --- a/src/repository/versions.rs +++ b/src/repository/versions.rs @@ -1,17 +1,67 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; +use std::fmt::Debug; use tokio::fs; -use crate::{consts::VERSION_FILE_PATH, error::SerializeJsonError, web_api::VersionInfo}; -use miette::{IntoDiagnostic, Result}; +use crate::{consts::VERSION_FILE_PATH, error::SerializeBincodeError, web_api::VersionInfo}; +use miette::{Context, IntoDiagnostic, Result}; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct Versions { - lts_versions: HashMap, - versions: HashMap, - sorted_versions: Vec, + lts_versions: HashMap, + versions: HashMap, + sorted_versions: Vec, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize, Hash)] +pub struct SimpleVersion { + pub major: u16, + pub minor: u16, + pub patch: u32, +} + +impl From for SimpleVersion { + fn from(value: semver::Version) -> Self { + Self { + major: value.major as u16, + minor: value.minor as u16, + patch: value.patch as u32, + } + } +} + +impl From for semver::Version { + fn from(value: SimpleVersion) -> Self { + Self::new(value.major as u64, value.minor as u64, value.patch as u64) + } +} + +impl Display for SimpleVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + major, + minor, + patch, + } = self; + write!(f, "{major}.{minor}.{patch}") + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct SimpleVersionInfo { + pub version: Version, + pub lts: Option, +} + +impl From for SimpleVersionInfo { + fn from(value: VersionInfo) -> Self { + Self { + version: value.version, + lts: value.lts.lts(), + } + } } impl Versions { @@ -20,32 +70,34 @@ impl Versions { 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) + let byte_contents = fs::read(&*VERSION_FILE_PATH).await.ok()?; + + match bincode::deserialize(&byte_contents) { + Ok(versions) => Some(versions), + Err(e) => { + tracing::error!("Failed to deserialize cache {e}"); + fs::remove_file(&*VERSION_FILE_PATH).await.ok()?; + None + } + } } /// creates a new instance to access version information + #[tracing::instrument(level = "debug", skip_all)] pub fn new(all_versions: Vec) -> Self { let lts_versions = all_versions .iter() - .filter_map(|v| { - Some(( - v.lts.as_ref()?.to_lowercase(), - VersionReq::parse(&format!("{}", v.version.major)).ok()?, - )) - }) + .filter_map(|v| Some((v.lts.lts_ref()?.to_lowercase(), v.version.major as u16))) .collect::>(); let mut sorted_versions = all_versions .iter() - .map(|v| v.version.to_owned()) + .map(|v| v.version.to_owned().into()) .collect::>(); sorted_versions.sort(); let versions = all_versions .into_iter() - .map(|v| (v.version.to_owned(), v)) + .map(|v| (v.version.to_owned().into(), v.into())) .collect::>(); Self { @@ -55,52 +107,74 @@ impl Versions { } } + #[tracing::instrument(level = "debug", skip_all)] pub(crate) async fn save(&self) -> Result<()> { - let json_string = serde_json::to_string(&self).map_err(SerializeJsonError::from)?; - fs::write(&*VERSION_FILE_PATH, json_string) + let byte_content = bincode::serialize(self).map_err(SerializeBincodeError::from)?; + fs::write(&*VERSION_FILE_PATH, byte_content) .await - .into_diagnostic()?; + .into_diagnostic() + .context("Caching available node version.")?; Ok(()) } /// Returns the latest known node version - pub fn latest(&self) -> &VersionInfo { + #[tracing::instrument(level = "debug", skip_all)] + pub fn latest(&self) -> &SimpleVersionInfo { self.versions .get(self.sorted_versions.last().expect("No known node versions")) .unwrap() } /// Returns the latest node lts version - pub fn latest_lts(&self) -> &VersionInfo { + #[tracing::instrument(level = "debug", skip_all)] + pub fn latest_lts(&self) -> &SimpleVersionInfo { let mut versions = self .lts_versions .values() - .filter_map(|req| self.get_fulfilling(req)) + .filter_map(|req| self.get_latest_for_major(*req)) .collect::>(); versions.sort_by_key(|v| &v.version); versions.last().expect("No known lts node versions") } /// Returns a lts version by name - pub fn get_lts>(&self, lts_name: S) -> Option<&VersionInfo> { + #[tracing::instrument(level = "debug", skip(self))] + pub fn get_lts + Debug>(&self, lts_name: S) -> Option<&SimpleVersionInfo> { let lts_version = self.lts_versions.get(lts_name.as_ref())?; - self.get_fulfilling(lts_version) + self.get_latest_for_major(*lts_version) } /// Returns any version that fulfills the given requirement - pub fn get_fulfilling(&self, req: &VersionReq) -> Option<&VersionInfo> { + #[tracing::instrument(level = "debug", skip(self))] + pub fn get_fulfilling(&self, req: &VersionReq) -> Option<&SimpleVersionInfo> { let fulfilling_versions = self .sorted_versions .iter() + .map(|v| (*v).into()) .filter(|v| req.matches(v)) .collect::>(); - self.versions.get(fulfilling_versions.last()?) + let version = fulfilling_versions.last()?.clone().into(); + self.versions.get(&version).into() } /// Returns the info for the given version - pub fn get(&self, version: &Version) -> Option<&VersionInfo> { - self.versions.get(version) + #[tracing::instrument(level = "debug", skip(self))] + pub fn get(&self, version: &Version) -> Option<&SimpleVersionInfo> { + self.versions.get(&version.clone().into()) + } + + /// Returns any version that fulfills the given requirement + #[tracing::instrument(level = "debug", skip(self))] + fn get_latest_for_major(&self, major: u16) -> Option<&SimpleVersionInfo> { + let fulfilling_versions = self + .sorted_versions + .iter() + .filter(|v| v.major == major) + .collect::>(); + + let version = fulfilling_versions.last()?; + self.versions.get(&version).into() } } diff --git a/src/web_api/mod.rs b/src/web_api/mod.rs index 502c3eb..beac43d 100644 --- a/src/web_api/mod.rs +++ b/src/web_api/mod.rs @@ -42,7 +42,7 @@ impl WebApi { } /// Returns the list of available node versions - #[tracing::instrument(level = "trace")] + #[tracing::instrument(level = "debug")] pub async fn get_versions(&self) -> Result> { let versions = self .client @@ -61,7 +61,7 @@ impl WebApi { /// Downloads a specific node version /// and writes it to the given writer - #[tracing::instrument(level = "trace", skip(writer))] + #[tracing::instrument(level = "debug", skip(writer))] pub async fn download_version( &self, version: S, diff --git a/src/web_api/model.rs b/src/web_api/model.rs index d66f57f..e5f3649 100644 --- a/src/web_api/model.rs +++ b/src/web_api/model.rs @@ -1,49 +1,52 @@ -use std::borrow::Cow; - -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer}; /// Represents a single nodejs version info entry /// as retrieved from nodejs.org -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize)] pub struct VersionInfo { #[serde(deserialize_with = "deserialize_prefixed_version")] pub version: semver::Version, pub date: String, pub modules: Option, - #[serde(deserialize_with = "deserialize_false_as_none")] - pub lts: Option, + pub lts: LtsInfo, pub security: bool, - #[serde(flatten)] - pub module_versions: ModuleVersions, - pub files: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ModuleVersions { pub v8: String, pub npm: Option, pub uv: Option, pub zlib: Option, pub openssl: Option, + pub files: Vec, } -fn deserialize_false_as_none<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result, D::Error> { - Ok(String::deserialize(deserializer).ok()) +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum LtsInfo { + Version(String), + NotLts(bool), +} + +impl LtsInfo { + pub fn lts(self) -> Option { + match self { + LtsInfo::Version(v) => Some(v), + LtsInfo::NotLts(_) => None, + } + } + pub fn lts_ref(&self) -> Option<&String> { + match &self { + LtsInfo::Version(v) => Some(v), + LtsInfo::NotLts(_) => None, + } + } } fn deserialize_prefixed_version<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result { let version = String::deserialize(deserializer)?; - let version = if let Some(v) = version.strip_prefix('v') { - Cow::Borrowed(v) - } else { - Cow::Owned(version) - }; - let version = semver::Version::parse(version.as_ref()).map_err(serde::de::Error::custom)?; + let version = semver::Version::parse(version.trim_start_matches('v')) + .map_err(serde::de::Error::custom)?; Ok(version) }