Improve the version cache to be way faster and smaller

feature/lookup-installed
trivernis 2 years ago
parent 9a7c91620e
commit 309189cb88
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -12,7 +12,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"
@ -32,7 +34,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"

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

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

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

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

@ -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<OsString>) -> Result<ExitStatus> {
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?;

@ -20,6 +20,7 @@ pub struct Nenv {
}
impl Nenv {
#[tracing::instrument(level = "debug")]
pub async fn init() -> Result<Self> {
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<OsString>) -> Result<i32> {
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<NodeVersion> {
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<Mapper> {
let node_path = self
.repo

@ -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> {
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<Option<NodePath>> {
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<Vec<Version>> {
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<bool> {
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<PathBuf> {
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<Versions> {
let versions = if let Some(v) = Versions::load().await {
v

@ -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<String, VersionReq>,
versions: HashMap<Version, VersionInfo>,
sorted_versions: Vec<Version>,
lts_versions: HashMap<String, u16>,
versions: HashMap<SimpleVersion, SimpleVersionInfo>,
sorted_versions: Vec<SimpleVersion>,
}
#[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<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,
}
}
}
impl From<SimpleVersion> 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<String>,
}
impl From<VersionInfo> 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<VersionInfo>) -> 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::<HashMap<_, _>>();
let mut sorted_versions = all_versions
.iter()
.map(|v| v.version.to_owned())
.map(|v| v.version.to_owned().into())
.collect::<Vec<_>>();
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::<HashMap<_, _>>();
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::<Vec<_>>();
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<S: AsRef<str>>(&self, lts_name: S) -> Option<&VersionInfo> {
#[tracing::instrument(level = "debug", skip(self))]
pub fn get_lts<S: AsRef<str> + 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::<Vec<_>>();
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::<Vec<_>>();
let version = fulfilling_versions.last()?;
self.versions.get(&version).into()
}
}

@ -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<Vec<VersionInfo>> {
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<W: AsyncWrite + Unpin, S: Display + Debug>(
&self,
version: S,

@ -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<String>,
#[serde(deserialize_with = "deserialize_false_as_none")]
pub lts: Option<String>,
pub lts: LtsInfo,
pub security: bool,
#[serde(flatten)]
pub module_versions: ModuleVersions,
pub files: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ModuleVersions {
pub v8: String,
pub npm: Option<String>,
pub uv: Option<String>,
pub zlib: Option<String>,
pub openssl: Option<String>,
pub files: Vec<String>,
}
fn deserialize_false_as_none<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<String>, 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<String> {
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<semver::Version, D::Error> {
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)
}

Loading…
Cancel
Save